Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
76 changes: 61 additions & 15 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,18 @@ EXTRAS_FLAG=""
MODEL_INVOCABLE_FLAG=""
NON_INTERACTIVE=0
RECONFIGURE=0
# Binary-only mode. Installs just the plannotator binary (to $INSTALL_DIR) and
# nothing else — no sem sidecar, no agent-terminal runtime, no skills, hooks,
# slash commands, or per-agent config (Claude, Codex, OpenCode, Gemini, Kiro).
# -1 = flag not set (fall through to env var); 0 = off; 1 = on. Resolved after
# arg parsing so PLANNOTATOR_MINIMAL can enable it for `curl | bash` runs.
MINIMAL_FLAG=-1
Comment on lines +43 to +49

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c112e39. Added an explicit --no-minimal (sets MINIMAL_FLAG=0), so a CLI flag can now override PLANNOTATOR_MINIMAL=1 in either direction and the -1/0/1 states in the comment are all reachable. --minimal/--no-minimal are mutually exclusive, mirroring --extras/--no-extras. Same flag added to install.ps1 (-NoMinimal) and install.cmd for parity.


usage() {
cat <<'USAGE'
Usage: install.sh [--version <tag>] [--verify-attestation | --skip-attestation]
[--extras | --no-extras] [--model-invocable <list>|none]
[--non-interactive] [--reconfigure] [--help]
[--minimal] [--non-interactive] [--reconfigure] [--help]
install.sh <tag>

Options:
Expand All @@ -63,6 +69,13 @@ Options:
--model-invocable <l> 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. 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). Nothing is written outside
$HOME/.local/bin. Also enabled by exporting
PLANNOTATOR_MINIMAL=1.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c112e39. The usage text now documents the --binary-only alias and the new --no-minimal, and the wording is softened to "No persistent state is written outside $HOME/.local/bin (a temp download file is still used and removed)" to avoid overpromising about mktemp/curl temp files and the config-dir read.

--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).
Expand Down Expand Up @@ -189,6 +202,10 @@ while [ $# -gt 0 ]; do
RECONFIGURE=1
shift
;;
--minimal|--binary-only)
MINIMAL_FLAG=1
shift
;;
-h|--help)
usage
exit 0
Expand All @@ -214,6 +231,17 @@ while [ $# -gt 0 ]; do
esac
done

# Resolve binary-only mode. Precedence: --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.
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" ;;
Expand Down Expand Up @@ -379,6 +407,37 @@ 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. Nothing outside $INSTALL_DIR is
# touched. See the MINIMAL_FLAG / PLANNOTATOR_MINIMAL resolution near the top.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c112e39. Reworded to "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)."

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" ;;
Expand Down Expand Up @@ -496,20 +555,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
Expand Down
52 changes: 52 additions & 0 deletions scripts/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,58 @@ 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.
expect(script).toContain("--minimal|--binary-only)");
});

test("minimal mode is resolved from flag with env-var fallback", () => {
// Flag wins over env var, which wins over the default (off).
expect(script).toContain("MINIMAL_FLAG=-1");
expect(script).toContain('case "${PLANNOTATOR_MINIMAL:-}" in');
expect(script).toContain('if [ "$MINIMAL_FLAG" -ne -1 ]; then');
});

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", () => {
Expand Down