Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03eda02c20 | ||
|
|
3b4055017e |
@@ -1,74 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
||||||
|
|
||||||
// homedir/platform are read at call time, so they can be stubbed per case.
|
|
||||||
vi.mock('node:os', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('node:os')>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
homedir: () => '/home/tester',
|
|
||||||
platform: () => mockPlatform,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let mockPlatform: NodeJS.Platform = 'linux';
|
|
||||||
|
|
||||||
const { getShellProfilePath, detectShell } = await import('../../src/platform/detect.js');
|
|
||||||
|
|
||||||
describe('getShellProfilePath', () => {
|
|
||||||
const originalShell = process.env['SHELL'];
|
|
||||||
const originalZdotdir = process.env['ZDOTDIR'];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockPlatform = 'linux';
|
|
||||||
delete process.env['ZDOTDIR'];
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (originalShell === undefined) delete process.env['SHELL'];
|
|
||||||
else process.env['SHELL'] = originalShell;
|
|
||||||
if (originalZdotdir === undefined) delete process.env['ZDOTDIR'];
|
|
||||||
else process.env['ZDOTDIR'] = originalZdotdir;
|
|
||||||
});
|
|
||||||
|
|
||||||
// The regression this guards: setupPath() in stages/finalize.ts appends the
|
|
||||||
// PATH export to whatever this returns. A line written to ~/.bashrc is
|
|
||||||
// unreachable to `bash -lc`, systemd units and agent seats, because Debian's
|
|
||||||
// default .bashrc returns early for non-interactive shells — so an install
|
|
||||||
// reported success and left `mosaic: command not found`. Same for .zshrc,
|
|
||||||
// which zsh only reads for interactive shells.
|
|
||||||
it('never targets an interactive-only rc file', () => {
|
|
||||||
for (const shell of ['/bin/bash', '/usr/bin/zsh']) {
|
|
||||||
process.env['SHELL'] = shell;
|
|
||||||
const profile = getShellProfilePath();
|
|
||||||
expect(profile).not.toMatch(/\.bashrc$/);
|
|
||||||
expect(profile).not.toMatch(/\.zshrc$/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses ~/.profile for bash', () => {
|
|
||||||
process.env['SHELL'] = '/bin/bash';
|
|
||||||
expect(getShellProfilePath()).toBe('/home/tester/.profile');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses ~/.zshenv for zsh', () => {
|
|
||||||
process.env['SHELL'] = '/usr/bin/zsh';
|
|
||||||
expect(getShellProfilePath()).toBe('/home/tester/.zshenv');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('honours ZDOTDIR for zsh', () => {
|
|
||||||
process.env['SHELL'] = '/usr/bin/zsh';
|
|
||||||
process.env['ZDOTDIR'] = '/custom/zdot';
|
|
||||||
expect(getShellProfilePath()).toBe('/custom/zdot/.zshenv');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to ~/.profile for an unknown shell', () => {
|
|
||||||
process.env['SHELL'] = '/bin/somethingelse';
|
|
||||||
expect(detectShell()).toBe('unknown');
|
|
||||||
expect(getShellProfilePath()).toBe('/home/tester/.profile');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('still routes fish to its own config', () => {
|
|
||||||
process.env['SHELL'] = '/usr/bin/fish';
|
|
||||||
expect(getShellProfilePath()).toBe('/home/tester/.config/fish/config.fish');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -35,6 +35,18 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||||
|
|
||||||
|
# Normalize the ambient umask so directory modes are a property of the installer
|
||||||
|
# and not of whatever shell invoked it (#1236). Debian/Ubuntu ship umask 002, so
|
||||||
|
# every `mkdir -p` below yielded 0775 — and the fleet env boundary rejects any
|
||||||
|
# managed directory with `mode & 0o022`, which made `mosaic fleet init --write`
|
||||||
|
# impossible on a stock install of those distros. Fedora/RHEL ship 022 and did
|
||||||
|
# not trip it, so the product worked or did not depending on the operator's
|
||||||
|
# login shell. 022 is what this script already assumes it produces: see the
|
||||||
|
# umask note in make_durable_snapshot, which restores to the ambient value
|
||||||
|
# precisely so "every later sync copy and new framework dir" gets 0644/0755.
|
||||||
|
# Now that value is 022 rather than whatever was inherited.
|
||||||
|
umask 022
|
||||||
|
|
||||||
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
|
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
|
||||||
# never an environment variable — so this opt-out can never sit silently
|
# never an environment variable — so this opt-out can never sit silently
|
||||||
# inherited in a shell profile. See #869 Point-1 C2.
|
# inherited in a shell profile. See #869 Point-1 C2.
|
||||||
@@ -696,6 +708,52 @@ sync_framework
|
|||||||
mkdir -p "$TARGET_DIR/memory"
|
mkdir -p "$TARGET_DIR/memory"
|
||||||
mkdir -p "$TARGET_DIR/credentials"
|
mkdir -p "$TARGET_DIR/credentials"
|
||||||
|
|
||||||
|
# Three directories must be 0700, not merely not-group-writable (#1236).
|
||||||
|
# The fleet code guards them with two different masks in two different
|
||||||
|
# languages, and the strict one wins:
|
||||||
|
#
|
||||||
|
# assertPrivateManagedDirectory (fleet-reconciler.js, `mode & 0o077`)
|
||||||
|
# -> MOSAIC_HOME and MOSAIC_HOME/fleet, checked before the roster lock is
|
||||||
|
# taken, so every mutating `mosaic fleet` command dies at 0755.
|
||||||
|
# assert_private_directory (tools/fleet/start-agent-session.sh, `mode & 077`)
|
||||||
|
# -> MOSAIC_HOME/fleet/agents, checked before a pane is ever spawned.
|
||||||
|
#
|
||||||
|
# Their laxer siblings (`mode & 0o022`) accept 0755, which is why normalizing
|
||||||
|
# the umask above is necessary and not sufficient — a correct umask-022 install
|
||||||
|
# still produces 0755 and still cannot run `mosaic fleet init --write`. Say the
|
||||||
|
# strict modes outright rather than inferring them from a umask.
|
||||||
|
#
|
||||||
|
# Only these. The rest of the tree is content, stays 0755, and is only ever
|
||||||
|
# reached by the 0o022 checks, which 0755 satisfies.
|
||||||
|
chmod 700 "$TARGET_DIR" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR — 'mosaic fleet' mutations will fail as unsafe-permissions."
|
||||||
|
if [[ -d "$TARGET_DIR/fleet" ]]; then
|
||||||
|
chmod 700 "$TARGET_DIR/fleet" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/fleet — 'mosaic fleet' mutations will fail as unsafe-permissions."
|
||||||
|
fi
|
||||||
|
# fleet/agents does not exist on a first install — the CLI creates it 0700 on
|
||||||
|
# demand. It is chmod'd here for the UPGRADE case: a tree built under umask 002
|
||||||
|
# has it at 0775, and the repair sweep below cannot rescue it, because stripping
|
||||||
|
# group/other write from 0755 leaves 0750 and `mode & 077` is still non-zero.
|
||||||
|
if [[ -d "$TARGET_DIR/fleet/agents" ]]; then
|
||||||
|
chmod 700 "$TARGET_DIR/fleet/agents" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/fleet/agents — agent sessions will fail to start as unsafe-permissions."
|
||||||
|
fi
|
||||||
|
# credentials/ holds secrets and was never meant to be group-readable either.
|
||||||
|
# It is not on the fleet boundary, so a failure here breaks nothing — but it is
|
||||||
|
# the one directory where a silently-failed chmod leaves secrets group-readable,
|
||||||
|
# which is precisely the failure worth a line in the output.
|
||||||
|
chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/credentials — stored secrets may be readable by other users on this host."
|
||||||
|
|
||||||
|
# Repair an existing tree. The umask above only governs directories this run
|
||||||
|
# creates, so a host installed under umask 002 before this fix keeps its 0775
|
||||||
|
# dirs through every upgrade and stays broken. Strips group/other WRITE only —
|
||||||
|
# never read or execute — so it can repair the boundary violation without
|
||||||
|
# changing who can traverse or read anything. Scoped to directories: file modes
|
||||||
|
# are the manifest's business, not this fix's.
|
||||||
|
find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w {} + 2>/dev/null || true
|
||||||
|
|
||||||
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
||||||
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
||||||
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
|
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { homedir, platform } from 'node:os';
|
import { homedir, platform } from 'node:os';
|
||||||
|
|
||||||
@@ -21,18 +22,15 @@ export function getShellProfilePath(): string | null {
|
|||||||
|
|
||||||
const shell = detectShell();
|
const shell = detectShell();
|
||||||
switch (shell) {
|
switch (shell) {
|
||||||
// Both of these deliberately avoid the interactive-only rc files.
|
|
||||||
// Debian's default .bashrc returns early for non-interactive shells, so a
|
|
||||||
// PATH line appended to it never runs for `bash -lc`, systemd units, or
|
|
||||||
// agent seats — an install could report success and still leave `mosaic`
|
|
||||||
// unreachable. .profile is read by login shells and sources .bashrc for
|
|
||||||
// interactive ones, so one line covers both; .zshenv is zsh's equivalent.
|
|
||||||
case 'zsh': {
|
case 'zsh': {
|
||||||
const zdotdir = process.env['ZDOTDIR'] ?? home;
|
const zdotdir = process.env['ZDOTDIR'] ?? home;
|
||||||
return join(zdotdir, '.zshenv');
|
return join(zdotdir, '.zshrc');
|
||||||
}
|
}
|
||||||
case 'bash':
|
case 'bash': {
|
||||||
|
const bashrc = join(home, '.bashrc');
|
||||||
|
if (existsSync(bashrc)) return bashrc;
|
||||||
return join(home, '.profile');
|
return join(home, '.profile');
|
||||||
|
}
|
||||||
case 'fish':
|
case 'fish':
|
||||||
return join(home, '.config', 'fish', 'config.fish');
|
return join(home, '.config', 'fish', 'config.fish');
|
||||||
default:
|
default:
|
||||||
|
|||||||
+15
-251
@@ -309,87 +309,6 @@ require_cmd() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# True if any shell rc file already puts $1 on PATH.
|
|
||||||
#
|
|
||||||
# Each file is tested for existence first and grepped one at a time, rather than
|
|
||||||
# handed to a single `grep -qs ... "${rc_files[@]}"`. Handing grep a missing file
|
|
||||||
# makes the exit status implementation-defined: GNU grep 3.11 returns 0 when -q
|
|
||||||
# matched an earlier file, ugrep 7.5 returns 2 for the missing one regardless.
|
|
||||||
# On the 2 path the caller reads "not present yet" and appends a duplicate PATH
|
|
||||||
# line on every single install.
|
|
||||||
path_entry_exists() {
|
|
||||||
local dir="$1" rc_file
|
|
||||||
for rc_file in "$HOME/.profile" "$HOME/.zshenv" "$HOME/.zshrc" "$HOME/.bashrc"; do
|
|
||||||
if [[ -f "$rc_file" ]] && grep -qF "$dir" "$rc_file"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Append `export PATH="$1:$PATH"` to the shell profile so $1 survives this
|
|
||||||
# process. An `export` here reaches only the installer; every directory the
|
|
||||||
# install leaves behind has to be written down somewhere a later shell reads.
|
|
||||||
#
|
|
||||||
# Deliberately NOT ~/.bashrc: Debian's default .bashrc returns early for
|
|
||||||
# non-interactive shells, so a PATH line appended to the bottom of it is
|
|
||||||
# unreachable to `bash -lc`, to systemd units, and to every agent seat — the
|
|
||||||
# exact consumers that need these binaries. ~/.profile is read by login shells
|
|
||||||
# and Debian's .profile sources .bashrc for interactive ones, so a single line
|
|
||||||
# there reaches both. For zsh the always-sourced file is .zshenv, not .zshrc.
|
|
||||||
#
|
|
||||||
# $1 = directory to add, $2 = label for the comment line.
|
|
||||||
# Returns 1 (having warned) if the profile could not be written.
|
|
||||||
persist_on_path() {
|
|
||||||
local dir="$1" label="$2" profile
|
|
||||||
|
|
||||||
if path_entry_exists "$dir"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$(basename "${SHELL:-}")" == "zsh" ]]; then
|
|
||||||
profile="$HOME/.zshenv"
|
|
||||||
else
|
|
||||||
profile="$HOME/.profile"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Probe writability in a subshell. A redirection failure on a special built-in
|
|
||||||
# aborts the shell it runs in, so it has to be a child; and the redirection on
|
|
||||||
# the subshell is what silences the "Permission denied" the shell would
|
|
||||||
# otherwise print ahead of our own message.
|
|
||||||
if ! ( : >>"$profile" ) 2>/dev/null; then
|
|
||||||
warn "$dir is not on your PATH and $profile could not be written"
|
|
||||||
dim " Add to your shell rc: export PATH=\"$dir:\$PATH\""
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "# $label"
|
|
||||||
echo "export PATH=\"$dir:\$PATH\""
|
|
||||||
} >>"$profile"
|
|
||||||
ok "Added $dir to PATH in $profile"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Persist $PREFIX/bin on PATH instead of only warning about it.
|
|
||||||
#
|
|
||||||
# The warning it replaces was the last step of an otherwise successful install,
|
|
||||||
# so the installer reported success and left `mosaic: command not found` — an
|
|
||||||
# unattended install had no operator to read the advice and act on it.
|
|
||||||
ensure_prefix_on_path() {
|
|
||||||
if [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if path_entry_exists "$PREFIX/bin"; then
|
|
||||||
warn "$PREFIX/bin is in your shell profile but not in this shell"
|
|
||||||
elif ! persist_on_path "$PREFIX/bin" "Mosaic CLI"; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
dim " Run: export PATH=\"$PREFIX/bin:\$PATH\" (or start a new login shell)"
|
|
||||||
}
|
|
||||||
|
|
||||||
installed_cli_version() {
|
installed_cli_version() {
|
||||||
local json
|
local json
|
||||||
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
||||||
@@ -597,175 +516,8 @@ install_next_cli_from_registry() {
|
|||||||
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── node bootstrap ───────────────────────────────────────────────────────────
|
|
||||||
#
|
|
||||||
# Nothing on a greenfield host installs Node.js, yet this installer and the CLI
|
|
||||||
# it installs both hard-require it. Measured on a clean Debian 13 image: the
|
|
||||||
# installer stopped at `require_cmd node` with "Required command not found" and
|
|
||||||
# nothing was installed, with no hint of how to proceed.
|
|
||||||
#
|
|
||||||
# Inlined rather than factored into a sibling file on purpose: this script is
|
|
||||||
# fetched standalone by curl and has nothing to source.
|
|
||||||
#
|
|
||||||
# No-op when a suitable node is already on PATH, so it never fights an
|
|
||||||
# operator's nvm/fnm/distro node.
|
|
||||||
|
|
||||||
NODE_ROOT="${MOSAIC_NODE_ROOT:-$HOME/.mosaic/node}"
|
|
||||||
NODE_BOOTSTRAP_VERSION="${MOSAIC_NODE_VERSION:-v22.23.2}"
|
|
||||||
NODE_MIN_MAJOR="${MOSAIC_NODE_MIN_MAJOR:-20}"
|
|
||||||
NODE_DIST_BASE="${MOSAIC_NODE_DIST_BASE:-https://nodejs.org/dist}"
|
|
||||||
|
|
||||||
# Major version of the node at $1, or empty if it will not run.
|
|
||||||
node_major_of() {
|
|
||||||
local candidate="$1" version
|
|
||||||
version="$("$candidate" -e 'process.stdout.write(process.versions.node)' 2>/dev/null)" || return 0
|
|
||||||
printf '%s' "${version%%.*}"
|
|
||||||
}
|
|
||||||
|
|
||||||
node_is_suitable() {
|
|
||||||
local major
|
|
||||||
major="$(node_major_of "$1")"
|
|
||||||
[[ -n "$major" ]] && [[ "$major" -ge "$NODE_MIN_MAJOR" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
install_node() {
|
|
||||||
local node_os node_arch tarball release_url work_dir extracted target node_bin
|
|
||||||
|
|
||||||
case "$(uname -s)" in
|
|
||||||
Linux) node_os="linux" ;;
|
|
||||||
Darwin) node_os="darwin" ;;
|
|
||||||
*) fail "Unsupported OS '$(uname -s)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Linux here means glibc. Node's official linux-x64 build is dynamically
|
|
||||||
# linked against glibc, so on musl (Alpine) the binary will not exec — but it
|
|
||||||
# fails visibly: node_is_suitable rejects it and ensure_node exits with
|
|
||||||
# "install Node.js manually". No silent breakage, just a wasted download.
|
|
||||||
# A musl host needs the unofficial build, which is out of scope here.
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) node_arch="x64" ;;
|
|
||||||
aarch64|arm64) node_arch="arm64" ;;
|
|
||||||
armv7l) node_arch="armv7l" ;;
|
|
||||||
*) fail "Unsupported architecture '$(uname -m)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# .tar.gz rather than the smaller .tar.xz: gzip is universally present, xz is
|
|
||||||
# not, and a minimal image is exactly the case this exists to handle.
|
|
||||||
tarball="node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}.tar.gz"
|
|
||||||
release_url="${NODE_DIST_BASE}/${NODE_BOOTSTRAP_VERSION}"
|
|
||||||
|
|
||||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-node-XXXXXX")"
|
|
||||||
|
|
||||||
info "Installing Node.js $NODE_BOOTSTRAP_VERSION ($node_os-$node_arch) to $NODE_ROOT…"
|
|
||||||
|
|
||||||
if ! curl -fsSL "${release_url}/${tarball}" -o "$work_dir/$tarball"; then
|
|
||||||
fail "Download failed: ${release_url}/${tarball}"
|
|
||||||
rm -rf "$work_dir"; return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Trust assumption, stated so nobody has to infer it: this verifies INTEGRITY
|
|
||||||
# (the tarball matches the manifest), not AUTHENTICITY (the manifest is
|
|
||||||
# genuinely Node's). The only thing establishing that is TLS to
|
|
||||||
# $NODE_DIST_BASE. Node publishes SHASUMS256.txt.sig signed by its release
|
|
||||||
# keys and we do not check it, which is on par with nvm but means pointing
|
|
||||||
# MOSAIC_NODE_DIST_BASE at an untrusted mirror has no signature backstop.
|
|
||||||
# Tracked as a hardening follow-up (raised by scooby in the #1229 review).
|
|
||||||
if ! curl -fsSL "${release_url}/SHASUMS256.txt" -o "$work_dir/SHASUMS256.txt"; then
|
|
||||||
fail "Could not fetch SHASUMS256.txt; refusing to install an unverified runtime."
|
|
||||||
rm -rf "$work_dir"; return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Keep only our artifact's line, so a missing entry is an error not a pass.
|
|
||||||
if ! grep " ${tarball}\$" "$work_dir/SHASUMS256.txt" >"$work_dir/expected.sha256"; then
|
|
||||||
fail "$tarball has no entry in SHASUMS256.txt; refusing to install."
|
|
||||||
rm -rf "$work_dir"; return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! (cd "$work_dir" && verify_sha256 expected.sha256); then
|
|
||||||
fail "Checksum mismatch for $tarball; refusing to install."
|
|
||||||
rm -rf "$work_dir"; return 1
|
|
||||||
fi
|
|
||||||
ok "Checksum verified"
|
|
||||||
|
|
||||||
tar xzf "$work_dir/$tarball" -C "$work_dir"
|
|
||||||
extracted="$work_dir/node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}"
|
|
||||||
if [[ ! -x "$extracted/bin/node" ]]; then
|
|
||||||
fail "Extracted archive has no bin/node"
|
|
||||||
rm -rf "$work_dir"; return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$NODE_ROOT"
|
|
||||||
target="$NODE_ROOT/$NODE_BOOTSTRAP_VERSION"
|
|
||||||
rm -rf "$target.incoming"
|
|
||||||
mv "$extracted" "$target.incoming"
|
|
||||||
rm -rf "$target"
|
|
||||||
mv "$target.incoming" "$target"
|
|
||||||
ln -sfn "$NODE_BOOTSTRAP_VERSION" "$NODE_ROOT/current"
|
|
||||||
rm -rf "$work_dir"
|
|
||||||
|
|
||||||
node_bin="$NODE_ROOT/current/bin"
|
|
||||||
if ! node_is_suitable "$node_bin/node"; then
|
|
||||||
fail "Installed node at $node_bin/node did not run"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PATH="$node_bin:$PATH"
|
|
||||||
ok "Node.js $(node -v) installed with npm $(npm -v 2>/dev/null || echo '?')"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make the Mosaic-managed Node reachable from the next shell as well as this
|
|
||||||
# one. Measured on a greenfield canary run: without this the install finished
|
|
||||||
# rc=0, wrote $PREFIX/bin to ~/.profile, and the next login shell found `mosaic`
|
|
||||||
# and then died on `env: 'node': No such file or directory` — the CLI is a Node
|
|
||||||
# script, so a CLI on PATH without its runtime is a successful install that
|
|
||||||
# produces a broken command.
|
|
||||||
persist_node_on_path() {
|
|
||||||
persist_on_path "$NODE_ROOT/current/bin" "Mosaic-managed Node.js" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_node() {
|
|
||||||
if command -v node &>/dev/null && node_is_suitable node; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A previous run may have installed one that is not on this shell's PATH.
|
|
||||||
if node_is_suitable "$NODE_ROOT/current/bin/node"; then
|
|
||||||
export PATH="$NODE_ROOT/current/bin:$PATH"
|
|
||||||
persist_node_on_path
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${MOSAIC_SKIP_NODE_BOOTSTRAP:-0}" == "1" ]]; then
|
|
||||||
fail "No suitable Node.js and MOSAIC_SKIP_NODE_BOOTSTRAP=1; refusing to download."
|
|
||||||
echo " Install Node.js >= $NODE_MIN_MAJOR yourself, then re-run this script."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd curl
|
|
||||||
require_cmd tar
|
|
||||||
|
|
||||||
# sha256sum on Linux, shasum on macOS. Verification is not optional: without a
|
|
||||||
# checksum this would install an unauthenticated runtime.
|
|
||||||
if command -v sha256sum &>/dev/null; then
|
|
||||||
verify_sha256() { sha256sum -c --status "$1"; }
|
|
||||||
elif command -v shasum &>/dev/null; then
|
|
||||||
verify_sha256() { shasum -a 256 -c --status "$1"; }
|
|
||||||
else
|
|
||||||
fail "sha256sum or shasum required to verify the Node.js download"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! install_node; then
|
|
||||||
fail "Could not bootstrap Node.js. Install Node.js >= $NODE_MIN_MAJOR and re-run."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
persist_node_on_path
|
|
||||||
}
|
|
||||||
|
|
||||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
ensure_node
|
|
||||||
require_cmd node
|
require_cmd node
|
||||||
require_cmd npm
|
require_cmd npm
|
||||||
|
|
||||||
@@ -930,7 +682,11 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
|||||||
ensure_monorepo
|
ensure_monorepo
|
||||||
install_cli_from_source
|
install_cli_from_source
|
||||||
|
|
||||||
ensure_prefix_on_path
|
# PATH check for npm prefix
|
||||||
|
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||||
|
warn "$PREFIX/bin is not on your PATH"
|
||||||
|
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||||
|
fi
|
||||||
elif is_next_registry_lane; then
|
elif is_next_registry_lane; then
|
||||||
info "Next mode — trying fast npm @next install from ${REGISTRY}…"
|
info "Next mode — trying fast npm @next install from ${REGISTRY}…"
|
||||||
if install_next_cli_from_registry; then
|
if install_next_cli_from_registry; then
|
||||||
@@ -943,7 +699,11 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
|||||||
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_prefix_on_path
|
# PATH check for npm prefix
|
||||||
|
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||||
|
warn "$PREFIX/bin is not on your PATH"
|
||||||
|
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
if [[ -z "$LATEST" ]]; then
|
if [[ -z "$LATEST" ]]; then
|
||||||
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
|
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
|
||||||
@@ -961,7 +721,11 @@ if [[ "$FLAG_CLI" == "true" ]]; then
|
|||||||
ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
|
ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_prefix_on_path
|
# PATH check for npm prefix
|
||||||
|
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||||
|
warn "$PREFIX/bin is not on your PATH"
|
||||||
|
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user