diff --git a/AGENTS.md b/AGENTS.md
index 5c5323ca0..bb1424185 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -142,6 +142,8 @@ claude --plugin-dir ./apps/hook
| `PLANNOTATOR_GLIMPSE_HEIGHT` | Height in pixels for the Glimpse native window. Default: `900`. |
| `PLANNOTATOR_VERIFY_ATTESTATION` | **Read by the install scripts only**, not by the runtime binary. Set to `1` / `true` to have `scripts/install.sh` / `install.ps1` / `install.cmd` run `gh attestation verify` on every install. Off by default. Can also be set persistently via `~/.plannotator/config.json` (`{ "verifyAttestation": true }`) or per-invocation via `--verify-attestation`. Requires `gh` installed and authenticated. |
| `PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL` | Set to `1` / `true` to skip installing the managed Node/WebTUI runtime used by compiled Bun builds for the annotate-mode agent terminal. Read by `plannotator install-runtime agent-terminal`, which the installers call automatically. |
+| `PLANNOTATOR_MINIMAL` | **Read by the install scripts only**, not by the runtime binary. Set to `1` / `true` / `yes` to have `scripts/install.sh` / `install.ps1` / `install.cmd` install **only** the `plannotator` binary — skipping the sem sidecar, the agent-terminal runtime, and all per-agent skills, hooks, slash commands, and config. Equivalent to the `--minimal` (aliased `--binary-only`) flag; `--no-minimal` overrides it. Off by default. |
+| `PLANNOTATOR_SKIP_SEM_INSTALL` | **Read by the install scripts only.** Set to `1` / `true` to skip installing the optional `sem` semantic-diff sidecar (used by code review). Off by default. |
**Config-only settings (`~/.plannotator/config.json`)**: Some settings have no env-var equivalent and are toggled by editing the config file directly:
diff --git a/README.md b/README.md
index 6359b919f..54125b512 100644
--- a/README.md
+++ b/README.md
@@ -169,6 +169,12 @@ curl -fsSL https://plannotator.ai/install.sh | bash
irm https://plannotator.ai/install.ps1 | iex
```
+Want just the binary and nothing else? Pass `--minimal` (or export `PLANNOTATOR_MINIMAL=1`) to install only the `plannotator` binary to `~/.local/bin`, skipping every skill, hook, slash command, and per-agent config:
+
+```bash
+curl -fsSL https://plannotator.ai/install.sh | bash -s -- --minimal
+```
+
Then finish the step for your agent:
| Agent | After the installer | Details |
diff --git a/apps/marketing/src/content/docs/getting-started/installation.md b/apps/marketing/src/content/docs/getting-started/installation.md
index beae65652..1e88d3169 100644
--- a/apps/marketing/src/content/docs/getting-started/installation.md
+++ b/apps/marketing/src/content/docs/getting-started/installation.md
@@ -62,6 +62,27 @@ Version pinning is fully supported from **v0.17.2 onwards**. v0.17.2 is the firs
+
+Binary-only install (nothing but the CLI)
+
+Pass `--minimal` (aliased `--binary-only`) to install **only** the `plannotator` binary — no sem semantic-diff sidecar, no agent-terminal runtime, and none of the per-agent skills, hooks, slash commands, or config for Claude, Codex, OpenCode, Gemini, or Kiro. The only thing installed is the binary (the Windows PowerShell installer also adds the install directory to your user `PATH`), and because it skips the sparse checkout, **minimal mode does not require `git`**.
+
+```bash
+curl -fsSL https://plannotator.ai/install.sh | bash -s -- --minimal
+```
+
+```powershell
+& ([scriptblock]::Create((irm https://plannotator.ai/install.ps1))) -Minimal
+```
+
+```cmd
+curl -fsSL https://plannotator.ai/install.cmd -o install.cmd && install.cmd --minimal && del install.cmd
+```
+
+For `curl … | bash` pipelines you can set `PLANNOTATOR_MINIMAL=1` in the environment instead of passing the flag; pass `--no-minimal` to force a full install even when that variable is set.
+
+
+
Every release includes SHA256 checksums (verified automatically) and optional [SLSA build provenance](/docs/reference/verifying-your-install/) attestations.
## Claude Code
diff --git a/apps/marketing/src/content/docs/reference/environment-variables.md b/apps/marketing/src/content/docs/reference/environment-variables.md
index 37d7bdfa3..871b92003 100644
--- a/apps/marketing/src/content/docs/reference/environment-variables.md
+++ b/apps/marketing/src/content/docs/reference/environment-variables.md
@@ -64,6 +64,8 @@ When running your own paste service binary, these variables configure it:
| Variable | Default | Description |
|----------|---------|-------------|
| `PLANNOTATOR_VERIFY_ATTESTATION` | off | Set to `1` or `true` to have the install script run `gh attestation verify` on the downloaded binary. Requires `gh` CLI installed and authenticated. Can also be set via `~/.plannotator/config.json` (`{ "verifyAttestation": true }`) or per-invocation via `--verify-attestation`. |
+| `PLANNOTATOR_MINIMAL` | off | Set to `1` / `true` / `yes` to install **only** the `plannotator` binary — no sem sidecar, agent-terminal runtime, skills, hooks, slash commands, or per-agent config. Equivalent to passing `--minimal` (aliased `--binary-only`); pass `--no-minimal` to override. Read by the install scripts only, not the runtime binary. |
+| `PLANNOTATOR_SKIP_SEM_INSTALL` | off | Set to `1` / `true` to skip installing the optional `sem` semantic-diff sidecar used by code review. Read by the install scripts only. |
| `CLAUDE_CONFIG_DIR` | `~/.claude` | Custom Claude Code config directory. The install script places hooks here instead of the default location. |
## Remote mode behavior
diff --git a/scripts/install.cmd b/scripts/install.cmd
index 855d67ca3..a53041fca 100644
--- a/scripts/install.cmd
+++ b/scripts/install.cmd
@@ -18,6 +18,10 @@ set "EXTRAS_FLAG="
set "MODEL_INVOCABLE_FLAG="
set "NON_INTERACTIVE=0"
set "RECONFIGURE=0"
+REM Binary-only mode. Installs just plannotator.exe and no persistent state
+REM elsewhere. Set by --minimal (1) / --no-minimal (0); -1 = neither flag given
+REM (fall through to the PLANNOTATOR_MINIMAL env var, resolved after :args_done).
+set "MINIMAL_FLAG=-1"
:parse_args
if "%~1"=="" goto args_done
@@ -92,6 +96,33 @@ if /i "%~1"=="--reconfigure" (
shift
goto parse_args
)
+if /i "%~1"=="--minimal" (
+ if "!MINIMAL_FLAG!"=="0" (
+ echo --minimal and --no-minimal are mutually exclusive >&2
+ exit /b 1
+ )
+ set "MINIMAL_FLAG=1"
+ shift
+ goto parse_args
+)
+if /i "%~1"=="--binary-only" (
+ if "!MINIMAL_FLAG!"=="0" (
+ echo --binary-only and --no-minimal are mutually exclusive >&2
+ exit /b 1
+ )
+ set "MINIMAL_FLAG=1"
+ shift
+ goto parse_args
+)
+if /i "%~1"=="--no-minimal" (
+ if "!MINIMAL_FLAG!"=="1" (
+ echo --no-minimal and --minimal are mutually exclusive >&2
+ exit /b 1
+ )
+ set "MINIMAL_FLAG=0"
+ shift
+ goto parse_args
+)
REM Reject any other dash-prefixed token as an unknown option, so a typoed
REM flag like --verify-attesttion fails fast instead of being interpreted as
REM a version tag (which would 404 on releases/download/v--verify-attesttion/...).
@@ -106,7 +137,7 @@ REM unquoted arg containing `&` would re-trigger metacharacter interpretation.
set "CURRENT_ARG=%~1"
if "!CURRENT_ARG:~0,1!"=="-" (
echo Unknown option: "%~1" >&2
- echo Usage: install.cmd [--version ^] [--verify-attestation ^| --skip-attestation] [--extras ^| --no-extras] [--model-invocable ^] [--non-interactive] [--reconfigure] >&2
+ echo Usage: install.cmd [--version ^] [--verify-attestation ^| --skip-attestation] [--extras ^| --no-extras] [--model-invocable ^] [--minimal ^| --no-minimal] [--non-interactive] [--reconfigure] >&2
exit /b 1
)
REM Positional form: install.cmd vX.Y.Z (legacy interface).
@@ -122,6 +153,15 @@ shift
goto parse_args
:args_done
+REM Resolve binary-only mode. Precedence: --minimal / --no-minimal flag >
+REM PLANNOTATOR_MINIMAL env var > default (off). Mirrors install.sh / install.ps1.
+set "MINIMAL=0"
+if /i "%PLANNOTATOR_MINIMAL%"=="1" set "MINIMAL=1"
+if /i "%PLANNOTATOR_MINIMAL%"=="true" set "MINIMAL=1"
+if /i "%PLANNOTATOR_MINIMAL%"=="yes" set "MINIMAL=1"
+if "!MINIMAL_FLAG!"=="1" set "MINIMAL=1"
+if "!MINIMAL_FLAG!"=="0" set "MINIMAL=0"
+
set "REPO=backnotprop/plannotator"
set "SEM_REPO=Ataraxy-Labs/sem"
set "SEM_VERSION=v0.8.0"
@@ -388,23 +428,22 @@ move /y "!TEMP_FILE!" "!INSTALL_PATH!" >nul
echo.
echo plannotator !TAG! installed to !INSTALL_PATH!
+REM Binary-only mode stops here (see the MINIMAL resolution after :args_done):
+REM the binary is installed, so print PATH advice and exit before any sidecar
+REM download, agent integration, skill checkout, or config write runs. No
+REM persistent state is written outside !INSTALL_DIR!.
+if "!MINIMAL!"=="1" (
+ call :PrintPathAdvice
+ echo.
+ echo Minimal install complete - only the plannotator binary was installed.
+ echo No skills, hooks, agent integrations, or config files were written.
+ exit /b 0
+)
+
call :InstallSemSidecar
call :InstallAgentTerminalRuntime
-REM Check if install directory is in PATH
-echo !PATH! | findstr /i /c:"!INSTALL_DIR!" >nul
-if !ERRORLEVEL! neq 0 (
- echo.
- echo !INSTALL_DIR! is not in your PATH.
- echo.
- echo Add it permanently with:
- echo.
- echo setx PATH "%%PATH%%;!INSTALL_DIR!"
- echo.
- echo Or add it for this session only:
- echo.
- echo set PATH=%%PATH%%;!INSTALL_DIR!
-)
+call :PrintPathAdvice
REM Validate plugin hooks.json if plugin is already installed
if defined CLAUDE_CONFIG_DIR (
@@ -959,6 +998,27 @@ if exist "!PLUGIN_HOOKS!" if exist "!CLAUDE_SETTINGS!" (
echo.
exit /b 0
+REM ======================================================================
+REM Print the PATH-setup hint if INSTALL_DIR isn't already on PATH. Called by
+REM both the --minimal early exit and the normal flow (mirrors install.sh's
+REM print_path_advice).
+REM ======================================================================
+:PrintPathAdvice
+echo !PATH! | findstr /i /c:"!INSTALL_DIR!" >nul
+if !ERRORLEVEL! neq 0 (
+ echo.
+ echo !INSTALL_DIR! is not in your PATH.
+ echo.
+ echo Add it permanently with:
+ echo.
+ echo setx PATH "%%PATH%%;!INSTALL_DIR!"
+ echo.
+ echo Or add it for this session only:
+ echo.
+ echo set PATH=%%PATH%%;!INSTALL_DIR!
+)
+goto :eof
+
REM ======================================================================
REM Optional annotate agent terminal runtime install. Non-fatal: Plannotator
REM remains installed if Node/npm or npm install is unavailable.
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index 7a32df189..f1e37b528 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -7,7 +7,10 @@ param(
[switch]$NoExtras,
[string]$ModelInvocable = "",
[switch]$NonInteractive,
- [switch]$Reconfigure
+ [switch]$Reconfigure,
+ [Alias("BinaryOnly")]
+ [switch]$Minimal,
+ [switch]$NoMinimal
)
$ErrorActionPreference = "Stop"
@@ -23,6 +26,22 @@ if ($Extras -and $NoExtras) {
[Console]::Error.WriteLine("-Extras and -NoExtras are mutually exclusive. Pass one or the other.")
exit 1
}
+if ($Minimal -and $NoMinimal) {
+ [Console]::Error.WriteLine("-Minimal and -NoMinimal are mutually exclusive. Pass one or the other.")
+ exit 1
+}
+
+# Binary-only mode. Installs just the plannotator binary and no persistent state
+# elsewhere — no sem sidecar, agent-terminal runtime, skills, hooks, or per-agent
+# config. Precedence: -Minimal / -NoMinimal switch > PLANNOTATOR_MINIMAL env var
+# > default (off). Mirrors install.sh's --minimal / --no-minimal.
+$minimal = $false
+if ($env:PLANNOTATOR_MINIMAL -match '^(1|true|yes)$') {
+ $minimal = $true
+}
+if ($Minimal) { $minimal = $true }
+if ($NoMinimal) { $minimal = $false }
+
$repo = "backnotprop/plannotator"
$semRepo = "Ataraxy-Labs/sem"
$semVersion = "v0.8.0"
@@ -330,18 +349,37 @@ Move-Item -Force $tmpFile "$installDir\plannotator.exe"
Write-Host ""
Write-Host "plannotator $latestTag installed to $installDir\plannotator.exe"
-Install-SemSidecar
-Install-AgentTerminalRuntime
+# Add $installDir to the user PATH if not already there. Extracted so both the
+# -Minimal early exit and the normal flow reuse it (mirrors install.sh's
+# print_path_advice).
+function Show-PathAdvice {
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
+ if ($userPath -notlike "*$installDir*") {
+ Write-Host ""
+ Write-Host "$installDir is not in your PATH. Adding it..."
+ [Environment]::SetEnvironmentVariable("Path", "$userPath;$installDir", "User")
+ Write-Host "Added to PATH. Restart your terminal for changes to take effect."
+ }
+}
-# Add to PATH if not already there
-$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
-if ($userPath -notlike "*$installDir*") {
+# Binary-only mode stops here (see the $minimal resolution near the top): the
+# binary is installed, so add it to PATH and exit before any sidecar download,
+# agent integration, skill checkout, config write, or cleanup runs. Only the
+# binary and its PATH entry are added — none of the sem sidecar, agent-terminal
+# runtime, or per-agent skills, hooks, or config.
+if ($minimal) {
+ Show-PathAdvice
Write-Host ""
- Write-Host "$installDir is not in your PATH. Adding it..."
- [Environment]::SetEnvironmentVariable("Path", "$userPath;$installDir", "User")
- Write-Host "Added to PATH. Restart your terminal for changes to take effect."
+ Write-Host "Minimal install complete - only the plannotator binary was installed."
+ Write-Host "No skills, hooks, agent integrations, or config files were written."
+ exit 0
}
+Install-SemSidecar
+Install-AgentTerminalRuntime
+
+Show-PathAdvice
+
# Validate plugin hooks.json if plugin is already installed
$pluginHooks = if ($env:CLAUDE_CONFIG_DIR) { "$env:CLAUDE_CONFIG_DIR\plugins\marketplaces\plannotator\apps\hook\hooks\hooks.json" } else { "$env:USERPROFILE\.claude\plugins\marketplaces\plannotator\apps\hook\hooks\hooks.json" }
if (Test-Path $pluginHooks) {
diff --git a/scripts/install.sh b/scripts/install.sh
index f3aaa97a9..05ec1d483 100644
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -40,12 +40,20 @@ EXTRAS_FLAG=""
MODEL_INVOCABLE_FLAG=""
NON_INTERACTIVE=0
RECONFIGURE=0
+# Binary-only mode. Installs just the plannotator binary (to $INSTALL_DIR) and
+# no persistent state elsewhere — no sem sidecar, no agent-terminal runtime, no
+# skills, hooks, slash commands, or per-agent config (Claude, Codex, OpenCode,
+# Gemini, Kiro). Set by --minimal (1) / --no-minimal (0); -1 = neither flag
+# given (fall through to the PLANNOTATOR_MINIMAL env var). Resolved after arg
+# parsing so a flag overrides the env var in either direction.
+MINIMAL_FLAG=-1
usage() {
cat <<'USAGE'
Usage: install.sh [--version ] [--verify-attestation | --skip-attestation]
[--extras | --no-extras] [--model-invocable |none]
- [--non-interactive] [--reconfigure] [--help]
+ [--minimal | --no-minimal] [--non-interactive]
+ [--reconfigure] [--help]
install.sh
Options:
@@ -63,6 +71,16 @@ Options:
--model-invocable Comma-separated skill names to make model-invocable
(e.g. plannotator-review,plannotator-compound), or
"none". Skills are user-invoked-only by default.
+ --minimal Install only the plannotator binary (aliased
+ --binary-only). Skips the sem semantic-diff sidecar,
+ the agent-terminal runtime, and every per-agent
+ integration (skills, hooks, slash commands, and config
+ for Claude, Codex, OpenCode, Gemini, and Kiro). No
+ persistent state is written outside $HOME/.local/bin
+ (a temp download file is still used and removed). Also
+ enabled by exporting PLANNOTATOR_MINIMAL=1.
+ --no-minimal Force a full install even when PLANNOTATOR_MINIMAL is
+ set in the environment.
--non-interactive Never prompt, even in a terminal. Uses flags, then
saved answers from a previous run, then the defaults
(no extras, nothing model-invocable).
@@ -189,6 +207,24 @@ while [ $# -gt 0 ]; do
RECONFIGURE=1
shift
;;
+ --minimal|--binary-only)
+ if [ "$MINIMAL_FLAG" = "0" ]; then
+ echo "--minimal and --no-minimal are mutually exclusive" >&2
+ usage >&2
+ exit 1
+ fi
+ MINIMAL_FLAG=1
+ shift
+ ;;
+ --no-minimal)
+ if [ "$MINIMAL_FLAG" = "1" ]; then
+ echo "--no-minimal and --minimal are mutually exclusive" >&2
+ usage >&2
+ exit 1
+ fi
+ MINIMAL_FLAG=0
+ shift
+ ;;
-h|--help)
usage
exit 0
@@ -214,6 +250,18 @@ while [ $# -gt 0 ]; do
esac
done
+# Resolve binary-only mode. Precedence: --minimal / --no-minimal flag >
+# PLANNOTATOR_MINIMAL env var > default (off). The env var lets `curl ... | bash`
+# runs opt in without a flag, matching how PLANNOTATOR_SKIP_SEM_INSTALL et al.
+# work; --no-minimal lets a flag override an env var that enables it.
+minimal=0
+case "${PLANNOTATOR_MINIMAL:-}" in
+ 1|true|yes|TRUE|YES|True|Yes) minimal=1 ;;
+esac
+if [ "$MINIMAL_FLAG" -ne -1 ]; then
+ minimal="$MINIMAL_FLAG"
+fi
+
case "$(uname -s)" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
@@ -379,6 +427,39 @@ chmod +x "$INSTALL_DIR/plannotator"
echo ""
echo "plannotator ${latest_tag} installed to ${INSTALL_DIR}/plannotator"
+# Print the PATH-setup hint if $INSTALL_DIR isn't already on PATH. Extracted so
+# both the normal flow and the --minimal early exit below can reuse it.
+print_path_advice() {
+ if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
+ echo ""
+ echo "${INSTALL_DIR} is not in your PATH. Add it with:"
+ echo ""
+
+ case "$SHELL" in
+ */zsh) shell_config="~/.zshrc" ;;
+ */bash) shell_config="~/.bashrc" ;;
+ *) shell_config="your shell config" ;;
+ esac
+
+ echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ${shell_config}"
+ echo " source ${shell_config}"
+ fi
+}
+
+# Binary-only mode stops here: the binary is installed, so print PATH advice and
+# exit before any sidecar download, agent integration, skill checkout, config
+# write, cache clear, or cleanup migration runs. No persistent state is written
+# outside $INSTALL_DIR (the temp download file was already cleaned up above; the
+# config dir may have been read, never written). See the MINIMAL_FLAG /
+# PLANNOTATOR_MINIMAL resolution near the top.
+if [ "$minimal" -eq 1 ]; then
+ print_path_advice
+ echo ""
+ echo "Minimal install complete — only the plannotator binary was installed."
+ echo "No skills, hooks, agent integrations, or config files were written."
+ exit 0
+fi
+
sem_asset_for_platform() {
case "$platform" in
darwin-arm64) echo "sem-darwin-arm64.tar.gz" ;;
@@ -496,20 +577,7 @@ install_agent_terminal_runtime() {
install_sem_sidecar
install_agent_terminal_runtime
-if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
- echo ""
- echo "${INSTALL_DIR} is not in your PATH. Add it with:"
- echo ""
-
- case "$SHELL" in
- */zsh) shell_config="~/.zshrc" ;;
- */bash) shell_config="~/.bashrc" ;;
- *) shell_config="your shell config" ;;
- esac
-
- echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ${shell_config}"
- echo " source ${shell_config}"
-fi
+print_path_advice
# --- Codex CLI / Desktop app support (only if Codex is installed or configured) ---
# Codex stores config and state under $CODEX_HOME when set, falling back to
diff --git a/scripts/install.test.ts b/scripts/install.test.ts
index 2d95376d4..98eee06ae 100644
--- a/scripts/install.test.ts
+++ b/scripts/install.test.ts
@@ -284,6 +284,62 @@ describe("install.sh", () => {
expect(script).toContain('GEMINI_POLICY_EOF');
expect(script).toContain('GEMINI_SETTINGS_EOF');
});
+
+ test("--minimal flag and PLANNOTATOR_MINIMAL env var are documented", () => {
+ // Usage text advertises the flag and the env-var opt-in for curl | bash.
+ expect(script).toContain("--minimal");
+ expect(script).toContain("PLANNOTATOR_MINIMAL");
+ // Accepts both --minimal and the --binary-only alias, plus the opt-out.
+ expect(script).toContain("--minimal|--binary-only)");
+ expect(script).toContain("--no-minimal)");
+ });
+
+ test("minimal mode is resolved from flag with env-var fallback", () => {
+ // A flag (--minimal or --no-minimal) wins over the env var, which wins over
+ // the default (off). MINIMAL_FLAG stays -1 until a flag sets 0 or 1.
+ expect(script).toContain("MINIMAL_FLAG=-1");
+ expect(script).toContain('case "${PLANNOTATOR_MINIMAL:-}" in');
+ expect(script).toContain('if [ "$MINIMAL_FLAG" -ne -1 ]; then');
+ // --minimal and --no-minimal are mutually exclusive.
+ expect(script).toContain("--minimal and --no-minimal are mutually exclusive");
+ });
+
+ test("minimal mode exits after the binary install, before any extras", () => {
+ // The early exit must come AFTER the binary is moved into place but BEFORE
+ // the sidecar downloads, agent integrations, skill checkout, and config
+ // writes — that ordering is the whole point of #977.
+ const binaryInstalled = script.indexOf(
+ 'mv "$tmp_file" "$INSTALL_DIR/plannotator"',
+ );
+ const minimalExit = script.indexOf('if [ "$minimal" -eq 1 ]; then');
+ const semInstall = script.indexOf("install_sem_sidecar\n");
+ const agentTerminal = script.indexOf("install_agent_terminal_runtime\n");
+ const codexBlock = script.indexOf(
+ "# --- Codex CLI / Desktop app support",
+ );
+ const skillsCheckout = script.indexOf(
+ "git clone --depth 1 --filter=blob:none --sparse",
+ );
+
+ expect(binaryInstalled).toBeGreaterThan(0);
+ expect(minimalExit).toBeGreaterThan(binaryInstalled);
+ // Everything the reporter called "trash" runs strictly after the exit gate.
+ expect(semInstall).toBeGreaterThan(minimalExit);
+ expect(agentTerminal).toBeGreaterThan(minimalExit);
+ expect(codexBlock).toBeGreaterThan(minimalExit);
+ expect(skillsCheckout).toBeGreaterThan(minimalExit);
+ // The gate really exits rather than falling through.
+ const gateBody = script.slice(minimalExit, minimalExit + 400);
+ expect(gateBody).toContain("exit 0");
+ });
+
+ test("PATH advice is a reusable function shared by both paths", () => {
+ // Extracted so the minimal early exit and the normal flow both print it.
+ expect(script).toContain("print_path_advice() {");
+ // Called exactly once inside the minimal gate and once in the normal flow.
+ const calls = script.match(/^\s*print_path_advice$/gm) ?? [];
+ expect(calls.length).toBe(2);
+ });
});
describe("install.ps1", () => {
@@ -424,6 +480,35 @@ describe("install.ps1", () => {
expect(skillsInstallIndex).toBeGreaterThan(0);
expect(piUpdateCallIndex).toBeGreaterThan(skillsInstallIndex);
});
+
+ test("supports -Minimal / -BinaryOnly binary-only mode with env-var fallback", () => {
+ // Switch + alias in the param block, plus the PLANNOTATOR_MINIMAL env fallback.
+ expect(script).toContain('[Alias("BinaryOnly")]');
+ expect(script).toContain("[switch]$Minimal");
+ expect(script).toContain("[switch]$NoMinimal");
+ expect(script).toContain("$env:PLANNOTATOR_MINIMAL");
+ // -Minimal / -NoMinimal are mutually exclusive (parity with sh/cmd).
+ expect(script).toContain("-Minimal and -NoMinimal are mutually exclusive");
+ });
+
+ test("minimal mode exits after the binary install, before any extras", () => {
+ // Same ordering guarantee as install.sh: binary placed, then the early exit,
+ // then (only in the full install) the sidecar + integration work.
+ const binaryInstalled = script.indexOf(
+ 'Move-Item -Force $tmpFile "$installDir\\plannotator.exe"',
+ );
+ const minimalExit = script.indexOf("if ($minimal) {");
+ const semInstall = script.indexOf("Install-SemSidecar\n");
+ const pathAdvice = script.indexOf("function Show-PathAdvice");
+
+ expect(binaryInstalled).toBeGreaterThan(0);
+ expect(pathAdvice).toBeGreaterThan(binaryInstalled);
+ expect(minimalExit).toBeGreaterThan(binaryInstalled);
+ expect(semInstall).toBeGreaterThan(minimalExit);
+ // The gate exits rather than falling through.
+ const gateBody = script.slice(minimalExit, minimalExit + 400);
+ expect(gateBody).toContain("exit 0");
+ });
});
describe("install.cmd", () => {
@@ -580,6 +665,36 @@ describe("install.cmd", () => {
// Enforcement: hard-fail when opted in but gh missing
expect(script).toContain("gh CLI was not found");
});
+
+ test("supports --minimal / --binary-only binary-only mode with env-var fallback", () => {
+ expect(script).toContain('if /i "%~1"=="--minimal"');
+ expect(script).toContain('if /i "%~1"=="--binary-only"');
+ expect(script).toContain('if /i "%~1"=="--no-minimal"');
+ expect(script).toContain("PLANNOTATOR_MINIMAL");
+ // Usage string advertises the flag.
+ expect(script).toContain("[--minimal ^| --no-minimal]");
+ // --minimal / --no-minimal are mutually exclusive (parity with sh/ps1).
+ expect(script).toContain("--minimal and --no-minimal are mutually exclusive");
+ });
+
+ test("minimal mode exits after the binary install, before any extras", () => {
+ const binaryInstalled = script.indexOf(
+ 'move /y "!TEMP_FILE!" "!INSTALL_PATH!"',
+ );
+ const minimalExit = script.indexOf('if "!MINIMAL!"=="1" (');
+ const semInstall = script.indexOf("call :InstallSemSidecar");
+ const printPathAdvice = script.indexOf(":PrintPathAdvice");
+
+ expect(binaryInstalled).toBeGreaterThan(0);
+ expect(minimalExit).toBeGreaterThan(binaryInstalled);
+ expect(semInstall).toBeGreaterThan(minimalExit);
+ // The gate exits rather than falling through, and reuses :PrintPathAdvice.
+ const gateBody = script.slice(minimalExit, minimalExit + 400);
+ expect(gateBody).toContain("call :PrintPathAdvice");
+ expect(gateBody).toContain("exit /b 0");
+ // :PrintPathAdvice is defined as a subroutine.
+ expect(printPathAdvice).toBeGreaterThan(0);
+ });
});
describe("Core Plannotator skills", () => {
@@ -639,6 +754,26 @@ describe("install shared behavior", () => {
expect(cmdScript).not.toContain("/dev/null");
});
+ test("binary-only (minimal) mode exists in all three installers", () => {
+ const cmdScript = readFileSync(join(scriptsDir, "install.cmd"), "utf-8");
+ // Every installer exposes the flag, its --binary-only / -BinaryOnly alias,
+ // the explicit opt-out, and the PLANNOTATOR_MINIMAL env-var fallback — so a
+ // user gets the same binary-only path whatever host they install from.
+ expect(sh).toContain("--minimal|--binary-only)");
+ expect(sh).toContain("--no-minimal)");
+ expect(sh).toContain("PLANNOTATOR_MINIMAL");
+
+ expect(ps).toContain('[Alias("BinaryOnly")]');
+ expect(ps).toContain("[switch]$Minimal");
+ expect(ps).toContain("[switch]$NoMinimal");
+ expect(ps).toContain("$env:PLANNOTATOR_MINIMAL");
+
+ expect(cmdScript).toContain('if /i "%~1"=="--minimal"');
+ expect(cmdScript).toContain('if /i "%~1"=="--binary-only"');
+ expect(cmdScript).toContain('if /i "%~1"=="--no-minimal"');
+ expect(cmdScript).toContain("PLANNOTATOR_MINIMAL");
+ });
+
test("guided install exists in all three installers with safe automation behavior", () => {
const cmdScript = readFileSync(join(scriptsDir, "install.cmd"), "utf-8");
// Shared prefs file (same format across platforms) in the data dir.