diff --git a/docs/design/prerelease-next-dist-tag-pipeline.md b/docs/design/prerelease-next-dist-tag-pipeline.md index 5cf8d8d..7e2ecb1 100644 --- a/docs/design/prerelease-next-dist-tag-pipeline.md +++ b/docs/design/prerelease-next-dist-tag-pipeline.md @@ -1,40 +1,63 @@ -# Planned Design — npm `@next` prerelease lane +# npm `@next` prerelease lane -Status: **PLANNED / not yet built** +Status: **IMPLEMENTED** -## Current state +## Current behavior -`tools/install.sh --next` provides a prerelease integration lane by building the Mosaic CLI and gateway from source at the permanent `next` branch. This is correct for validating integration-branch source, but it is slower than the stable npm lane because it downloads the archive, installs workspace dependencies, builds packages, packs local tarballs, and installs those tarballs globally. +`tools/install.sh --next` provides the prerelease integration lane for the permanent `next` branch. -## Medium-term target +The lane is fast-by-default: -Publish every accepted `next` integration build to the npm registry under the `@next` dist-tag, for example: +1. Install framework files from the `next` source archive. +2. Resolve the Gitea npm registry `next` dist-tag for the globally installed packages: + + ```bash + npm view @mosaicstack/gateway@next version + npm view @mosaicstack/mosaic@next version + ``` + +3. Require both resolved versions to share the same `next.` suffix, then install the exact resolved versions. +4. If either `@next` package is missing, unreachable, mismatched, or fails to install, fall back to the source-build path at `next`. + +`--next` never hard-fails solely because the prerelease npm dist-tag is unavailable. + +## Published packages + +The `next` publish pipeline publishes non-private `@mosaicstack/*` packages to the Mosaic Gitea npm registry: ```text -@mosaicstack/mosaic@0.0.49-next.1 -@mosaicstack/mosaic@0.0.49-next.2 +https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ ``` -Then move `tools/install.sh --next` from source-build behavior to a fast npm install: +Observed `next` dist-tags after enabling the pipeline: -```bash -npm install -g @mosaicstack/mosaic@next +```text +@mosaicstack/mosaic@next -> 0.0.49-next.1633 +@mosaicstack/gateway@next -> 0.0.7-next.1633 ``` -The framework archive should still resolve from the matching `next` source/ref until framework packaging has a registry-backed equivalent. +The gateway also publishes a Docker image as `gateway:sha-` on `next` merges. The installer fast path uses the npm gateway package when available; the Docker image is for deployed gateway/runtime harness flows. + +## Explicit source lanes + +Source builds remain available and are still the authority for explicit ref validation: + +- `--dev` always builds from source. +- `--ref ` / `MOSAIC_REF=` wins over `--next` and uses the source path for that exact ref. ## Pipeline shape -1. Trigger on successful CI for `next`. -2. Compute the next prerelease version from the upcoming stable version plus a monotonic prerelease counter (`0.0.49-next.N`). -3. Build and pack publishable packages in CI. +1. Trigger on `next` merges. +2. Compute the next prerelease version from the upcoming stable version plus the Woodpecker pipeline number (`-next.`). +3. Build and publish non-private packages in CI. 4. Publish to the Mosaic Gitea npm registry with dist-tag `next`. 5. Keep `latest` untouched; only main/release promotion can update `latest`. -6. Teach the installer to prefer `@next` for the CLI/gateway prerelease lane once the registry tag is reliable. +6. Publish gateway Docker images from `next` as `gateway:sha-` only. ## Guardrails - `@next` is mutable prerelease convenience, not a deployment pin. - Stable installs continue to use `@latest`. - Contributor validation remains available through `--dev --ref `. -- Pipeline must be reproducible and trace every prerelease package back to the source commit on `next`. +- Pipeline output traces every prerelease package back to the source commit on `next`. +- The installer falls back to source rather than hard-failing on prerelease registry issues. diff --git a/docs/guides/user-guide.md b/docs/guides/user-guide.md index 0c12580..323159c 100644 --- a/docs/guides/user-guide.md +++ b/docs/guides/user-guide.md @@ -179,13 +179,13 @@ The installer places the `mosaic` binary at `~/.npm-global/bin/mosaic`. Install lanes: -| Lane | Command | Source | -| ------------------------ | ------------------------------------- | -------------------------------------------- | -| Stable | `bash tools/install.sh` | npm `@mosaicstack/mosaic@latest` + `main` | -| Prerelease integration | `bash tools/install.sh --next` | Build-from-source at permanent branch `next` | -| Contributor/source build | `bash tools/install.sh --dev --ref X` | Build-from-source at the requested ref | +| Lane | Command | Source | +| ------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- | +| Stable | `bash tools/install.sh` | npm `@mosaicstack/mosaic@latest` + `main` | +| Prerelease integration | `bash tools/install.sh --next` | Fast npm `@mosaicstack/mosaic@next` + `@mosaicstack/gateway@next`; source fallback at `next` | +| Contributor/source build | `bash tools/install.sh --dev --ref X` | Build-from-source at the requested ref | -`--next` implies source-build mode at `next`; explicit `--ref` or `MOSAIC_REF` wins. +`--next` is fast-by-default from the Gitea npm `next` dist-tag and falls back to a source build at the permanent `next` branch if the dist-tag is missing or unreachable. Explicit `--ref` or `MOSAIC_REF` still wins and uses the source path. Flags for non-interactive use: ```bash diff --git a/docs/scratchpads/installer-next-fast-npm-20260625.md b/docs/scratchpads/installer-next-fast-npm-20260625.md new file mode 100644 index 0000000..503f1eb --- /dev/null +++ b/docs/scratchpads/installer-next-fast-npm-20260625.md @@ -0,0 +1,40 @@ +# Installer `--next` fast npm lane — 2026-06-25 + +## Scope + +Flip `tools/install.sh --next` from source-build-first to fast npm `@next` first, with source fallback. + +## Registry reality check + +Gitea npm registry: `https://git.mosaicstack.dev/api/packages/mosaicstack/npm/` + +Verified before implementation: + +- `@mosaicstack/mosaic@next` resolves to `0.0.49-next.1633`. +- `@mosaicstack/gateway@next` resolves to `0.0.7-next.1633`. +- `@mosaicstack/gateway` dist-tags include `latest: 0.0.6` and `next: 0.0.7-next.1633`. +- `apps/gateway/package.json` is non-private and has Gitea npm `publishConfig`. + +Conclusion: the installer can fast-install both CLI and gateway npm packages for `--next`. The gateway Docker `gateway:sha-` remains the deployment/harness artifact; the npm gateway package is valid for the installer global package path. + +## Behavior + +- `--next` with no explicit ref: + 1. framework archive from `next`; + 2. resolve `@mosaicstack/gateway@next` and `@mosaicstack/mosaic@next`; + 3. require both resolved versions to share the same `next.` suffix; + 4. install the exact resolved package versions; + 5. set `MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1` so wizard does not overwrite the prerelease gateway; + 6. if either package is missing/unreachable/mismatched/fails, fall back to existing source build at `next`. +- `--dev` remains pure source build. +- explicit `--ref` / `MOSAIC_REF` still wins over `--next` and uses the source path for that exact ref. + +## Install detail + +The installer writes the scoped npmrc mapping (`@mosaicstack:registry=...`) and then runs npm install without overriding npm's default registry. Passing `--registry=` to `npm install` forces public transitive dependencies (for example `@anthropic-ai/sdk`) to resolve from Gitea and breaks the fast path; the scoped npmrc mapping is the correct split-registry behavior. + +## Verification notes + +- Added `tools/install-next-lane.test.sh` with a fake npm/source harness for exact-version fast install, registry failure source fallback, explicit-ref precedence, and mismatched suffix warning. +- Wired the installer harness into `pnpm test` via `pnpm run test:installer`. +- Real temp-prefix fast install succeeded with `@mosaicstack/gateway@0.0.7-next.1633` and `@mosaicstack/mosaic@0.0.49-next.1633`. diff --git a/package.json b/package.json index fb75bde..e24725f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "turbo run dev", "lint": "turbo run lint", "typecheck": "turbo run typecheck", - "test": "turbo run test", + "test": "turbo run test && pnpm run test:installer", + "test:installer": "bash tools/install-next-lane.test.sh", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", "prepare": "husky" diff --git a/packages/mosaic/framework/defaults/README.md b/packages/mosaic/framework/defaults/README.md index 1baaaec..82f3085 100644 --- a/packages/mosaic/framework/defaults/README.md +++ b/packages/mosaic/framework/defaults/README.md @@ -45,13 +45,13 @@ The installer: ### Install lanes -| Lane | Command | Use when | Source | -| ------------------------ | ------------------------------------- | ---------------------------------------------- | ------------------------------------------ | -| Stable | `bash tools/install.sh` | You want the released framework and CLI | npm `@mosaicstack/mosaic@latest` + `main` | -| Prerelease integration | `bash tools/install.sh --next` | You want the permanent `next` integration lane | Build-from-source at `next` | -| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are validating a branch before release | Build-from-source at the requested git ref | +| Lane | Command | Use when | Source | +| ------------------------ | ------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Stable | `bash tools/install.sh` | You want the released framework and CLI | npm `@mosaicstack/mosaic@latest` + `main` | +| Prerelease integration | `bash tools/install.sh --next` | You want the permanent `next` integration lane | Fast npm `@mosaicstack/mosaic@next` + `@mosaicstack/gateway@next`; source fallback at `next` | +| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are validating a branch before release | Build-from-source at the requested git ref | -`--next` is shorthand for source-build mode at `next`; explicit `--ref` or `MOSAIC_REF` wins when both are present. +`--next` is fast-by-default from the Gitea npm `next` dist-tag and falls back to a source build at the permanent `next` branch if the dist-tag is missing or unreachable. Explicit `--ref` or `MOSAIC_REF` wins and uses the source path. ## First Run @@ -184,7 +184,7 @@ The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by bash tools/install.sh --check # Version check only bash tools/install.sh --framework # Framework only (skip npm CLI) bash tools/install.sh --cli # npm CLI only (skip framework) -bash tools/install.sh --next # Prerelease lane: source build from next +bash tools/install.sh --next # Prerelease lane: npm @next, source fallback bash tools/install.sh --dev # Contributor lane: source build at --ref/main bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref wins over --next) ``` diff --git a/tools/install-next-lane.test.sh b/tools/install-next-lane.test.sh new file mode 100755 index 0000000..4dee134 --- /dev/null +++ b/tools/install-next-lane.test.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-next-install-test-XXXXXX")" +trap 'rm -rf "$TMP"' EXIT + +FAKE_BIN="$TMP/bin" +HOME_DIR="$TMP/home" +PREFIX="$TMP/prefix" +MOSAIC_HOME="$TMP/mosaic" +STATE="$TMP/state" +LOG="$TMP/npm.log" +mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE" + +cat > "$FAKE_BIN/npm" <<'FAKE_NPM' +#!/usr/bin/env bash +set -euo pipefail +LOG="${MOSAIC_TEST_NPM_LOG:?}" +STATE="${MOSAIC_TEST_STATE:?}" +echo "$*" >> "$LOG" + +if [[ "$1" == "view" ]]; then + case "$2 $3" in + "@mosaicstack/mosaic@next version") echo "0.0.49-next.999" ;; + "@mosaicstack/gateway@next version") echo "${MOSAIC_TEST_GATEWAY_NEXT_VERSION:-0.0.7-next.999}" ;; + "@mosaicstack/mosaic version") echo "0.0.48" ;; + *) echo "unexpected npm view: $*" >&2; exit 1 ;; + esac + exit 0 +fi + +if [[ "$1" == "install" ]]; then + case "$*" in + *"@mosaicstack/mosaic@0.0.49-next.999"*) + echo "0.0.49-next.999" > "$STATE/mosaic" + ;; + *"@mosaicstack/gateway@0.0.7-next.999"*) + if [[ "${MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL:-0}" == "1" ]]; then + echo "forced gateway install failure" >&2 + exit 1 + fi + echo "0.0.7-next.999" > "$STATE/gateway" + ;; + *"mosaicstack-mosaic-0.0.0-source.tgz"*) + echo "0.0.0-source" > "$STATE/mosaic" + ;; + *"mosaicstack-gateway-0.0.0-source.tgz"*) + echo "0.0.0-source" > "$STATE/gateway" + ;; + *) echo "unexpected npm install: $*" >&2; exit 1 ;; + esac + exit 0 +fi + +if [[ "$1" == "ls" ]]; then + cli="$(cat "$STATE/mosaic" 2>/dev/null || true)" + gateway="$(cat "$STATE/gateway" 2>/dev/null || true)" + node -e ' + const cli = process.argv[1]; + const gateway = process.argv[2]; + const dependencies = {}; + if (cli) dependencies["@mosaicstack/mosaic"] = { version: cli }; + if (gateway) dependencies["@mosaicstack/gateway"] = { version: gateway }; + process.stdout.write(JSON.stringify({ dependencies })); + ' "$cli" "$gateway" + exit 0 +fi + +echo "unexpected npm command: $*" >&2 +exit 1 +FAKE_NPM +chmod +x "$FAKE_BIN/npm" + +cat > "$FAKE_BIN/curl" <<'FAKE_CURL' +#!/usr/bin/env bash +set -euo pipefail +# The fake tar creates the source tree; curl only needs to keep the pipe alive. +exit 0 +FAKE_CURL +chmod +x "$FAKE_BIN/curl" + +cat > "$FAKE_BIN/tar" <<'FAKE_TAR' +#!/usr/bin/env bash +set -euo pipefail +dest="" +while [[ $# -gt 0 ]]; do + case "$1" in + -C) dest="$2"; shift 2 ;; + *) shift ;; + esac +done +if [[ -z "$dest" ]]; then + echo "fake tar missing -C destination" >&2 + exit 1 +fi +mkdir -p "$dest/stack/packages/mosaic" "$dest/stack/apps/gateway" +FAKE_TAR +chmod +x "$FAKE_BIN/tar" + +cat > "$FAKE_BIN/pnpm" <<'FAKE_PNPM' +#!/usr/bin/env bash +set -euo pipefail +LOG="${MOSAIC_TEST_NPM_LOG:?}" +echo "pnpm $*" >> "$LOG" + +if [[ "$1" == "pack" ]]; then + out="" + while [[ $# -gt 0 ]]; do + case "$1" in + --pack-destination) out="$2"; shift 2 ;; + *) shift ;; + esac + done + if [[ -z "$out" ]]; then + echo "fake pnpm pack missing destination" >&2 + exit 1 + fi + mkdir -p "$out" + case "$PWD" in + */apps/gateway) touch "$out/mosaicstack-gateway-0.0.0-source.tgz" ;; + */packages/mosaic) touch "$out/mosaicstack-mosaic-0.0.0-source.tgz" ;; + *) echo "unexpected pnpm pack cwd: $PWD" >&2; exit 1 ;; + esac + exit 0 +fi + +# install/build commands are no-ops in this harness. +exit 0 +FAKE_PNPM +chmod +x "$FAKE_BIN/pnpm" + +reset_state() { + : > "$LOG" + rm -f "$STATE"/* +} + +reset_state +echo "[test] --next fast path pins resolved package versions" +OUTPUT="$( + HOME="$HOME_DIR" \ + MOSAIC_HOME="$MOSAIC_HOME" \ + MOSAIC_PREFIX="$PREFIX" \ + MOSAIC_NO_COLOR=1 \ + MOSAIC_TEST_NPM_LOG="$LOG" \ + MOSAIC_TEST_STATE="$STATE" \ + PATH="$FAKE_BIN:$PATH" \ + bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch +)" + +grep -qF 'Installed @next packages: CLI 0.0.49-next.999, gateway 0.0.7-next.999' <<<"$OUTPUT" +grep -qF 'install -g @mosaicstack/gateway@0.0.7-next.999' "$LOG" +grep -qF 'install -g @mosaicstack/mosaic@0.0.49-next.999' "$LOG" +if grep -qE '^install -g .+@next( |$)' "$LOG"; then + echo "expected exact-version installs, found mutable @next install" >&2 + exit 1 +fi +if grep -qF 'Downloading source from next' <<<"$OUTPUT"; then + echo "fast path unexpectedly fell back to source" >&2 + exit 1 +fi + +reset_state +echo "[test] fast path failure falls back to source build" +OUTPUT="$( + HOME="$HOME_DIR" \ + MOSAIC_HOME="$MOSAIC_HOME" \ + MOSAIC_PREFIX="$PREFIX" \ + MOSAIC_NO_COLOR=1 \ + MOSAIC_TEST_NPM_LOG="$LOG" \ + MOSAIC_TEST_STATE="$STATE" \ + MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \ + PATH="$FAKE_BIN:$PATH" \ + bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch +)" + +grep -qF 'Fast gateway @next install failed.' <<<"$OUTPUT" +grep -qF 'Falling back to source build at ref next; --next will not hard-fail on registry issues.' <<<"$OUTPUT" +grep -qF 'Downloading source from next' <<<"$OUTPUT" +grep -qF 'Installed from source: CLI 0.0.0-source' <<<"$OUTPUT" +grep -qF 'install -g @mosaicstack/mosaic@0.0.49-next.999' "$LOG" +grep -qE 'install -g .*/mosaicstack-gateway-0\.0\.0-source\.tgz' "$LOG" +grep -qE 'install -g .*/mosaicstack-mosaic-0\.0\.0-source\.tgz' "$LOG" +[[ "$(cat "$STATE/mosaic")" == "0.0.0-source" ]] +[[ "$(cat "$STATE/gateway")" == "0.0.0-source" ]] + +reset_state +echo "[test] explicit --ref keeps source lane and avoids @next lookup" +OUTPUT="$( + HOME="$HOME_DIR" \ + MOSAIC_HOME="$MOSAIC_HOME" \ + MOSAIC_PREFIX="$PREFIX" \ + MOSAIC_NO_COLOR=1 \ + MOSAIC_TEST_NPM_LOG="$LOG" \ + MOSAIC_TEST_STATE="$STATE" \ + PATH="$FAKE_BIN:$PATH" \ + bash "$ROOT/tools/install.sh" --check --cli --next --ref feature-x +)" + +grep -qF 'explicit ref wins, build-from-source' <<<"$OUTPUT" +if grep -qF '@next version' "$LOG"; then + echo "explicit ref should not query @next dist-tags" >&2 + exit 1 +fi + +reset_state +echo "[test] --check --next warns on mismatched prerelease pipeline suffixes" +OUTPUT="$( + HOME="$HOME_DIR" \ + MOSAIC_HOME="$MOSAIC_HOME" \ + MOSAIC_PREFIX="$PREFIX" \ + MOSAIC_NO_COLOR=1 \ + MOSAIC_TEST_NPM_LOG="$LOG" \ + MOSAIC_TEST_STATE="$STATE" \ + MOSAIC_TEST_GATEWAY_NEXT_VERSION="0.0.7-next.1000" \ + PATH="$FAKE_BIN:$PATH" \ + bash "$ROOT/tools/install.sh" --check --cli --next +)" + +grep -qF '@next registry lane incomplete, mismatched, or unreachable; --next would fall back to source.' <<<"$OUTPUT" + +echo "[test] installer next lane tests passed" diff --git a/tools/install.sh b/tools/install.sh index 79dfc4d..e8303af 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -16,9 +16,10 @@ # --framework Install/upgrade framework only (skip npm CLI) # --cli Install/upgrade npm CLI only (skip framework) # --ref Git ref for framework archive (default: main) -# --next Prerelease lane: build CLI + gateway FROM SOURCE at the -# permanent next integration branch. Shorthand for --dev -# with ref=next; explicit --ref/MOSAIC_REF wins. +# --next Prerelease lane: try fast npm @next install for CLI + +# gateway from the Gitea registry, then fall back to a +# source build at next if unavailable. Explicit +# --ref/MOSAIC_REF wins and uses the source path. # --dev Build CLI + gateway FROM SOURCE at --ref instead of the # registry @latest. Zero registry writes — packs local # tarballs and installs them globally. Use to test a branch @@ -70,10 +71,10 @@ if [[ "${MOSAIC_DEV:-0}" == "1" ]]; then FLAG_DEV=true fi -# MOSAIC_NEXT env var acts the same as --next: source build from the -# permanent next integration branch unless MOSAIC_REF/--ref explicitly wins. +# MOSAIC_NEXT env var acts the same as --next: fast npm @next install with +# source fallback from the permanent next integration branch unless +# MOSAIC_REF/--ref explicitly wins. if [[ "${MOSAIC_NEXT:-0}" == "1" ]]; then - FLAG_DEV=true FLAG_NEXT=true if [[ "$GIT_REF_EXPLICIT" == "false" ]]; then GIT_REF="next" @@ -87,7 +88,7 @@ while [[ $# -gt 0 ]]; do --cli) FLAG_FRAMEWORK=false; shift ;; --ref) GIT_REF="${2:-main}"; GIT_REF_EXPLICIT=true; shift 2 ;; --dev) FLAG_DEV=true; shift ;; - --next) FLAG_DEV=true; FLAG_NEXT=true; if [[ "$GIT_REF_EXPLICIT" == "false" ]]; then GIT_REF="next"; fi; shift ;; + --next) FLAG_NEXT=true; if [[ "$GIT_REF_EXPLICIT" == "false" ]]; then GIT_REF="next"; fi; shift ;; --yes|-y) FLAG_YES=true; shift ;; --no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;; --uninstall) FLAG_UNINSTALL=true; shift ;; @@ -95,6 +96,13 @@ while [[ $# -gt 0 ]]; do esac done +# Explicit refs represent a request for that exact source tree. Keep --next as +# a lane selector, but do not install the registry @next package for a different +# ref than the permanent next branch. +if [[ "$FLAG_NEXT" == "true" && "$GIT_REF_EXPLICIT" == "true" ]]; then + FLAG_DEV=true +fi + if [[ "$FLAG_YES" == "true" ]]; then export MOSAIC_ASSUME_YES=1 fi @@ -105,6 +113,7 @@ REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstac SCOPE="${MOSAIC_SCOPE:-@mosaicstack}" PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}" CLI_PKG="${SCOPE}/mosaic" +GATEWAY_PKG="${SCOPE}/gateway" REPO_BASE="https://git.mosaicstack.dev/mosaicstack/stack" ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz" @@ -252,9 +261,15 @@ fail() { echo "${R}✖${RESET} $*" >&2; } dim() { echo "${DIM}$*${RESET}"; } step() { echo ""; echo "${BOLD}$*${RESET}"; } +is_next_registry_lane() { + [[ "$FLAG_NEXT" == "true" && "$FLAG_DEV" == "false" && "$GIT_REF" == "next" && "$GIT_REF_EXPLICIT" == "false" ]] +} + source_ref_details() { - if [[ "$FLAG_NEXT" == "true" && "$GIT_REF" == "next" ]]; then + if is_next_registry_lane; then echo "ref: next, --next prerelease lane" + elif [[ "$FLAG_NEXT" == "true" && "$GIT_REF" == "next" ]]; then + echo "ref: next, --next prerelease lane (build-from-source)" elif [[ "$FLAG_NEXT" == "true" ]]; then echo "ref: ${GIT_REF}, --next requested, explicit ref wins" else @@ -284,10 +299,43 @@ installed_cli_version() { fi } +installed_gateway_version() { + local json + json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true + if [[ -n "$json" ]]; then + node -e " + const d = JSON.parse(process.argv[1]); + const v = d?.dependencies?.['${GATEWAY_PKG}']?.version ?? ''; + process.stdout.write(v); + " "$json" 2>/dev/null || true + fi +} + latest_cli_version() { npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true } +next_cli_version() { + npm view "${CLI_PKG}@next" version --registry="$REGISTRY" 2>/dev/null || true +} + +next_gateway_version() { + npm view "${GATEWAY_PKG}@next" version --registry="$REGISTRY" 2>/dev/null || true +} + +next_pipeline_suffix() { + printf '%s' "$1" | sed -n 's/.*-next\.\([0-9][0-9]*\)$/\1/p' +} + +next_versions_share_pipeline() { + local cli_next="$1" + local gateway_next="$2" + local cli_pipeline gateway_pipeline + cli_pipeline="$(next_pipeline_suffix "$cli_next")" + gateway_pipeline="$(next_pipeline_suffix "$gateway_next")" + [[ -n "$cli_pipeline" && -n "$gateway_pipeline" && "$cli_pipeline" == "$gateway_pipeline" ]] +} + version_lt() { node -e " const a=process.argv[1], b=process.argv[2]; @@ -403,6 +451,49 @@ install_cli_from_source() { ok "Installed from source: CLI $(installed_cli_version)" } +install_next_cli_from_registry() { + local cli_next gateway_next + cli_next="$(next_cli_version)" + gateway_next="$(next_gateway_version)" + + if [[ -z "$cli_next" ]]; then + warn "${CLI_PKG}@next is unavailable from $REGISTRY." + return 1 + fi + if [[ -z "$gateway_next" ]]; then + warn "${GATEWAY_PKG}@next is unavailable from $REGISTRY." + return 1 + fi + + if ! next_versions_share_pipeline "$cli_next" "$gateway_next"; then + warn "@next CLI/gateway versions do not share a pipeline suffix (${cli_next}, ${gateway_next})." + return 1 + fi + + info "Installing ${CLI_PKG}@${cli_next} from registry…" + if ! npm install -g "${CLI_PKG}@${cli_next}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'; then + warn "Fast CLI @next install failed." + return 1 + fi + + info "Installing ${GATEWAY_PKG}@${gateway_next} from registry…" + if ! npm install -g "${GATEWAY_PKG}@${gateway_next}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'; then + warn "Fast gateway @next install failed." + return 1 + fi + + local installed_cli installed_gateway + installed_cli="$(installed_cli_version)" + installed_gateway="$(installed_gateway_version)" + if [[ "$installed_cli" != "$cli_next" || "$installed_gateway" != "$gateway_next" ]]; then + warn "Installed @next versions did not match resolved versions (CLI: ${installed_cli:-missing}, gateway: ${installed_gateway:-missing})." + return 1 + fi + + export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1 + ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}" +} + # ─── preflight ──────────────────────────────────────────────────────────────── require_cmd node @@ -503,8 +594,12 @@ if [[ "$FLAG_CLI" == "true" ]]; then fi CURRENT="$(installed_cli_version)" + NEXT_GATEWAY="" if [[ "$FLAG_DEV" == "true" ]]; then LATEST="" + elif is_next_registry_lane; then + LATEST="$(next_cli_version)" + NEXT_GATEWAY="$(next_gateway_version)" else LATEST="$(latest_cli_version)" fi @@ -517,6 +612,18 @@ if [[ "$FLAG_CLI" == "true" ]]; then if [[ "$FLAG_DEV" == "true" ]]; then dim " Source: ${REPO_BASE} ($(source_ref_details), build-from-source)" + elif is_next_registry_lane; then + if [[ -n "$LATEST" ]]; then + dim " Next CLI: ${CLI_PKG}@${LATEST}" + else + dim " Next CLI: (registry @next unreachable)" + fi + if [[ -n "$NEXT_GATEWAY" ]]; then + dim " Next GW: ${GATEWAY_PKG}@${NEXT_GATEWAY}" + else + dim " Next GW: (registry @next unreachable)" + fi + dim " Fallback: ${REPO_BASE} (ref: next, build-from-source)" elif [[ -n "$LATEST" ]]; then dim " Latest: ${CLI_PKG}@${LATEST}" else @@ -527,6 +634,12 @@ if [[ "$FLAG_CLI" == "true" ]]; then if [[ "$FLAG_CHECK" == "true" ]]; then if [[ "$FLAG_DEV" == "true" ]]; then info "Dev mode: installed version is ${CURRENT:-(none)} (no registry comparison)." + elif is_next_registry_lane; then + if [[ -n "$LATEST" && -n "$NEXT_GATEWAY" ]] && next_versions_share_pipeline "$LATEST" "$NEXT_GATEWAY"; then + ok "@next registry lane available: ${CLI_PKG}@${LATEST}, ${GATEWAY_PKG}@${NEXT_GATEWAY}." + else + warn "@next registry lane incomplete, mismatched, or unreachable; --next would fall back to source." + fi elif [[ -z "$LATEST" ]]; then warn "Could not reach registry." elif [[ -z "$CURRENT" ]]; then @@ -543,6 +656,23 @@ if [[ "$FLAG_CLI" == "true" ]]; then ensure_monorepo install_cli_from_source + # 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 + info "Next mode — trying fast npm @next install from ${REGISTRY}…" + if install_next_cli_from_registry; then + : + else + warn "Falling back to source build at ref ${GIT_REF}; --next will not hard-fail on registry issues." + unset MOSAIC_GATEWAY_SKIP_NPM_INSTALL + ensure_monorepo + install_cli_from_source + export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1 + fi + # PATH check for npm prefix if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then warn "$PREFIX/bin is not on your PATH"