mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
db86d38ca4
* feat(skills): add the plannotator knowledge-layer skill with a CLI freshness guard A new model-invocable core skill (apps/skills/core/plannotator) that teaches an agent the whole CLI surface: decision guide, per-command reference with flags and exit codes, env vars, the external-annotations API, and a do-not list. The existing plannotator-* core skills stay lightweight action stubs. A freshness test (apps/hook/server/plannotator-skill-reference.test.ts) parses the skill's documented subcommands and flags and diffs them against cli.ts usage text plus the CLI arg-parsing sources, in both directions, so the reference cannot drift from the real CLI without failing the suite. Installers copy the single-sourced core body into ~/.claude/skills and ~/.agents/skills on all three platforms; uninstall removes it from both scopes. The skill ships model-invocable as a documented exception to the locked-by-default rule, asserted both ways in install.test.ts. * feat(marketing): serve the plannotator knowledge skill as /llms.txt Single-sourced at build time from apps/skills/core/plannotator/SKILL.md per the llmstxt.org spec (H1, blockquote, detail sections, Docs link list), so the CLI freshness guard transitively keeps llms.txt current. * fix(skills): reach every install path with the plannotator knowledge skill The knowledge skill reached Claude Code and ~/.agents but was missing from three install paths. Six fixes from the install-reach review of #1377. Kiro: the installer's Kiro leg copied only the two action skills, so Kiro users got launchers and no CLI reference. One copy line per installer, and "plannotator" joins uninstall.ts's KIRO_SKILLS. OpenCode npm: @plannotator/opencode's postinstall copied only commands/*.md. The package now ships the skill (copied at build time like the HTML assets, gitignored so the shipped copy cannot drift) and postinstall places it under ${XDG_CONFIG_HOME:-$HOME/.config}/opencode/skills/plannotator/, which is a path OpenCode really scans ({skill,skills}/**/SKILL.md under xdgConfig/ opencode). Uninstall sweeps it, skills only, so a user's own opencode/commands/plannotator.md stays out of scope. Pi npm: vendor.sh copies the skill to apps/pi-extension/skills/plannotator/ and package.json declares it under pi.skills, which Pi resolves relative to the package root. Neither vendored copy carries the // @generated header the .ts files use: a SKILL.md must open with its frontmatter on line 1. llms.txt: the endpoint resolved the skill through process.cwd(), which breaks under any invocation but --cwd apps/marketing. new URL(import.meta.url) does not fix it either, because Vite rewrites import.meta.url to the emitted SSR chunk's location. Inlined with Vite's ?raw, resolved by the bundler relative to the source file. Also drops the summary paragraph the required blockquote already carries; SKILL.md itself is unchanged. Uninstall: KNOWLEDGE_SKILLS is a separate list from CORE_SKILLS precisely so the bare name "plannotator" cannot leak into LEGACY_COMMAND_NAMES or STALE_CODEX_SKILLS and delete a user's own files. Nothing tested that; now a test proves the five installed scopes are removed and commands/plannotator.md (Claude and OpenCode) plus ~/.codex/skills/plannotator survive. Also cleanupStaleSkillLayout now knows KNOWLEDGE_SKILLS. Origins: oh-my-pi (#1373) was missing from SKILL.md's PLANNOTATOR_ORIGIN row. The guard now imports AGENT_CONFIG and asserts the row names every key and invents none, and its header comment is narrowed to what it actually proves: bidirectional for subcommands and origins, one-directional for flags. AI-assisted (Claude) under maintainer direction.
1816 lines
84 KiB
Batchfile
1816 lines
84 KiB
Batchfile
@echo off
|
|
setlocal enabledelayedexpansion
|
|
|
|
REM Plannotator Windows CMD Bootstrap Script
|
|
|
|
REM Parse command line arguments
|
|
set "VERSION=latest"
|
|
REM Tracks whether a version was explicitly set via --version or positional.
|
|
REM Used to reject mixing --version <tag> with a stray positional token.
|
|
set "VERSION_EXPLICIT=0"
|
|
REM Three-layer opt-in for SLSA provenance verification.
|
|
REM Precedence: CLI flag > env var > %USERPROFILE%\.plannotator\config.json > default.
|
|
REM -1 = flag not set (fall through); 0 = disable; 1 = enable.
|
|
set "VERIFY_ATTESTATION_FLAG=-1"
|
|
REM Opt-in install of the pruned CallDiff call-flow core (default off; the
|
|
REM review UI offers a one-click install). Precedence: --with-call-flow >
|
|
REM PLANNOTATOR_INSTALL_CALLDIFF > config installCallFlow > default (off).
|
|
REM -1 = flag not set (fall through); 1 = enable.
|
|
set "WITH_CALL_FLOW_FLAG=-1"
|
|
REM Guided-install answers. Precedence: CLI flags > wizard (interactive, first
|
|
REM run or --reconfigure) > saved prefs from a previous run > defaults.
|
|
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"
|
|
REM Per-agent integration opt-outs (#1178). Skip means do-not-write: a skipped
|
|
REM agent's home is neither written to nor cleaned up, and detected-but-skipped
|
|
REM is reported honestly. Resolution (flag > env var > config skipInstall.<agent>
|
|
REM > default off) happens after _CONFIG_DIR is known.
|
|
set "SKIP_CODEX_FLAG=0"
|
|
set "SKIP_GEMINI_FLAG=0"
|
|
set "SKIP_KIRO_FLAG=0"
|
|
set "SKIP_OPENCODE_FLAG=0"
|
|
REM Same shape, but scoped to the skills/slash-command sparse checkout rather
|
|
REM than one agent's home: --skip-skills turns the whole fetch into a no-op for
|
|
REM every scope it writes (Claude, .agents, OpenCode, Gemini, Kiro), including
|
|
REM the extras and the skill-scope cleanup sweeps.
|
|
set "SKIP_SKILLS_FLAG=0"
|
|
|
|
:parse_args
|
|
if "%~1"=="" goto args_done
|
|
if /i "%~1"=="--version" (
|
|
if "%~2"=="" (
|
|
echo --version requires an argument >&2
|
|
exit /b 1
|
|
)
|
|
REM Reject dash-prefixed values - prevents `install.cmd --version
|
|
REM --skip-attestation` from silently setting VERSION=--skip-attestation.
|
|
set "NEXT_ARG=%~2"
|
|
if "!NEXT_ARG:~0,1!"=="-" (
|
|
echo --version requires a tag value, got flag: "%~2" >&2
|
|
exit /b 1
|
|
)
|
|
set "VERSION=%~2"
|
|
set "VERSION_EXPLICIT=1"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--verify-attestation" (
|
|
if "!VERIFY_ATTESTATION_FLAG!"=="0" (
|
|
echo --verify-attestation and --skip-attestation are mutually exclusive >&2
|
|
exit /b 1
|
|
)
|
|
set "VERIFY_ATTESTATION_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--skip-attestation" (
|
|
if "!VERIFY_ATTESTATION_FLAG!"=="1" (
|
|
echo --skip-attestation and --verify-attestation are mutually exclusive >&2
|
|
exit /b 1
|
|
)
|
|
set "VERIFY_ATTESTATION_FLAG=0"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--with-call-flow" (
|
|
set "WITH_CALL_FLOW_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--extras" (
|
|
set "EXTRAS_FLAG=yes"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--no-extras" (
|
|
set "EXTRAS_FLAG=no"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--model-invocable" (
|
|
if "%~2"=="" (
|
|
echo --model-invocable requires a comma-separated skill list or 'none' >&2
|
|
exit /b 1
|
|
)
|
|
set "MODEL_INVOCABLE_FLAG=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--non-interactive" (
|
|
set "NON_INTERACTIVE=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--yes" (
|
|
set "NON_INTERACTIVE=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--reconfigure" (
|
|
set "RECONFIGURE=1"
|
|
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
|
|
)
|
|
if /i "%~1"=="--skip-codex" (
|
|
set "SKIP_CODEX_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--skip-gemini" (
|
|
set "SKIP_GEMINI_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--skip-kiro" (
|
|
set "SKIP_KIRO_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--skip-opencode" (
|
|
set "SKIP_OPENCODE_FLAG=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--skip-skills" (
|
|
set "SKIP_SKILLS_FLAG=1"
|
|
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/...).
|
|
REM
|
|
REM Uses a variable-assigned substring test instead of `echo %~1 | findstr`
|
|
REM because unquoted %~1 in an echo pipe lets cmd.exe interpret shell
|
|
REM metacharacters (& | > <) in the argument before the pipe runs. Assigning
|
|
REM to a `set "VAR=%~1"` literal-quoted form preserves metacharacters safely,
|
|
REM and delayed-expansion substring (!VAR:~0,1!) avoids the subprocess entirely.
|
|
REM The error-message echo also quotes "%~1" for the same reason - echoing an
|
|
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 ^<tag^>] [--verify-attestation ^| --skip-attestation] [--with-call-flow] [--extras ^| --no-extras] [--model-invocable ^<list^>] [--minimal ^| --no-minimal] [--skip-codex] [--skip-gemini] [--skip-kiro] [--skip-opencode] [--skip-skills] [--non-interactive] [--reconfigure] >&2
|
|
exit /b 1
|
|
)
|
|
REM Positional form: install.cmd vX.Y.Z (legacy interface).
|
|
REM Reject if --version was already passed - silent overwrite is worse
|
|
REM than a clean usage error.
|
|
if "!VERSION_EXPLICIT!"=="1" (
|
|
echo Unexpected positional argument: "%~1" ^(version already set^) >&2
|
|
exit /b 1
|
|
)
|
|
set "VERSION=%~1"
|
|
set "VERSION_EXPLICIT=1"
|
|
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"
|
|
set "INSTALL_DIR=%USERPROFILE%\.local\bin"
|
|
|
|
REM First plannotator release that carries SLSA build-provenance attestations.
|
|
REM See scripts/install.sh for the full explanation - this constant is
|
|
REM bumped once at the first attested release via the release skill.
|
|
set "MIN_ATTESTED_VERSION=v0.17.2"
|
|
|
|
REM Attestation-bundle fetch helper, passed to PowerShell via -EncodedCommand
|
|
REM so NO script file ever touches %%TEMP%% (a %%RANDOM%%-named .ps1 would be a
|
|
REM predictable-path code-execution vector). Inputs travel via env vars
|
|
REM (REPO, ATT_DIGEST, ATT_BUNDLE_FILE); exit codes: 0 = bundle written,
|
|
REM 2 = fetch failed, 3 = nothing extracted. The blob decodes to EXACTLY the
|
|
REM script below (UTF-16LE base64). scripts/install.test.ts decodes the blob,
|
|
REM asserts it matches these REM PS: lines byte for byte, and executes its
|
|
REM scanner against a captured real attestations response - so the blob
|
|
REM cannot silently drift from what is documented here. Regenerate after
|
|
REM editing the REM PS: lines:
|
|
REM [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($script))
|
|
REM PS: $ErrorActionPreference = 'Stop'
|
|
REM PS: try {
|
|
REM PS: $resp = Invoke-WebRequest -Uri ('https://api.github.com/repos/' + $env:REPO + '/attestations/sha256:' + $env:ATT_DIGEST) -UseBasicParsing -TimeoutSec 30
|
|
REM PS: $raw = if ($resp.Content -is [byte[]]) { [System.Text.Encoding]::UTF8.GetString($resp.Content) } else { [string]$resp.Content }
|
|
REM PS: } catch { exit 2 }
|
|
REM PS: $bundles = @()
|
|
REM PS: $searchFrom = 0
|
|
REM PS: while ($true) {
|
|
REM PS: $keyIdx = $raw.IndexOf('"bundle"', $searchFrom, [System.StringComparison]::Ordinal)
|
|
REM PS: if ($keyIdx -lt 0) { break }
|
|
REM PS: $searchFrom = $keyIdx + 8
|
|
REM PS: $i = $keyIdx + 8
|
|
REM PS: while ($i -lt $raw.Length -and [char]::IsWhiteSpace($raw[$i])) { $i++ }
|
|
REM PS: if ($i -ge $raw.Length -or $raw[$i] -ne ':') { continue }
|
|
REM PS: $i++
|
|
REM PS: while ($i -lt $raw.Length -and [char]::IsWhiteSpace($raw[$i])) { $i++ }
|
|
REM PS: if ($i -ge $raw.Length -or $raw[$i] -ne '{') { continue }
|
|
REM PS: $depth = 0
|
|
REM PS: $inString = $false
|
|
REM PS: $escaped = $false
|
|
REM PS: $start = $i
|
|
REM PS: for (; $i -lt $raw.Length; $i++) {
|
|
REM PS: $ch = $raw[$i]
|
|
REM PS: if ($escaped) { $escaped = $false; continue }
|
|
REM PS: if ($inString) {
|
|
REM PS: if ($ch -eq '\') { $escaped = $true }
|
|
REM PS: elseif ($ch -eq '"') { $inString = $false }
|
|
REM PS: continue
|
|
REM PS: }
|
|
REM PS: if ($ch -eq '"') { $inString = $true; continue }
|
|
REM PS: if ($ch -eq '{') { $depth++; continue }
|
|
REM PS: if ($ch -eq '}') {
|
|
REM PS: $depth--
|
|
REM PS: if ($depth -eq 0) {
|
|
REM PS: $bundles += $raw.Substring($start, $i - $start + 1)
|
|
REM PS: $searchFrom = $i + 1
|
|
REM PS: break
|
|
REM PS: }
|
|
REM PS: }
|
|
REM PS: }
|
|
REM PS: }
|
|
REM PS: if ($bundles.Count -eq 0) { exit 3 }
|
|
REM PS: try { [System.IO.File]::WriteAllText($env:ATT_BUNDLE_FILE, (($bundles -join [char]10) + [char]10)) } catch { exit 3 }
|
|
REM PS: exit 0
|
|
set "ATT_FETCH_B64=JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKAHQAcgB5ACAAewAKACAAIAAgACAAJAByAGUAcwBwACAAPQAgAEkAbgB2AG8AawBlAC0AVwBlAGIAUgBlAHEAdQBlAHMAdAAgAC0AVQByAGkAIAAoACcAaAB0AHQAcABzADoALwAvAGEAcABpAC4AZwBpAHQAaAB1AGIALgBjAG8AbQAvAHIAZQBwAG8AcwAvACcAIAArACAAJABlAG4AdgA6AFIARQBQAE8AIAArACAAJwAvAGEAdAB0AGUAcwB0AGEAdABpAG8AbgBzAC8AcwBoAGEAMgA1ADYAOgAnACAAKwAgACQAZQBuAHYAOgBBAFQAVABfAEQASQBHAEUAUwBUACkAIAAtAFUAcwBlAEIAYQBzAGkAYwBQAGEAcgBzAGkAbgBnACAALQBUAGkAbQBlAG8AdQB0AFMAZQBjACAAMwAwAAoAIAAgACAAIAAkAHIAYQB3ACAAPQAgAGkAZgAgACgAJAByAGUAcwBwAC4AQwBvAG4AdABlAG4AdAAgAC0AaQBzACAAWwBiAHkAdABlAFsAXQBdACkAIAB7ACAAWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAJAByAGUAcwBwAC4AQwBvAG4AdABlAG4AdAApACAAfQAgAGUAbABzAGUAIAB7ACAAWwBzAHQAcgBpAG4AZwBdACQAcgBlAHMAcAAuAEMAbwBuAHQAZQBuAHQAIAB9AAoAfQAgAGMAYQB0AGMAaAAgAHsAIABlAHgAaQB0ACAAMgAgAH0ACgAkAGIAdQBuAGQAbABlAHMAIAA9ACAAQAAoACkACgAkAHMAZQBhAHIAYwBoAEYAcgBvAG0AIAA9ACAAMAAKAHcAaABpAGwAZQAgACgAJAB0AHIAdQBlACkAIAB7AAoAIAAgACAAIAAkAGsAZQB5AEkAZAB4ACAAPQAgACQAcgBhAHcALgBJAG4AZABlAHgATwBmACgAJwAiAGIAdQBuAGQAbABlACIAJwAsACAAJABzAGUAYQByAGMAaABGAHIAbwBtACwAIABbAFMAeQBzAHQAZQBtAC4AUwB0AHIAaQBuAGcAQwBvAG0AcABhAHIAaQBzAG8AbgBdADoAOgBPAHIAZABpAG4AYQBsACkACgAgACAAIAAgAGkAZgAgACgAJABrAGUAeQBJAGQAeAAgAC0AbAB0ACAAMAApACAAewAgAGIAcgBlAGEAawAgAH0ACgAgACAAIAAgACQAcwBlAGEAcgBjAGgARgByAG8AbQAgAD0AIAAkAGsAZQB5AEkAZAB4ACAAKwAgADgACgAgACAAIAAgACQAaQAgAD0AIAAkAGsAZQB5AEkAZAB4ACAAKwAgADgACgAgACAAIAAgAHcAaABpAGwAZQAgACgAJABpACAALQBsAHQAIAAkAHIAYQB3AC4ATABlAG4AZwB0AGgAIAAtAGEAbgBkACAAWwBjAGgAYQByAF0AOgA6AEkAcwBXAGgAaQB0AGUAUwBwAGEAYwBlACgAJAByAGEAdwBbACQAaQBdACkAKQAgAHsAIAAkAGkAKwArACAAfQAKACAAIAAgACAAaQBmACAAKAAkAGkAIAAtAGcAZQAgACQAcgBhAHcALgBMAGUAbgBnAHQAaAAgAC0AbwByACAAJAByAGEAdwBbACQAaQBdACAALQBuAGUAIAAnADoAJwApACAAewAgAGMAbwBuAHQAaQBuAHUAZQAgAH0ACgAgACAAIAAgACQAaQArACsACgAgACAAIAAgAHcAaABpAGwAZQAgACgAJABpACAALQBsAHQAIAAkAHIAYQB3AC4ATABlAG4AZwB0AGgAIAAtAGEAbgBkACAAWwBjAGgAYQByAF0AOgA6AEkAcwBXAGgAaQB0AGUAUwBwAGEAYwBlACgAJAByAGEAdwBbACQAaQBdACkAKQAgAHsAIAAkAGkAKwArACAAfQAKACAAIAAgACAAaQBmACAAKAAkAGkAIAAtAGcAZQAgACQAcgBhAHcALgBMAGUAbgBnAHQAaAAgAC0AbwByACAAJAByAGEAdwBbACQAaQBdACAALQBuAGUAIAAnAHsAJwApACAAewAgAGMAbwBuAHQAaQBuAHUAZQAgAH0ACgAgACAAIAAgACQAZABlAHAAdABoACAAPQAgADAACgAgACAAIAAgACQAaQBuAFMAdAByAGkAbgBnACAAPQAgACQAZgBhAGwAcwBlAAoAIAAgACAAIAAkAGUAcwBjAGEAcABlAGQAIAA9ACAAJABmAGEAbABzAGUACgAgACAAIAAgACQAcwB0AGEAcgB0ACAAPQAgACQAaQAKACAAIAAgACAAZgBvAHIAIAAoADsAIAAkAGkAIAAtAGwAdAAgACQAcgBhAHcALgBMAGUAbgBnAHQAaAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAIAAgACAAIAAkAGMAaAAgAD0AIAAkAHIAYQB3AFsAJABpAF0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAGUAcwBjAGEAcABlAGQAKQAgAHsAIAAkAGUAcwBjAGEAcABlAGQAIAA9ACAAJABmAGEAbABzAGUAOwAgAGMAbwBuAHQAaQBuAHUAZQAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAGkAbgBTAHQAcgBpAG4AZwApACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABjAGgAIAAtAGUAcQAgACcAXAAnACkAIAB7ACAAJABlAHMAYwBhAHAAZQBkACAAPQAgACQAdAByAHUAZQAgAH0ACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABlAGwAcwBlAGkAZgAgACgAJABjAGgAIAAtAGUAcQAgACcAIgAnACkAIAB7ACAAJABpAG4AUwB0AHIAaQBuAGcAIAA9ACAAJABmAGEAbABzAGUAIAB9AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAYwBvAG4AdABpAG4AdQBlAAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAGMAaAAgAC0AZQBxACAAJwAiACcAKQAgAHsAIAAkAGkAbgBTAHQAcgBpAG4AZwAgAD0AIAAkAHQAcgB1AGUAOwAgAGMAbwBuAHQAaQBuAHUAZQAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAGMAaAAgAC0AZQBxACAAJwB7ACcAKQAgAHsAIAAkAGQAZQBwAHQAaAArACsAOwAgAGMAbwBuAHQAaQBuAHUAZQAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAGMAaAAgAC0AZQBxACAAJwB9ACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGQAZQBwAHQAaAAtAC0ACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAZABlAHAAdABoACAALQBlAHEAIAAwACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGIAdQBuAGQAbABlAHMAIAArAD0AIAAkAHIAYQB3AC4AUwB1AGIAcwB0AHIAaQBuAGcAKAAkAHMAdABhAHIAdAAsACAAJABpACAALQAgACQAcwB0AGEAcgB0ACAAKwAgADEAKQAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABzAGUAYQByAGMAaABGAHIAbwBtACAAPQAgACQAaQAgACsAIAAxAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABiAHIAZQBhAGsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgAH0ACgB9AAoAaQBmACAAKAAkAGIAdQBuAGQAbABlAHMALgBDAG8AdQBuAHQAIAAtAGUAcQAgADAAKQAgAHsAIABlAHgAaQB0ACAAMwAgAH0ACgB0AHIAeQAgAHsAIABbAFMAeQBzAHQAZQBtAC4ASQBPAC4ARgBpAGwAZQBdADoAOgBXAHIAaQB0AGUAQQBsAGwAVABlAHgAdAAoACQAZQBuAHYAOgBBAFQAVABfAEIAVQBOAEQATABFAF8ARgBJAEwARQAsACAAKAAoACQAYgB1AG4AZABsAGUAcwAgAC0AagBvAGkAbgAgAFsAYwBoAGEAcgBdADEAMAApACAAKwAgAFsAYwBoAGEAcgBdADEAMAApACkAIAB9ACAAYwBhAHQAYwBoACAAewAgAGUAeABpAHQAIAAzACAAfQAKAGUAeABpAHQAIAAwAAoA"
|
|
|
|
REM Detect architecture. Native ARM64 Windows binaries are built from
|
|
REM bun-windows-arm64 (stable since Bun v1.3.10), so ARM64 hosts get a
|
|
REM native binary - no Windows x86-64 emulation tax. PROCESSOR_ARCHITECTURE
|
|
REM reports the architecture the current cmd.exe process is running under;
|
|
REM PROCESSOR_ARCHITEW6432 is set only in 32-bit processes running via
|
|
REM WoW64 and reflects the host architecture (covers the edge case of a
|
|
REM 32-bit tool launching install.cmd on an ARM64 machine).
|
|
set "PLATFORM="
|
|
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "PLATFORM=win32-x64"
|
|
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "PLATFORM=win32-arm64"
|
|
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "PLATFORM=win32-x64"
|
|
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" set "PLATFORM=win32-arm64"
|
|
|
|
if "!PLATFORM!"=="" (
|
|
echo Plannotator does not support 32-bit Windows. >&2
|
|
exit /b 1
|
|
)
|
|
|
|
REM Check for curl availability
|
|
curl --version >nul 2>&1
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo curl is required but not available. Please use the PowerShell installer. >&2
|
|
exit /b 1
|
|
)
|
|
|
|
REM Create install directory
|
|
if not exist "!INSTALL_DIR!" mkdir "!INSTALL_DIR!"
|
|
|
|
REM Get version to install
|
|
if /i "!VERSION!"=="latest" (
|
|
echo Fetching latest version...
|
|
|
|
REM api.github.com caps unauthenticated requests at 60/hour per source IP,
|
|
REM which fails installs behind shared egress IPs (NAT/CGNAT/corporate
|
|
REM proxies) and during repeated/debug runs within an hour. Attach an
|
|
REM Authorization header when a token is available (raises the limit to
|
|
REM 5000/hour); when none is found, fall back to anonymous (unchanged
|
|
REM behavior). Precedence matches `gh`: GITHUB_TOKEN > GH_TOKEN > gh auth token.
|
|
REM Read the env vars via delayed expansion, never percent expansion, so
|
|
REM the value is not re-parsed by cmd's phase-1 expansion - a value
|
|
REM containing metacharacters or exclamation marks can neither inject
|
|
REM commands nor be corrupted here.
|
|
set "GH_TOKEN_VAL="
|
|
if defined GITHUB_TOKEN set "GH_TOKEN_VAL=!GITHUB_TOKEN!"
|
|
if not defined GH_TOKEN_VAL if defined GH_TOKEN set "GH_TOKEN_VAL=!GH_TOKEN!"
|
|
if not defined GH_TOKEN_VAL (
|
|
REM Resolve gh via `where` and invoke the absolute path so the for /f
|
|
REM command line never runs a bare `gh` name, which cmd would resolve
|
|
REM from the current directory first. (`where` itself also searches
|
|
REM the CWD first - accepted limitation, documented here.)
|
|
REM --hostname github.com scopes the fallback to github.com
|
|
REM credentials, so a gh setup whose default host is a GitHub
|
|
REM Enterprise server never leaks a GHES token to api.github.com. On
|
|
REM an ancient gh without the flag, stderr is swallowed and we fall
|
|
REM back to anonymous.
|
|
set "GH_EXE="
|
|
for /f "delims=" %%g in ('where gh 2^>nul') do if not defined GH_EXE set "GH_EXE=%%g"
|
|
if defined GH_EXE (
|
|
for /f "delims=" %%i in ('"!GH_EXE!" auth token --hostname github.com 2^>nul') do set "GH_TOKEN_VAL=%%i"
|
|
)
|
|
set "GH_EXE="
|
|
)
|
|
REM Charset allowlist: GitHub tokens are [A-Za-z0-9_] (plus - to be
|
|
REM safe). Strip every allowed character (cmd substitution is
|
|
REM case-insensitive, covering A-Z too); anything left over means an
|
|
REM unexpected character - a quote in particular could break out of the
|
|
REM quoted Authorization header on the curl line below - so drop the
|
|
REM token and go anonymous. Done with pure delayed-expansion
|
|
REM substitutions: a findstr pipe cannot be used because delayed
|
|
REM expansion does not survive into pipe children, and writing the token
|
|
REM to a temp file for findstr would leak the secret to disk.
|
|
if defined GH_TOKEN_VAL (
|
|
set "TOKEN_RESIDUE=!GH_TOKEN_VAL!"
|
|
for %%c in (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 _ -) do if defined TOKEN_RESIDUE set "TOKEN_RESIDUE=!TOKEN_RESIDUE:%%c=!"
|
|
if defined TOKEN_RESIDUE set "GH_TOKEN_VAL="
|
|
set "TOKEN_RESIDUE="
|
|
)
|
|
if defined GH_TOKEN_VAL (
|
|
set "GH_AUTH_HEADER=-H "Authorization: Bearer !GH_TOKEN_VAL!""
|
|
) else (
|
|
set "GH_AUTH_HEADER="
|
|
)
|
|
|
|
REM Download release info to a randomized temp file so concurrent
|
|
REM invocations don't collide and a same-user pre-placed symlink at
|
|
REM a predictable path can't redirect curl's output.
|
|
set "RELEASE_JSON=%TEMP%\plannotator-release-%RANDOM%.json"
|
|
curl -fsSL !GH_AUTH_HEADER! "https://api.github.com/repos/!REPO!/releases/latest" -o "!RELEASE_JSON!"
|
|
REM A stale/revoked token (expired GITHUB_TOKEN lingering in CI images,
|
|
REM dotfiles, direnv) gets a 401 here and would break an install that
|
|
REM works fine anonymously today. If the authenticated call failed and
|
|
REM we had a token, retry once without the header so a bad token costs
|
|
REM one extra request but never blocks an otherwise-working install.
|
|
REM Note: install.sh / install.ps1 inspect the HTTP status and retry
|
|
REM only on 401; capturing the status portably in batch is not worth the
|
|
REM complexity, so cmd retries on any failure when a token was used - an
|
|
REM accepted cmd-only compromise. See backnotprop/plannotator#1157.
|
|
REM Both ERRORLEVEL reads below sit immediately adjacent to the curl
|
|
REM they test (only REM lines and a no-op if in between); the token
|
|
REM clears deliberately come AFTER the failure check because `set` can
|
|
REM disturb ERRORLEVEL.
|
|
if !ERRORLEVEL! neq 0 if defined GH_AUTH_HEADER (
|
|
curl -fsSL "https://api.github.com/repos/!REPO!/releases/latest" -o "!RELEASE_JSON!"
|
|
)
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Failed to get latest version >&2
|
|
exit /b 1
|
|
)
|
|
REM Drop the local token copies; downloads and git clone are anonymous.
|
|
REM GITHUB_TOKEN / GH_TOKEN themselves remain in the environment exactly
|
|
REM as the user set them.
|
|
set "GH_TOKEN_VAL="
|
|
set "GH_AUTH_HEADER="
|
|
|
|
REM Extract tag_name from JSON
|
|
for /f "tokens=2 delims=:," %%i in ('findstr /c:"\"tag_name\"" "!RELEASE_JSON!"') do (
|
|
set "TAG=%%i"
|
|
set "TAG=!TAG: =!"
|
|
set "TAG=!TAG:"=!"
|
|
)
|
|
del "!RELEASE_JSON!"
|
|
|
|
if "!TAG!"=="" (
|
|
echo Failed to parse version >&2
|
|
exit /b 1
|
|
)
|
|
) else (
|
|
set "TAG=!VERSION!"
|
|
REM Add v prefix if not present. Use a substring test rather than
|
|
REM piping the expanded variable through findstr - an unquoted echo
|
|
REM pipe re-exposes cmd metacharacters (& | > <) in the value before
|
|
REM the pipe runs. Matches the safe pattern used in the arg parser.
|
|
if not "!TAG:~0,1!"=="v" set "TAG=v!TAG!"
|
|
)
|
|
|
|
echo Installing plannotator !TAG!...
|
|
|
|
REM Resolve SLSA build-provenance verification opt-in BEFORE the download so
|
|
REM we can fail fast without wasting bandwidth if the requested tag predates
|
|
REM provenance support. Precedence: CLI flag > env var > config.json > default.
|
|
set "VERIFY_ATTESTATION=0"
|
|
|
|
REM Layer 3: config file (lowest precedence of the opt-in sources).
|
|
REM Unset PLANNOTATOR_DATA_DIR: an existing %USERPROFILE%\.plannotator
|
|
REM (legacy default) always wins; otherwise an explicitly-set absolute
|
|
REM XDG_DATA_HOME (rare on Windows but honored the same way as the runtime;
|
|
REM drive-rooted or UNC) places the directory at XDG_DATA_HOME\plannotator;
|
|
REM otherwise %USERPROFILE%\.plannotator.
|
|
if defined PLANNOTATOR_DATA_DIR (
|
|
set "_CONFIG_DIR=!PLANNOTATOR_DATA_DIR!"
|
|
) else (
|
|
set "_CONFIG_DIR=%USERPROFILE%\.plannotator"
|
|
if not exist "%USERPROFILE%\.plannotator\" if defined XDG_DATA_HOME (
|
|
if "!XDG_DATA_HOME:~1,1!"==":" (
|
|
set "_CONFIG_DIR=!XDG_DATA_HOME!\plannotator"
|
|
) else if "!XDG_DATA_HOME:~0,2!"=="\\" (
|
|
set "_CONFIG_DIR=!XDG_DATA_HOME!\plannotator"
|
|
)
|
|
)
|
|
)
|
|
if /i "!_CONFIG_DIR!"=="~" set "_CONFIG_DIR=%USERPROFILE%"
|
|
if "!_CONFIG_DIR:~0,2!"=="~\" set "_CONFIG_DIR=%USERPROFILE%\!_CONFIG_DIR:~2!"
|
|
if "!_CONFIG_DIR:~0,2!"=="~/" set "_CONFIG_DIR=%USERPROFILE%\!_CONFIG_DIR:~2!"
|
|
if exist "!_CONFIG_DIR!\config.json" (
|
|
findstr /r /c:"\"verifyAttestation\"[ ]*:[ ]*true" "!_CONFIG_DIR!\config.json" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "VERIFY_ATTESTATION=1"
|
|
)
|
|
|
|
REM Layer 2: env var (overrides config file).
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="1" set "VERIFY_ATTESTATION=1"
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="true" set "VERIFY_ATTESTATION=1"
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="yes" set "VERIFY_ATTESTATION=1"
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="0" set "VERIFY_ATTESTATION=0"
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="false" set "VERIFY_ATTESTATION=0"
|
|
if /i "!PLANNOTATOR_VERIFY_ATTESTATION!"=="no" set "VERIFY_ATTESTATION=0"
|
|
|
|
REM Layer 1: CLI flag (overrides everything).
|
|
if "!VERIFY_ATTESTATION_FLAG!"=="1" set "VERIFY_ATTESTATION=1"
|
|
if "!VERIFY_ATTESTATION_FLAG!"=="0" set "VERIFY_ATTESTATION=0"
|
|
|
|
REM Resolve the CallDiff call-flow runtime opt-in. Same three-layer shape as
|
|
REM verifyAttestation: CLI flag > env var > config.json > default (off).
|
|
set "INSTALL_CALL_FLOW=0"
|
|
if exist "!_CONFIG_DIR!\config.json" (
|
|
findstr /r /c:"\"installCallFlow\"[ ]*:[ ]*true" "!_CONFIG_DIR!\config.json" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "INSTALL_CALL_FLOW=1"
|
|
)
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="1" set "INSTALL_CALL_FLOW=1"
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="true" set "INSTALL_CALL_FLOW=1"
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="yes" set "INSTALL_CALL_FLOW=1"
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="0" set "INSTALL_CALL_FLOW=0"
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="false" set "INSTALL_CALL_FLOW=0"
|
|
if /i "!PLANNOTATOR_INSTALL_CALLDIFF!"=="no" set "INSTALL_CALL_FLOW=0"
|
|
if "!WITH_CALL_FLOW_FLAG!"=="1" set "INSTALL_CALL_FLOW=1"
|
|
|
|
REM Resolve the per-agent integration opt-outs (#1178). Same three-layer shape
|
|
REM as verifyAttestation: CLI flag > env var > config skipInstall.<agent> >
|
|
REM default (off). The config layer parses the REAL JSON with PowerShell
|
|
REM (M2): findstr is line-oblivious, so a "codex": true under some OTHER key
|
|
REM would have opted users out, and an explicit false inside skipInstall
|
|
REM would have been ignored. PowerShell reads the actual skipInstall object
|
|
REM (strict boolean check, matching install.ps1) and emits one agent name
|
|
REM per enabled skip; the config path travels via an env var so nothing is
|
|
REM re-parsed as code. Each resolved skip remembers its source so the
|
|
REM detected-but-skipped report can name what the user set.
|
|
set "SKIP_CODEX=0"
|
|
set "SKIP_CODEX_SOURCE="
|
|
set "SKIP_GEMINI=0"
|
|
set "SKIP_GEMINI_SOURCE="
|
|
set "SKIP_KIRO=0"
|
|
set "SKIP_KIRO_SOURCE="
|
|
set "SKIP_OPENCODE=0"
|
|
set "SKIP_OPENCODE_SOURCE="
|
|
REM skipInstall.skills is not an agent - it opts out of the skills/slash-command
|
|
REM checkout for every scope at once - but it shares the same three layers.
|
|
set "SKIP_SKILLS=0"
|
|
set "SKIP_SKILLS_SOURCE="
|
|
if exist "!_CONFIG_DIR!\config.json" (
|
|
set "PLN_CONFIG_JSON=!_CONFIG_DIR!\config.json"
|
|
for /f "usebackq delims=" %%K in (`powershell -NoProfile -Command "try { $c = Get-Content $env:PLN_CONFIG_JSON -Raw | ConvertFrom-Json } catch { exit 0 }; if (-not $c.skipInstall) { exit 0 }; foreach ($k in @('codex','gemini','kiro','opencode','skills')) { $v = $c.skipInstall.$k; if ($v -is [bool] -and $v) { $k } }"`) do (
|
|
if /i "%%K"=="codex" (
|
|
set "SKIP_CODEX=1"
|
|
set "SKIP_CODEX_SOURCE=config skipInstall.codex"
|
|
)
|
|
if /i "%%K"=="gemini" (
|
|
set "SKIP_GEMINI=1"
|
|
set "SKIP_GEMINI_SOURCE=config skipInstall.gemini"
|
|
)
|
|
if /i "%%K"=="kiro" (
|
|
set "SKIP_KIRO=1"
|
|
set "SKIP_KIRO_SOURCE=config skipInstall.kiro"
|
|
)
|
|
if /i "%%K"=="opencode" (
|
|
set "SKIP_OPENCODE=1"
|
|
set "SKIP_OPENCODE_SOURCE=config skipInstall.opencode"
|
|
)
|
|
if /i "%%K"=="skills" (
|
|
set "SKIP_SKILLS=1"
|
|
set "SKIP_SKILLS_SOURCE=config skipInstall.skills"
|
|
)
|
|
)
|
|
set "PLN_CONFIG_JSON="
|
|
)
|
|
for %%V in (1 true yes) do if /i "!PLANNOTATOR_SKIP_CODEX_INSTALL!"=="%%V" (
|
|
set "SKIP_CODEX=1"
|
|
set "SKIP_CODEX_SOURCE=PLANNOTATOR_SKIP_CODEX_INSTALL"
|
|
)
|
|
for %%V in (0 false no) do if /i "!PLANNOTATOR_SKIP_CODEX_INSTALL!"=="%%V" (
|
|
set "SKIP_CODEX=0"
|
|
set "SKIP_CODEX_SOURCE="
|
|
)
|
|
for %%V in (1 true yes) do if /i "!PLANNOTATOR_SKIP_GEMINI_INSTALL!"=="%%V" (
|
|
set "SKIP_GEMINI=1"
|
|
set "SKIP_GEMINI_SOURCE=PLANNOTATOR_SKIP_GEMINI_INSTALL"
|
|
)
|
|
for %%V in (0 false no) do if /i "!PLANNOTATOR_SKIP_GEMINI_INSTALL!"=="%%V" (
|
|
set "SKIP_GEMINI=0"
|
|
set "SKIP_GEMINI_SOURCE="
|
|
)
|
|
for %%V in (1 true yes) do if /i "!PLANNOTATOR_SKIP_KIRO_INSTALL!"=="%%V" (
|
|
set "SKIP_KIRO=1"
|
|
set "SKIP_KIRO_SOURCE=PLANNOTATOR_SKIP_KIRO_INSTALL"
|
|
)
|
|
for %%V in (0 false no) do if /i "!PLANNOTATOR_SKIP_KIRO_INSTALL!"=="%%V" (
|
|
set "SKIP_KIRO=0"
|
|
set "SKIP_KIRO_SOURCE="
|
|
)
|
|
for %%V in (1 true yes) do if /i "!PLANNOTATOR_SKIP_OPENCODE_INSTALL!"=="%%V" (
|
|
set "SKIP_OPENCODE=1"
|
|
set "SKIP_OPENCODE_SOURCE=PLANNOTATOR_SKIP_OPENCODE_INSTALL"
|
|
)
|
|
for %%V in (0 false no) do if /i "!PLANNOTATOR_SKIP_OPENCODE_INSTALL!"=="%%V" (
|
|
set "SKIP_OPENCODE=0"
|
|
set "SKIP_OPENCODE_SOURCE="
|
|
)
|
|
for %%V in (1 true yes) do if /i "!PLANNOTATOR_SKIP_SKILLS_INSTALL!"=="%%V" (
|
|
set "SKIP_SKILLS=1"
|
|
set "SKIP_SKILLS_SOURCE=PLANNOTATOR_SKIP_SKILLS_INSTALL"
|
|
)
|
|
for %%V in (0 false no) do if /i "!PLANNOTATOR_SKIP_SKILLS_INSTALL!"=="%%V" (
|
|
set "SKIP_SKILLS=0"
|
|
set "SKIP_SKILLS_SOURCE="
|
|
)
|
|
if "!SKIP_CODEX_FLAG!"=="1" (
|
|
set "SKIP_CODEX=1"
|
|
set "SKIP_CODEX_SOURCE=--skip-codex"
|
|
)
|
|
if "!SKIP_GEMINI_FLAG!"=="1" (
|
|
set "SKIP_GEMINI=1"
|
|
set "SKIP_GEMINI_SOURCE=--skip-gemini"
|
|
)
|
|
if "!SKIP_KIRO_FLAG!"=="1" (
|
|
set "SKIP_KIRO=1"
|
|
set "SKIP_KIRO_SOURCE=--skip-kiro"
|
|
)
|
|
if "!SKIP_OPENCODE_FLAG!"=="1" (
|
|
set "SKIP_OPENCODE=1"
|
|
set "SKIP_OPENCODE_SOURCE=--skip-opencode"
|
|
)
|
|
if "!SKIP_SKILLS_FLAG!"=="1" (
|
|
set "SKIP_SKILLS=1"
|
|
set "SKIP_SKILLS_SOURCE=--skip-skills"
|
|
)
|
|
|
|
REM Pre-flight: reject verification requests for tags older than the first
|
|
REM attested release BEFORE downloading. Critical security point: the version
|
|
REM comparison uses $env:TAG_NUM / $env:MIN_NUM instead of interpolating
|
|
REM !TAG_NUM! / !MIN_NUM! into the PowerShell command string. Interpolation
|
|
REM would let a crafted --version value break out of the single-quoted literal
|
|
REM and execute arbitrary PowerShell (e.g. --version "0.18.0'; calc; '0.18.0"
|
|
REM would run Calculator). $env: reads the raw string; PowerShell never parses
|
|
REM the value as code. [version] cast throws on invalid input, catch swallows,
|
|
REM VERSION_OK stays empty, and the guard rejects - safe fail.
|
|
if "!VERIFY_ATTESTATION!"=="1" (
|
|
REM Strip the leading `v` via substring-from-index-1. cmd's `:str=repl`
|
|
REM substitution is GLOBAL, not anchored - `!TAG:v=!` would remove every
|
|
REM `v` in the string, not just the leading one, so a hypothetical tag
|
|
REM like `v1.0.0-rev2` would become `1.0.0-re2` and break the [version]
|
|
REM cast. TAG is guaranteed to start with `v` by the normalization step
|
|
REM above, so `:~1` (drop first char) is equivalent to stripping the
|
|
REM leading prefix.
|
|
set "TAG_NUM=!TAG:~1!"
|
|
set "MIN_NUM=!MIN_ATTESTED_VERSION:~1!"
|
|
|
|
REM Detect pre-release / build-metadata tags (e.g. v0.18.0-rc1) BEFORE
|
|
REM handing the value to PowerShell. [System.Version] doesn't support
|
|
REM semver prerelease suffixes and would throw inside the try/catch,
|
|
REM leaving VERSION_OK empty and surfacing a misleading "predates
|
|
REM attestation support" error. install.sh handles these correctly via
|
|
REM `sort -V`; Windows doesn't have a built-in semver comparator, so
|
|
REM we reject explicitly with an accurate diagnosis instead of silently
|
|
REM misclassifying the failure.
|
|
REM
|
|
REM Uses native cmd substitution `!VAR:-=!` to check for `-` presence -
|
|
REM no subshell, no metacharacter risk. If removing `-` changes the
|
|
REM string, the original contained a `-`.
|
|
if not "!TAG_NUM!"=="!TAG_NUM:-=!" (
|
|
echo Pre-release tags like !TAG! aren't currently supported for >&2
|
|
echo provenance verification on Windows. [System.Version] doesn't >&2
|
|
echo parse semver prerelease suffixes. Options: >&2
|
|
echo - Install without provenance verification: --skip-attestation >&2
|
|
echo - Pin to a stable release tag ^(no `-rc`, `-beta`, etc.^) >&2
|
|
exit /b 1
|
|
)
|
|
|
|
set "VERSION_OK="
|
|
for /f "delims=" %%i in ('powershell -NoProfile -Command "try { if ([version]$env:TAG_NUM -ge [version]$env:MIN_NUM) { 'yes' } } catch {}"') do set "VERSION_OK=%%i"
|
|
if not "!VERSION_OK!"=="yes" (
|
|
echo Provenance verification was requested, but !TAG! predates >&2
|
|
echo plannotator's attestation support. The first release carrying >&2
|
|
echo signed build provenance is !MIN_ATTESTED_VERSION!. Options: >&2
|
|
echo - Pin to !MIN_ATTESTED_VERSION! or later: --version !MIN_ATTESTED_VERSION! >&2
|
|
echo - Install without provenance verification: --skip-attestation >&2
|
|
echo - Or unset PLANNOTATOR_VERIFY_ATTESTATION / remove verifyAttestation >&2
|
|
echo from %USERPROFILE%\.plannotator\config.json >&2
|
|
exit /b 1
|
|
)
|
|
)
|
|
|
|
set "BINARY_NAME=plannotator-!PLATFORM!.exe"
|
|
set "BINARY_URL=https://github.com/!REPO!/releases/download/!TAG!/!BINARY_NAME!"
|
|
set "CHECKSUM_URL=!BINARY_URL!.sha256"
|
|
|
|
REM Download binary to a randomized temp path so concurrent invocations
|
|
REM don't collide and a same-user pre-placed symlink at a predictable
|
|
REM path can't redirect where curl writes the downloaded executable.
|
|
REM The SHA256 check would pass regardless (content is authentic), but
|
|
REM the install destination would be corrupted.
|
|
set "TEMP_FILE=%TEMP%\plannotator-%RANDOM%.exe"
|
|
curl -fsSL "!BINARY_URL!" -o "!TEMP_FILE!"
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Failed to download binary >&2
|
|
if exist "!TEMP_FILE!" del "!TEMP_FILE!"
|
|
exit /b 1
|
|
)
|
|
|
|
REM Download checksum to a randomized temp path for the same reason as
|
|
REM the binary download above (concurrent collision + symlink pre-placement).
|
|
set "CHECKSUM_FILE=%TEMP%\plannotator-checksum-%RANDOM%.txt"
|
|
curl -fsSL "!CHECKSUM_URL!" -o "!CHECKSUM_FILE!"
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Failed to download checksum >&2
|
|
REM curl -o creates the output file before receiving data, so a
|
|
REM network failure or HTTP error leaves a 0-byte/partial file
|
|
REM at CHECKSUM_FILE. Clean it up to match the discipline used
|
|
REM for TEMP_FILE elsewhere in this script.
|
|
if exist "!CHECKSUM_FILE!" del "!CHECKSUM_FILE!"
|
|
del "!TEMP_FILE!"
|
|
exit /b 1
|
|
)
|
|
|
|
REM Extract expected checksum (first field)
|
|
set /p EXPECTED_CHECKSUM=<"!CHECKSUM_FILE!"
|
|
for /f "tokens=1" %%i in ("!EXPECTED_CHECKSUM!") do set "EXPECTED_CHECKSUM=%%i"
|
|
del "!CHECKSUM_FILE!"
|
|
|
|
REM Verify checksum using certutil
|
|
set "ACTUAL_CHECKSUM="
|
|
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!TEMP_FILE!" SHA256') do (
|
|
if not defined ACTUAL_CHECKSUM (
|
|
set "ACTUAL_CHECKSUM=%%i"
|
|
set "ACTUAL_CHECKSUM=!ACTUAL_CHECKSUM: =!"
|
|
)
|
|
)
|
|
|
|
if /i "!ACTUAL_CHECKSUM!" neq "!EXPECTED_CHECKSUM!" (
|
|
echo Checksum verification failed >&2
|
|
del "!TEMP_FILE!"
|
|
exit /b 1
|
|
)
|
|
|
|
if "!VERIFY_ATTESTATION!"=="1" (
|
|
REM VERIFY_ATTESTATION was resolved before the download; MIN_ATTESTED_VERSION
|
|
REM pre-flight already ran and rejected older tags. At this point we know
|
|
REM the tag is attested and gh should find a bundle.
|
|
where gh >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
REM Capture combined output to a randomized temp file so gh's
|
|
REM actual error message (auth, network, missing attestation, etc.)
|
|
REM can be surfaced on failure. Randomized to match the existing
|
|
REM %RANDOM% pattern used elsewhere in this script and avoid races
|
|
REM between concurrent invocations. Matches install.sh / install.ps1.
|
|
REM
|
|
REM Verification is constrained to the exact tag (--source-ref) AND
|
|
REM the specific signing workflow file (--signer-workflow) - not
|
|
REM just "built somewhere in this repo". See install.sh for full
|
|
REM rationale.
|
|
REM Credential-free path first (#1178): the attestations endpoint on
|
|
REM api.github.com is world-readable for public repos, so fetch the
|
|
REM Sigstore bundle anonymously and hand it to gh via --bundle. This
|
|
REM drops the gh-login requirement; gh's own authenticated fetch is
|
|
REM only used as a fallback when the public fetch fails. Single fetch
|
|
REM attempt, deliberately never retried: the unauthenticated API
|
|
REM allows 60 requests/hour per IP. The response is
|
|
REM { "attestations": [ { "bundle": {...} } ] }; gh --bundle expects
|
|
REM the bundle values one JSON document per line (the JSONL format
|
|
REM `gh attestation download` writes). PowerShell does the fetch and
|
|
REM the extraction via the ATT_FETCH_B64 -EncodedCommand payload
|
|
REM (documented and drift-guarded where it is defined, near the top
|
|
REM of this file): each bundle is copied as a byte-exact substring of
|
|
REM the response, never a ConvertFrom-Json round trip whose DateTime
|
|
REM coercion could corrupt a bundle field. Values pass via $env: so
|
|
REM nothing is re-parsed as code, and no helper file ever exists on
|
|
REM disk. Exit codes: 0 = bundle written, 2 = fetch failed, 3 = no
|
|
REM bundle extracted.
|
|
set "ATT_BUNDLE_FILE=%TEMP%\plannotator-bundle-%RANDOM%.jsonl"
|
|
set "ATT_DIGEST=!ACTUAL_CHECKSUM!"
|
|
set "ATT_BUNDLE_OK=0"
|
|
REM NOTE: the ATT_FALLBACK_REASON literals below are assigned inside
|
|
REM parenthesized blocks - they must stay free of unescaped cmd
|
|
REM metacharacters, parentheses above all, or the block parse breaks.
|
|
set "ATT_FALLBACK_REASON=Could not fetch the attestation bundle from the public API"
|
|
powershell -NoProfile -EncodedCommand !ATT_FETCH_B64!
|
|
if !ERRORLEVEL! equ 0 (
|
|
if exist "!ATT_BUNDLE_FILE!" set "ATT_BUNDLE_OK=1"
|
|
) else if !ERRORLEVEL! equ 3 (
|
|
set "ATT_FALLBACK_REASON=Could not extract a bundle from the attestations API response"
|
|
)
|
|
set "GH_OUTPUT=%TEMP%\plannotator-gh-%RANDOM%.txt"
|
|
set "ATT_USED_BUNDLE=0"
|
|
if "!ATT_BUNDLE_OK!"=="1" (
|
|
set "ATT_USED_BUNDLE=1"
|
|
gh attestation verify "!TEMP_FILE!" ^
|
|
--bundle "!ATT_BUNDLE_FILE!" ^
|
|
--repo "!REPO!" ^
|
|
--source-ref "refs/tags/!TAG!" ^
|
|
--signer-workflow "backnotprop/plannotator/.github/workflows/release.yml" ^
|
|
> "!GH_OUTPUT!" 2>&1
|
|
if !ERRORLEVEL! neq 0 (
|
|
REM H1: a --bundle failure is not necessarily a provenance
|
|
REM failure - an older gh rejects the flag outright, a corrupt
|
|
REM bundle fails parsing, etc. Retry once through the exact
|
|
REM authenticated path before classifying anything; only the
|
|
REM retry's verdict is reported. A real provenance failure
|
|
REM fails again here, so nothing bad ever slips through.
|
|
echo Bundle-based verification did not complete; retrying via gh's authenticated fetch.
|
|
set "ATT_USED_BUNDLE=0"
|
|
gh attestation verify "!TEMP_FILE!" ^
|
|
--repo "!REPO!" ^
|
|
--source-ref "refs/tags/!TAG!" ^
|
|
--signer-workflow "backnotprop/plannotator/.github/workflows/release.yml" ^
|
|
> "!GH_OUTPUT!" 2>&1
|
|
)
|
|
) else (
|
|
echo !ATT_FALLBACK_REASON!; falling back to gh's authenticated fetch.
|
|
gh attestation verify "!TEMP_FILE!" ^
|
|
--repo "!REPO!" ^
|
|
--source-ref "refs/tags/!TAG!" ^
|
|
--signer-workflow "backnotprop/plannotator/.github/workflows/release.yml" ^
|
|
> "!GH_OUTPUT!" 2>&1
|
|
)
|
|
if !ERRORLEVEL! neq 0 (
|
|
type "!GH_OUTPUT!" >&2
|
|
REM Classify the failure so connectivity problems are never
|
|
REM conflated with a provenance failure. "Sigstore verifiers"
|
|
REM means gh could not initialize the TUF trust root, which is
|
|
REM fetched on EVERY run (never cached); "gh auth login" is only
|
|
REM reachable on the authenticated path. Precedence: TUF wins
|
|
REM over auth (the auth findstr runs first so the TUF assignment
|
|
REM lands last, matching install.sh / install.ps1).
|
|
set "VERIFY_FAIL_KIND=provenance"
|
|
findstr /c:"gh auth login" "!GH_OUTPUT!" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "VERIFY_FAIL_KIND=auth"
|
|
findstr /c:"Sigstore verifiers" "!GH_OUTPUT!" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "VERIFY_FAIL_KIND=tuf"
|
|
del "!GH_OUTPUT!"
|
|
if exist "!ATT_BUNDLE_FILE!" del "!ATT_BUNDLE_FILE!"
|
|
if "!VERIFY_FAIL_KIND!"=="tuf" (
|
|
echo Could not initialize the Sigstore trust root ^(TUF^). Provenance >&2
|
|
echo verification needs network access on every run; the trusted root >&2
|
|
echo is fetched per-run, never cached. This is a connectivity failure, >&2
|
|
echo NOT evidence of a bad binary. Refusing to install unverified; >&2
|
|
echo retry with network access or pass --skip-attestation. >&2
|
|
)
|
|
if "!VERIFY_FAIL_KIND!"=="auth" (
|
|
echo The credential-free bundle path did not complete and gh is not >&2
|
|
echo logged in, so the authenticated fallback could not run. Retry with >&2
|
|
echo network access to api.github.com, run 'gh auth login', or pass >&2
|
|
echo --skip-attestation. >&2
|
|
)
|
|
if "!VERIFY_FAIL_KIND!"=="provenance" (
|
|
echo Attestation verification failed! >&2
|
|
echo The binary's SHA256 matched, but no valid signed provenance was found >&2
|
|
echo for !REPO!. Refusing to install. >&2
|
|
)
|
|
del "!TEMP_FILE!"
|
|
exit /b 1
|
|
)
|
|
del "!GH_OUTPUT!"
|
|
if "!ATT_USED_BUNDLE!"=="1" (
|
|
echo [OK] verified build provenance ^(SLSA, credential-free via the public attestations API^)
|
|
) else (
|
|
echo [OK] verified build provenance ^(SLSA^)
|
|
)
|
|
if exist "!ATT_BUNDLE_FILE!" del "!ATT_BUNDLE_FILE!"
|
|
) else (
|
|
echo verifyAttestation is enabled but gh CLI was not found. >&2
|
|
echo Install https://cli.github.com ^(no login is needed when the public >&2
|
|
echo attestation bundle fetch succeeds^), or unset >&2
|
|
echo PLANNOTATOR_VERIFY_ATTESTATION / remove verifyAttestation >&2
|
|
echo from %USERPROFILE%\.plannotator\config.json / pass --skip-attestation. >&2
|
|
del "!TEMP_FILE!"
|
|
exit /b 1
|
|
)
|
|
) else (
|
|
echo SHA256 verified. For build provenance verification, see
|
|
echo https://docs.plannotator.ai/open-source/start/installation#pin-or-verify-a-release
|
|
)
|
|
|
|
REM Install binary
|
|
set "INSTALL_PATH=!INSTALL_DIR!\plannotator.exe"
|
|
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
|
|
call :InstallCallFlowRuntime
|
|
|
|
call :PrintPathAdvice
|
|
|
|
REM Validate plugin hooks.json if plugin is already installed
|
|
if defined CLAUDE_CONFIG_DIR (
|
|
set "PLUGIN_HOOKS=%CLAUDE_CONFIG_DIR%\plugins\marketplaces\plannotator\apps\hook\hooks\hooks.json"
|
|
) else (
|
|
set "PLUGIN_HOOKS=%USERPROFILE%\.claude\plugins\marketplaces\plannotator\apps\hook\hooks\hooks.json"
|
|
)
|
|
if exist "!PLUGIN_HOOKS!" (
|
|
REM Use full path so the hook works without PATH being set in the shell
|
|
set "EXE_PATH=!INSTALL_PATH:\=/!"
|
|
(
|
|
echo {
|
|
echo "hooks": {
|
|
echo "PreToolUse": [
|
|
echo {
|
|
echo "matcher": "EnterPlanMode",
|
|
echo "hooks": [
|
|
echo {
|
|
echo "type": "command",
|
|
echo "command": "\"!EXE_PATH!\" improve-context",
|
|
echo "timeout": 5
|
|
echo }
|
|
echo ]
|
|
echo }
|
|
echo ],
|
|
echo "PermissionRequest": [
|
|
echo {
|
|
echo "matcher": "ExitPlanMode",
|
|
echo "hooks": [
|
|
echo {
|
|
echo "type": "command",
|
|
echo "command": "\"!EXE_PATH!\"",
|
|
echo "timeout": 345600
|
|
echo }
|
|
echo ]
|
|
echo }
|
|
echo ]
|
|
echo }
|
|
echo }
|
|
) > "!PLUGIN_HOOKS!"
|
|
echo Updated plugin hooks at !PLUGIN_HOOKS!
|
|
)
|
|
|
|
REM Codex hooks on Windows are still experimental upstream. Do not mutate
|
|
REM the Codex home automatically from the cmd installer until that path
|
|
REM is verified end-to-end.
|
|
REM Codex stores config and state under CODEX_HOME when set, falling back to
|
|
REM %%USERPROFILE%%\.codex (developers.openai.com/codex/config-advanced). (#852)
|
|
set "CODEX_DIR=%USERPROFILE%\.codex"
|
|
if defined CODEX_HOME set "CODEX_DIR=%CODEX_HOME%"
|
|
set "CODEX_AVAILABLE=0"
|
|
where codex >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "CODEX_AVAILABLE=1"
|
|
if exist "!CODEX_DIR!" (
|
|
for /f "delims=" %%C in ('dir /b /a "!CODEX_DIR!" 2^>nul') do (
|
|
if /i not "%%C"=="skills" if /i not "%%C"==".DS_Store" set "CODEX_AVAILABLE=1"
|
|
)
|
|
)
|
|
REM Kiro is auto-detected like Codex/Gemini: PATH executable or an existing %USERPROFILE%\.kiro.
|
|
set "KIRO_AVAILABLE=0"
|
|
where kiro-cli >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "KIRO_AVAILABLE=1"
|
|
if exist "%USERPROFILE%\.kiro" set "KIRO_AVAILABLE=1"
|
|
REM HONEST three-state reporting (#1178): detected-but-skipped is its own
|
|
REM state, never conflated with "not detected". A Codex opt-out leaves the
|
|
REM Codex home entirely untouched (no writes, no cleanup, no removal).
|
|
if "!CODEX_AVAILABLE!"=="1" if "!SKIP_CODEX!"=="1" (
|
|
echo.
|
|
echo Codex: detected, skipped ^(!SKIP_CODEX_SOURCE!^).
|
|
echo The Windows installer only prints manual Codex setup instructions; they
|
|
echo were suppressed.
|
|
REM M4: the existing-integration note only fires when hooks.json actually
|
|
REM references plannotator - mere existence proves nothing, since the
|
|
REM file may hold only the user's own hooks.
|
|
if exist "!CODEX_DIR!\hooks.json" (
|
|
findstr /c:"plannotator" "!CODEX_DIR!\hooks.json" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 echo Your existing Codex Stop hook at !CODEX_DIR!\hooks.json is unaffected.
|
|
)
|
|
echo Note: the shared agent skills in %USERPROFILE%\.agents\skills serve multiple
|
|
echo agents ^(Codex among them^) and are still installed.
|
|
)
|
|
if "!CODEX_AVAILABLE!"=="1" if "!SKIP_CODEX!"=="0" (
|
|
echo.
|
|
echo Codex detected.
|
|
echo Codex plan review hooks are experimental on Windows. To try them manually:
|
|
echo.
|
|
echo 1. Add this to !CODEX_DIR!\config.toml:
|
|
echo.
|
|
echo [features]
|
|
echo hooks = true
|
|
echo.
|
|
echo 2. Add a Stop hook in !CODEX_DIR!\hooks.json that runs:
|
|
echo.
|
|
echo !INSTALL_PATH!
|
|
echo.
|
|
)
|
|
|
|
REM Clear any cached OpenCode plugin to force fresh download on next run.
|
|
REM An OpenCode opt-out (#1178) leaves OpenCode's own cache directory alone;
|
|
REM the Bun package cache is a shared cache, not OpenCode's home, and is
|
|
REM always cleared.
|
|
if "!SKIP_OPENCODE!"=="0" (
|
|
if exist "%USERPROFILE%\.cache\opencode\node_modules\@plannotator" rmdir /s /q "%USERPROFILE%\.cache\opencode\node_modules\@plannotator" >nul 2>&1
|
|
if exist "%USERPROFILE%\.cache\opencode\packages\@plannotator" rmdir /s /q "%USERPROFILE%\.cache\opencode\packages\@plannotator" >nul 2>&1
|
|
)
|
|
if exist "%USERPROFILE%\.bun\install\cache\@plannotator" rmdir /s /q "%USERPROFILE%\.bun\install\cache\@plannotator" >nul 2>&1
|
|
|
|
REM ----------------------------------------------------------------------
|
|
REM Skills + command stubs install (requires git)
|
|
REM
|
|
REM Claude Code commands are deprecated in favor of skills. Core skills
|
|
REM installed to %%USERPROFILE%%\.claude\skills are user-invocable by directory
|
|
REM name (/plannotator-review etc.), so no command files are written anymore.
|
|
REM
|
|
REM Install matrix (all copies verbatim, copy-if-present so older-tag pinned
|
|
REM installs never fail when a source dir is absent):
|
|
REM %%USERPROFILE%%\.claude\skills <- apps\skills\core\* (all 4)
|
|
REM %%USERPROFILE%%\.agents\skills <- apps\skills\core\* (all 4)
|
|
REM %%USERPROFILE%%\.kiro\skills <- apps\kiro-cli\skills\* (3) + 2 extras (when kiro detected)
|
|
REM %%USERPROFILE%%\.config\opencode\commands <- apps\opencode-plugin\commands\*.md (always)
|
|
REM %%USERPROFILE%%\.gemini\commands <- apps\gemini\commands\*.toml (when ~/.gemini exists)
|
|
REM Nothing goes to the Codex home (CODEX_DIR\skills) anymore.
|
|
REM ----------------------------------------------------------------------
|
|
|
|
REM Aggressive cleanup on upgrade - echo each removal, ignore missing.
|
|
REM NOTE: legacy Claude command cleanup happens AFTER the skill install below -
|
|
REM a command file is only removed once its replacement skill is on disk, so a
|
|
REM failed or skipped skill install never leaves users with neither.
|
|
if defined CLAUDE_CONFIG_DIR (
|
|
set "CLAUDE_COMMANDS_DIR=%CLAUDE_CONFIG_DIR%\commands"
|
|
) else (
|
|
set "CLAUDE_COMMANDS_DIR=%USERPROFILE%\.claude\commands"
|
|
)
|
|
|
|
REM NOTE: Codex stale-skill cleanup happens AFTER the skill install below -
|
|
REM the core skills are only removed from the Codex home once their
|
|
REM replacement exists in %%USERPROFILE%%\.agents\skills.
|
|
set "STALE_CODEX_SKILLS_DIR=!CODEX_DIR!\skills"
|
|
|
|
REM Old installers (pre core/extra split) ran a wholesale skills copy against
|
|
REM a new-layout tag and could leave junk core/extra directory copies in the
|
|
REM Claude skills scope. Never valid skill names - always safe to remove.
|
|
if defined CLAUDE_CONFIG_DIR (
|
|
set "CLAUDE_SKILLS_SCOPE=%CLAUDE_CONFIG_DIR%\skills"
|
|
) else (
|
|
set "CLAUDE_SKILLS_SCOPE=%USERPROFILE%\.claude\skills"
|
|
)
|
|
for %%J in (core extra) do (
|
|
if exist "!CLAUDE_SKILLS_SCOPE!\%%J" (
|
|
rmdir /s /q "!CLAUDE_SKILLS_SCOPE!\%%J" >nul 2>&1
|
|
echo Removed stale layout directory !CLAUDE_SKILLS_SCOPE!\%%J ^(left by an older installer^)
|
|
)
|
|
)
|
|
|
|
REM Extras are no longer managed in the Claude / shared-agent scopes. Remove
|
|
REM previously default-installed copies ONCE per machine - recorded in the
|
|
REM migrations ledger under the Plannotator data dir - because copies the user
|
|
REM reinstalls via `npx skills add` are byte-identical to ours and can only be
|
|
REM told apart by remembering that this cleanup already ran.
|
|
if defined CLAUDE_CONFIG_DIR (
|
|
set "CLAUDE_SKILLS_DIR=%CLAUDE_CONFIG_DIR%\skills"
|
|
) else (
|
|
set "CLAUDE_SKILLS_DIR=%USERPROFILE%\.claude\skills"
|
|
)
|
|
set "AGENTS_SKILLS_DIR=%USERPROFILE%\.agents\skills"
|
|
set "MIGRATIONS_DIR=!_CONFIG_DIR!\migrations"
|
|
set "EXTRAS_MIGRATION=!MIGRATIONS_DIR!\2026-06-extras-default-install-removed"
|
|
if not exist "!EXTRAS_MIGRATION!" (
|
|
for %%S in (plannotator-compound plannotator-setup-goal plannotator-visual-explainer) do (
|
|
if exist "!CLAUDE_SKILLS_DIR!\%%S" (
|
|
rmdir /s /q "!CLAUDE_SKILLS_DIR!\%%S" >nul 2>&1
|
|
echo Removed extra Plannotator skill from !CLAUDE_SKILLS_DIR!\%%S ^(reinstall via npx skills add^)
|
|
)
|
|
if exist "!AGENTS_SKILLS_DIR!\%%S" (
|
|
rmdir /s /q "!AGENTS_SKILLS_DIR!\%%S" >nul 2>&1
|
|
echo Removed extra Plannotator skill from !AGENTS_SKILLS_DIR!\%%S ^(reinstall via npx skills add^)
|
|
)
|
|
)
|
|
if not exist "!MIGRATIONS_DIR!" mkdir "!MIGRATIONS_DIR!" >nul 2>&1
|
|
type nul > "!EXTRAS_MIGRATION!"
|
|
)
|
|
|
|
REM --- Guided install (interactive consoles only) ---
|
|
REM Mirrors install.sh: two questions (extras? model-invocable skills?),
|
|
REM answers persisted to install-prefs in the Plannotator data dir and reused
|
|
REM silently on re-runs. --reconfigure re-opens the wizard; --non-interactive
|
|
REM forces silence. `set /p` returns empty at EOF, so redirected/CI runs fall
|
|
REM through to the defaults without hanging. Flags win over everything.
|
|
REM No checkbox UI in batch - the skill picker uses numbered toggles instead.
|
|
set "PREFS_FILE=!_CONFIG_DIR!\install-prefs"
|
|
set "SAVED_EXTRAS="
|
|
set "SAVED_INVOCABLE="
|
|
if exist "!PREFS_FILE!" (
|
|
for /f "usebackq tokens=1,* delims==" %%A in ("!PREFS_FILE!") do (
|
|
if /i "%%A"=="extras" set "SAVED_EXTRAS=%%B"
|
|
if /i "%%A"=="model_invocable" set "SAVED_INVOCABLE=%%B"
|
|
)
|
|
)
|
|
|
|
REM Extras already on disk? Then the extras question is moot - they still
|
|
REM count toward the picker list, and we never launch the npx flow over them.
|
|
set "EXTRAS_PRESENT=0"
|
|
for %%S in (plannotator-compound plannotator-setup-goal plannotator-visual-explainer) do (
|
|
if exist "!CLAUDE_SKILLS_DIR!\%%S" set "EXTRAS_PRESENT=1"
|
|
if exist "!AGENTS_SKILLS_DIR!\%%S" set "EXTRAS_PRESENT=1"
|
|
)
|
|
|
|
REM A wizard needs a real console. `timeout` exits non-zero when stdin is
|
|
REM redirected ("Input redirection is not supported"), making it a reliable
|
|
REM console probe - CI and redirected runs never see the wizard and never
|
|
REM trigger the wizard-only installs (npx extras). The set /p
|
|
REM EOF-fallthrough remains as a second line of defense.
|
|
set "CAN_PROMPT=0"
|
|
timeout /t 0 >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 set "CAN_PROMPT=1"
|
|
if "!NON_INTERACTIVE!"=="1" set "CAN_PROMPT=0"
|
|
set "RUN_WIZARD=0"
|
|
if "!CAN_PROMPT!"=="1" (
|
|
if "!RECONFIGURE!"=="1" set "RUN_WIZARD=1"
|
|
if not exist "!PREFS_FILE!" set "RUN_WIZARD=1"
|
|
)
|
|
|
|
set "EXTRAS_CHOICE="
|
|
set "INVOCABLE_CHOICE="
|
|
if "!RUN_WIZARD!"=="1" call :guided_wizard
|
|
|
|
REM Flags override the wizard and saved answers; otherwise saved, then defaults.
|
|
if defined EXTRAS_FLAG set "EXTRAS_CHOICE=!EXTRAS_FLAG!"
|
|
if defined MODEL_INVOCABLE_FLAG set "INVOCABLE_CHOICE=!MODEL_INVOCABLE_FLAG!"
|
|
if not defined EXTRAS_CHOICE (
|
|
if defined SAVED_EXTRAS (set "EXTRAS_CHOICE=!SAVED_EXTRAS!") else (set "EXTRAS_CHOICE=no")
|
|
)
|
|
if not defined INVOCABLE_CHOICE (
|
|
if defined SAVED_INVOCABLE (set "INVOCABLE_CHOICE=!SAVED_INVOCABLE!") else (set "INVOCABLE_CHOICE=none")
|
|
)
|
|
|
|
REM Persist only when the wizard ran or a flag set something - silent re-runs
|
|
REM must not clobber saved answers with defaults.
|
|
set "DO_PERSIST=0"
|
|
if "!RUN_WIZARD!"=="1" set "DO_PERSIST=1"
|
|
if defined EXTRAS_FLAG set "DO_PERSIST=1"
|
|
if defined MODEL_INVOCABLE_FLAG set "DO_PERSIST=1"
|
|
if "!DO_PERSIST!"=="1" (
|
|
if not exist "!_CONFIG_DIR!" mkdir "!_CONFIG_DIR!" >nul 2>&1
|
|
> "!PREFS_FILE!" (
|
|
echo extras=!EXTRAS_CHOICE!
|
|
echo model_invocable=!INVOCABLE_CHOICE!
|
|
)
|
|
)
|
|
|
|
REM Extras install is delegated to the skills CLI (its UI picks the agents).
|
|
REM Interactive wizard runs only - silent runs and CI get the printed command.
|
|
REM Never runs when the extras already exist. The extras ARE skills, so
|
|
REM --skip-skills suppresses them too - a saved extras=yes preference must not
|
|
REM smuggle a skill install past the opt-out.
|
|
if "!SKIP_SKILLS!"=="0" if "!EXTRAS_CHOICE!"=="yes" if "!EXTRAS_PRESENT!"=="0" (
|
|
set "NPX_OK=0"
|
|
where npx >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 if "!RUN_WIZARD!"=="1" set "NPX_OK=1"
|
|
if "!NPX_OK!"=="1" (
|
|
echo Launching the skills CLI for the extras ^(pick your agents in its UI^)...
|
|
call npx skills add backnotprop/plannotator/apps/skills/extra --global
|
|
if not !ERRORLEVEL! equ 0 echo skills CLI did not complete - install later with: npx skills add backnotprop/plannotator/apps/skills/extra --global
|
|
) else (
|
|
echo Install the extras with: npx skills add backnotprop/plannotator/apps/skills/extra --global
|
|
)
|
|
)
|
|
|
|
REM File-copy installs require git (sparse checkout). Hard requirement: without
|
|
REM git we cannot install the /plannotator-* skills, so fail loudly instead of
|
|
REM leaving a partial install. Hook/config writing above has already run; the
|
|
REM Pi update and Gemini config below are skipped on failure and complete when
|
|
REM the user re-runs the installer.
|
|
REM
|
|
REM Skills/commands opt-out (--skip-skills / PLANNOTATOR_SKIP_SKILLS_INSTALL /
|
|
REM skipInstall.skills). HONEST reporting like the per-agent family: the skipped
|
|
REM state is announced, and skip means do-not-write - nothing already on disk in
|
|
REM any skill or command scope is fetched, replaced, or removed on this run.
|
|
REM Nothing is fetched, so git also stops being a requirement here.
|
|
if "!SKIP_SKILLS!"=="1" (
|
|
echo.
|
|
echo Skills: skipped ^(!SKIP_SKILLS_SOURCE!^).
|
|
echo No skills or slash commands were fetched, and none already installed
|
|
echo were changed or removed. The /plannotator-* commands are NOT installed
|
|
echo by this run - re-run without the opt-out to install them.
|
|
) else (
|
|
where git >nul 2>&1
|
|
if not !ERRORLEVEL! equ 0 (
|
|
echo Error: git is required to install Plannotator's skills and slash commands. 1>&2
|
|
echo Install git, then run this installer again. 1>&2
|
|
echo To install without them, re-run with --skip-skills. 1>&2
|
|
exit /b 1
|
|
)
|
|
)
|
|
set "CHECKOUT_FAILED=0"
|
|
set "KIRO_SKILLS_DIR=%USERPROFILE%\.kiro\skills"
|
|
set "KIRO_AGENTS_DIR=%USERPROFILE%\.kiro\agents"
|
|
set "OPENCODE_COMMANDS_DIR=%USERPROFILE%\.config\opencode\commands"
|
|
set "GEMINI_COMMANDS_DIR=%USERPROFILE%\.gemini\commands"
|
|
set "SKILLS_TMP=%TEMP%\plannotator-skills-%RANDOM%"
|
|
REM git's stderr is captured OUTSIDE SKILLS_TMP (which is removed before the
|
|
REM failure message prints) so a failed clone can show the real git error
|
|
REM (#1238) instead of only the generic "network or git error" line.
|
|
set "GIT_ERR_FILE=%TEMP%\plannotator-git-stderr-%RANDOM%.txt"
|
|
mkdir "!SKILLS_TMP!" >nul 2>&1
|
|
|
|
REM Opt-out: jump past the clone so no network call is made and
|
|
REM CHECKOUT_FAILED stays 0 - an opt-out is not a fetch failure and must not
|
|
REM trip the guard below. Reported above, next to the git check.
|
|
if "!SKIP_SKILLS!"=="1" goto skills_checkout_done
|
|
|
|
set "CLONE_OK=0"
|
|
set "SPARSE_CLONE=1"
|
|
REM LC_ALL=C pins git's error strings to English for the capability probe
|
|
REM below: a localized git would emit a translated "unknown option" message
|
|
REM the findstr match misses, sending old-git non-English users to a hard
|
|
REM failure instead of the fallback. Saved and restored around the probe.
|
|
set "PLANNOTATOR_SAVED_LC_ALL=!LC_ALL!"
|
|
set "LC_ALL=C"
|
|
git clone --depth 1 --filter=blob:none --sparse "https://github.com/!REPO!.git" --branch "!TAG!" "!SKILLS_TMP!\repo" >nul 2>"!GIT_ERR_FILE!"
|
|
if !ERRORLEVEL! equ 0 set "CLONE_OK=1"
|
|
set "LC_ALL=!PLANNOTATOR_SAVED_LC_ALL!"
|
|
set "PLANNOTATOR_SAVED_LC_ALL="
|
|
|
|
REM Capability probe, not a version parse (same philosophy as the GitButler
|
|
REM flag probing in packages/shared/gitbutler-core.ts): `git clone --sparse`
|
|
REM needs git >= 2.25, and an older git rejects the flag instantly with
|
|
REM "error: unknown option `sparse'" before any network call (#1238). Fall
|
|
REM back to a plain shallow clone - it costs download size, not correctness:
|
|
REM every path the copy steps below read is present in the full checkout, and
|
|
REM `git sparse-checkout set` (equally missing on that git) is skipped
|
|
REM because there is nothing to narrow.
|
|
set "SPARSE_UNSUPPORTED=0"
|
|
if "!CLONE_OK!"=="0" (
|
|
findstr /i /c:"unknown option" "!GIT_ERR_FILE!" >nul 2>&1 && findstr /i /c:"sparse" "!GIT_ERR_FILE!" >nul 2>&1 && set "SPARSE_UNSUPPORTED=1"
|
|
)
|
|
if "!SPARSE_UNSUPPORTED!"=="1" (
|
|
echo This git does not support "git clone --sparse" ^(needs git ^>= 2.25^) - falling back to a plain shallow clone.
|
|
set "SPARSE_CLONE=0"
|
|
if exist "!SKILLS_TMP!\repo" rmdir /s /q "!SKILLS_TMP!\repo" >nul 2>&1
|
|
git clone --depth 1 "https://github.com/!REPO!.git" --branch "!TAG!" "!SKILLS_TMP!\repo" >nul 2>"!GIT_ERR_FILE!"
|
|
if !ERRORLEVEL! equ 0 set "CLONE_OK=1"
|
|
)
|
|
|
|
if "!CLONE_OK!"=="1" (
|
|
pushd "!SKILLS_TMP!\repo"
|
|
if "!SPARSE_CLONE!"=="1" git sparse-checkout set apps/skills apps/kiro-cli apps/opencode-plugin/commands apps/gemini/commands >nul 2>&1
|
|
|
|
REM Claude Code reads apps\skills\claude\* (injection `!`plannotator ... $ARGUMENTS``
|
|
REM + allowed-tools, so /plannotator-* run with no permission prompt); Codex
|
|
REM reads apps\skills\core\* (prose). The `!`...`` injection is Claude-Code-only,
|
|
REM so the two are sourced separately. Replace rather than merge on each run.
|
|
if exist "apps\skills\claude" (
|
|
if not exist "!CLAUDE_SKILLS_DIR!" mkdir "!CLAUDE_SKILLS_DIR!"
|
|
for %%S in (plannotator-review plannotator-annotate plannotator-last) do (
|
|
if exist "apps\skills\claude\%%S" (
|
|
if exist "!CLAUDE_SKILLS_DIR!\%%S" rmdir /s /q "!CLAUDE_SKILLS_DIR!\%%S" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\claude\%%S" "!CLAUDE_SKILLS_DIR!\%%S\" >nul 2>&1
|
|
)
|
|
)
|
|
REM The plannotator knowledge skill (CLI reference) has no Claude-only
|
|
REM injection form, so Claude installs the single-sourced core copy.
|
|
if exist "apps\skills\core\plannotator" (
|
|
if exist "!CLAUDE_SKILLS_DIR!\plannotator" rmdir /s /q "!CLAUDE_SKILLS_DIR!\plannotator" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\core\plannotator" "!CLAUDE_SKILLS_DIR!\plannotator\" >nul 2>&1
|
|
)
|
|
echo Installed Claude Code skills to !CLAUDE_SKILLS_DIR!\
|
|
)
|
|
if exist "apps\skills\core" (
|
|
if not exist "!AGENTS_SKILLS_DIR!" mkdir "!AGENTS_SKILLS_DIR!"
|
|
for %%S in (plannotator-review plannotator-annotate plannotator-last plannotator) do (
|
|
if exist "apps\skills\core\%%S" (
|
|
REM Replace rather than merge so files removed upstream don't linger.
|
|
if exist "!AGENTS_SKILLS_DIR!\%%S" rmdir /s /q "!AGENTS_SKILLS_DIR!\%%S" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\core\%%S" "!AGENTS_SKILLS_DIR!\%%S\" >nul 2>&1
|
|
)
|
|
)
|
|
echo Installed shared agent skills to !AGENTS_SKILLS_DIR!\
|
|
) else (
|
|
echo Tag !TAG! predates the core/extra skill layout - skipping core skill install
|
|
)
|
|
|
|
REM OpenCode command stubs -> always (plugin intercepts execution),
|
|
REM unless opted out via --skip-opencode (#1178).
|
|
if "!SKIP_OPENCODE!"=="0" if exist "apps\opencode-plugin\commands" (
|
|
if not exist "!OPENCODE_COMMANDS_DIR!" mkdir "!OPENCODE_COMMANDS_DIR!"
|
|
xcopy /y /q "apps\opencode-plugin\commands\*.md" "!OPENCODE_COMMANDS_DIR!\" >nul 2>&1
|
|
echo Installed OpenCode commands to !OPENCODE_COMMANDS_DIR!\
|
|
)
|
|
|
|
REM Gemini TOML commands -> only when ~/.gemini exists (Gemini's native
|
|
REM format) and not opted out (#1178).
|
|
if exist "%USERPROFILE%\.gemini" if "!SKIP_GEMINI!"=="0" if exist "apps\gemini\commands" (
|
|
if not exist "!GEMINI_COMMANDS_DIR!" mkdir "!GEMINI_COMMANDS_DIR!"
|
|
xcopy /y /q "apps\gemini\commands\*.toml" "!GEMINI_COMMANDS_DIR!\" >nul 2>&1
|
|
echo Installed Gemini commands to !GEMINI_COMMANDS_DIR!\
|
|
)
|
|
|
|
REM Kiro -> hand-maintained kiro skills (3) + 2 extras, only when detected
|
|
REM and not opted out (#1178: a Kiro opt-out leaves ~/.kiro untouched).
|
|
if "!KIRO_AVAILABLE!"=="1" if "!SKIP_KIRO!"=="0" if exist "apps\kiro-cli\skills" (
|
|
if not exist "!KIRO_SKILLS_DIR!" mkdir "!KIRO_SKILLS_DIR!"
|
|
REM Kiro-specific skills with origin baked in come from apps\kiro-cli\skills.
|
|
for %%S in (plannotator-review plannotator-annotate) do (
|
|
if exist "apps\kiro-cli\skills\%%S" (
|
|
if exist "!KIRO_SKILLS_DIR!\%%S" rmdir /s /q "!KIRO_SKILLS_DIR!\%%S" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\kiro-cli\skills\%%S" "!KIRO_SKILLS_DIR!\%%S\" >nul 2>&1
|
|
)
|
|
)
|
|
REM The plannotator knowledge skill (CLI reference) has no Kiro-specific
|
|
REM form, so Kiro receives the single-sourced core copy like every other scope.
|
|
if exist "apps\skills\core\plannotator" (
|
|
if exist "!KIRO_SKILLS_DIR!\plannotator" rmdir /s /q "!KIRO_SKILLS_DIR!\plannotator" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\core\plannotator" "!KIRO_SKILLS_DIR!\plannotator\" >nul 2>&1
|
|
)
|
|
REM The two extras Kiro keeps receiving come from apps\skills\extra.
|
|
if exist "apps\skills\extra\plannotator-setup-goal" (
|
|
if exist "!KIRO_SKILLS_DIR!\plannotator-setup-goal" rmdir /s /q "!KIRO_SKILLS_DIR!\plannotator-setup-goal" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\extra\plannotator-setup-goal" "!KIRO_SKILLS_DIR!\plannotator-setup-goal\" >nul 2>&1
|
|
)
|
|
if exist "apps\skills\extra\plannotator-visual-explainer" (
|
|
if exist "!KIRO_SKILLS_DIR!\plannotator-visual-explainer" rmdir /s /q "!KIRO_SKILLS_DIR!\plannotator-visual-explainer" >nul 2>&1
|
|
xcopy /s /i /y /q "apps\skills\extra\plannotator-visual-explainer" "!KIRO_SKILLS_DIR!\plannotator-visual-explainer\" >nul 2>&1
|
|
)
|
|
REM Plannotator custom agent - don't clobber a user's existing one.
|
|
if not exist "!KIRO_AGENTS_DIR!\plannotator.json" if exist "apps\kiro-cli\agents\plannotator.json" (
|
|
if not exist "!KIRO_AGENTS_DIR!" mkdir "!KIRO_AGENTS_DIR!"
|
|
copy /y "apps\kiro-cli\agents\plannotator.json" "!KIRO_AGENTS_DIR!\plannotator.json" >nul 2>&1
|
|
)
|
|
echo Installed Kiro skills to !KIRO_SKILLS_DIR!\ and agent to !KIRO_AGENTS_DIR!\plannotator.json
|
|
)
|
|
|
|
popd
|
|
) else (
|
|
set "CHECKOUT_FAILED=1"
|
|
)
|
|
|
|
:skills_checkout_done
|
|
rmdir /s /q "!SKILLS_TMP!" >nul 2>&1
|
|
|
|
if "!CHECKOUT_FAILED!"=="1" (
|
|
echo Error: unable to fetch !REPO! at !TAG! ^(network or git error^). 1>&2
|
|
if exist "!GIT_ERR_FILE!" (
|
|
echo git reported: 1>&2
|
|
type "!GIT_ERR_FILE!" 1>&2
|
|
del /q "!GIT_ERR_FILE!" >nul 2>&1
|
|
)
|
|
echo Something went wrong - run the installer again. 1>&2
|
|
exit /b 1
|
|
)
|
|
del /q "!GIT_ERR_FILE!" >nul 2>&1
|
|
|
|
REM Claude Code commands are deprecated in favor of skills. Remove a legacy
|
|
REM command file only once its replacement skill is actually on disk - running
|
|
REM AFTER the install above guarantees a failed or skipped skill install never
|
|
REM leaves users with neither the command nor the skill.
|
|
REM A skills opt-out installed no replacement this run, so it removes nothing
|
|
REM either - skip means do-not-write, never remove.
|
|
for %%C in (plannotator-review plannotator-annotate plannotator-last) do (
|
|
if "!SKIP_SKILLS!"=="0" if exist "!CLAUDE_SKILLS_DIR!\%%C" if exist "!CLAUDE_COMMANDS_DIR!\%%C.md" (
|
|
del /q "!CLAUDE_COMMANDS_DIR!\%%C.md" >nul 2>&1
|
|
echo Removed deprecated Claude command !CLAUDE_COMMANDS_DIR!\%%C.md ^(replaced by the %%C skill^)
|
|
)
|
|
)
|
|
|
|
REM plannotator-archive no longer ships as a skill. Remove any stale installed
|
|
REM copy from every skill scope so upgraders don't keep a dead skill around.
|
|
for %%D in ("!CLAUDE_SKILLS_DIR!" "!AGENTS_SKILLS_DIR!" "!KIRO_SKILLS_DIR!") do (
|
|
REM A Kiro opt-out leaves ~/.kiro entirely untouched - including this sweep.
|
|
REM A skills opt-out leaves every skill scope untouched, sweep included.
|
|
set "SCOPE_OK=1"
|
|
if "!SKIP_SKILLS!"=="1" set "SCOPE_OK=0"
|
|
if /i "%%~D"=="!KIRO_SKILLS_DIR!" if "!SKIP_KIRO!"=="1" set "SCOPE_OK=0"
|
|
if "!SCOPE_OK!"=="1" if exist "%%~D\plannotator-archive" (
|
|
rmdir /s /q "%%~D\plannotator-archive" >nul 2>&1
|
|
echo Removed stale plannotator-archive skill from %%~D\plannotator-archive
|
|
)
|
|
)
|
|
|
|
REM The /plannotator-archive OpenCode command was removed too - sweep the stub.
|
|
REM An OpenCode opt-out suspends the sweep: skip means do-not-write, never remove.
|
|
REM A skills opt-out suspends it for the same reason.
|
|
if "!SKIP_OPENCODE!"=="0" if "!SKIP_SKILLS!"=="0" if exist "!OPENCODE_COMMANDS_DIR!\plannotator-archive.md" (
|
|
del /q "!OPENCODE_COMMANDS_DIR!\plannotator-archive.md" >nul 2>&1
|
|
echo Removed stale plannotator-archive command from !OPENCODE_COMMANDS_DIR!
|
|
)
|
|
|
|
REM Codex no longer hosts core skills (they live in %%USERPROFILE%%\.agents\skills).
|
|
REM Core skills are removed only once their replacement exists; the stale
|
|
REM shared-agent extras were never Codex's and are removed unconditionally.
|
|
for %%S in (plannotator-review plannotator-annotate plannotator-last plannotator-compound plannotator-setup-goal) do (
|
|
REM A Codex opt-out leaves the Codex home entirely untouched - including
|
|
REM this stale-skill cleanup. Skip means do-not-write, never remove. A
|
|
REM skills opt-out installed no replacement, so it suspends the sweep too.
|
|
if "!SKIP_CODEX!"=="0" if "!SKIP_SKILLS!"=="0" if exist "!STALE_CODEX_SKILLS_DIR!\%%S" (
|
|
set "OK_REMOVE=1"
|
|
if "%%S"=="plannotator-review" if not exist "!AGENTS_SKILLS_DIR!\%%S" set "OK_REMOVE=0"
|
|
if "%%S"=="plannotator-annotate" if not exist "!AGENTS_SKILLS_DIR!\%%S" set "OK_REMOVE=0"
|
|
if "%%S"=="plannotator-last" if not exist "!AGENTS_SKILLS_DIR!\%%S" set "OK_REMOVE=0"
|
|
if "!OK_REMOVE!"=="1" (
|
|
rmdir /s /q "!STALE_CODEX_SKILLS_DIR!\%%S" >nul 2>&1
|
|
echo Removed Plannotator skill from !STALE_CODEX_SKILLS_DIR!\%%S
|
|
)
|
|
)
|
|
)
|
|
|
|
REM Apply the saved model-invocation choices. Installed skill copies always
|
|
REM arrive locked (disable-model-invocation: true in SKILL.md); for each
|
|
REM chosen skill we unlock the INSTALLED copy by removing that line, and flip
|
|
REM the Codex sidecar's allow_implicit_invocation to match. Re-applied on
|
|
REM every run because installs replace the skill folders wholesale.
|
|
REM A skills opt-out installed no skill copies this run, so there is nothing to
|
|
REM unlock - and rewriting a PREVIOUS run's SKILL.md would be a write the
|
|
REM opt-out promised not to make.
|
|
if "!SKIP_SKILLS!"=="0" if defined INVOCABLE_CHOICE if not "!INVOCABLE_CHOICE!"=="none" (
|
|
for %%K in ("!INVOCABLE_CHOICE:,=" "!") do (
|
|
for %%D in ("!CLAUDE_SKILLS_DIR!" "!AGENTS_SKILLS_DIR!") do (
|
|
if exist "%%~D\%%~K\SKILL.md" (
|
|
findstr /c:"disable-model-invocation: true" "%%~D\%%~K\SKILL.md" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
findstr /v /c:"disable-model-invocation: true" "%%~D\%%~K\SKILL.md" > "%%~D\%%~K\SKILL.md.tmp"
|
|
move /y "%%~D\%%~K\SKILL.md.tmp" "%%~D\%%~K\SKILL.md" >nul 2>&1
|
|
echo Enabled model invocation: %%~D\%%~K
|
|
)
|
|
)
|
|
if exist "%%~D\%%~K\agents\openai.yaml" (
|
|
findstr /c:"allow_implicit_invocation: false" "%%~D\%%~K\agents\openai.yaml" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
powershell -NoProfile -Command "(Get-Content '%%~D\%%~K\agents\openai.yaml' -Raw) -replace 'allow_implicit_invocation: false','allow_implicit_invocation: true' | Set-Content '%%~D\%%~K\agents\openai.yaml' -NoNewline"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
REM Update Pi extension if pi is installed. Pi keeps its 6 extension commands
|
|
REM and the plannotator_submit_plan tool; it no longer bundles skills, so there
|
|
REM is no settings.json package-skills filter to configure.
|
|
where pi >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
echo Updating Pi extension...
|
|
pi install npm:@plannotator/pi-extension
|
|
if !ERRORLEVEL! equ 0 (
|
|
echo Pi extension updated.
|
|
) else (
|
|
echo Skipping Pi update ^(pi install failed^)
|
|
)
|
|
)
|
|
|
|
REM --- Gemini CLI support (only if Gemini is installed) ---
|
|
REM HONEST three-state reporting (#1178): detected-but-skipped is its own
|
|
REM state. A Gemini opt-out leaves ~/.gemini entirely untouched.
|
|
if exist "%USERPROFILE%\.gemini" if "!SKIP_GEMINI!"=="1" (
|
|
echo.
|
|
echo Gemini: detected, skipped ^(!SKIP_GEMINI_SOURCE!^).
|
|
if exist "%USERPROFILE%\.gemini\settings.json" (
|
|
findstr /c:"plannotator" "%USERPROFILE%\.gemini\settings.json" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 echo An existing Gemini integration at %USERPROFILE%\.gemini\settings.json was left untouched.
|
|
)
|
|
)
|
|
if exist "%USERPROFILE%\.gemini" if "!SKIP_GEMINI!"=="0" (
|
|
REM Install policy file
|
|
if not exist "%USERPROFILE%\.gemini\policies" mkdir "%USERPROFILE%\.gemini\policies"
|
|
(
|
|
echo # Plannotator policy for Gemini CLI
|
|
echo # Allows exit_plan_mode without TUI confirmation so the browser UI is the sole gate.
|
|
echo [[rule]]
|
|
echo toolName = "exit_plan_mode"
|
|
echo decision = "allow"
|
|
echo priority = 100
|
|
) > "%USERPROFILE%\.gemini\policies\plannotator.toml"
|
|
echo Installed Gemini policy to %USERPROFILE%\.gemini\policies\plannotator.toml
|
|
|
|
REM Configure hook in settings.json
|
|
if not exist "%USERPROFILE%\.gemini\settings.json" (
|
|
(
|
|
echo {
|
|
echo "hooks": {
|
|
echo "BeforeTool": [
|
|
echo {
|
|
echo "matcher": "exit_plan_mode",
|
|
echo "hooks": [
|
|
echo {
|
|
echo "type": "command",
|
|
echo "command": "plannotator",
|
|
echo "timeout": 345600
|
|
echo }
|
|
echo ]
|
|
echo }
|
|
echo ]
|
|
echo },
|
|
echo "experimental": {
|
|
echo "plan": true
|
|
echo }
|
|
echo }
|
|
) > "%USERPROFILE%\.gemini\settings.json"
|
|
echo Created Gemini settings at %USERPROFILE%\.gemini\settings.json
|
|
) else (
|
|
findstr /c:"plannotator" "%USERPROFILE%\.gemini\settings.json" >nul 2>&1
|
|
if !ERRORLEVEL! neq 0 (
|
|
REM Merge hook into existing settings.json using node (ships with Gemini CLI)
|
|
where node >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
set "GEMINI_SETTINGS_PATH=%USERPROFILE%\.gemini\settings.json"
|
|
set "GEMINI_SETTINGS_FWD=!GEMINI_SETTINGS_PATH:\=/!"
|
|
node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('!GEMINI_SETTINGS_FWD!','utf8'));s.hooks=s.hooks||{};s.hooks.BeforeTool=s.hooks.BeforeTool||[];s.hooks.BeforeTool.push({matcher:'exit_plan_mode',hooks:[{type:'command',command:'plannotator',timeout:345600}]});fs.writeFileSync('!GEMINI_SETTINGS_FWD!',JSON.stringify(s,null,2)+'\n');"
|
|
echo Added plannotator hook to !GEMINI_SETTINGS_PATH!
|
|
) else (
|
|
echo.
|
|
echo Add the following to your ~/.gemini/settings.json hooks:
|
|
echo.
|
|
echo "hooks": {
|
|
echo "BeforeTool": [{
|
|
echo "matcher": "exit_plan_mode",
|
|
echo "hooks": [{"type": "command", "command": "plannotator", "timeout": 345600}]
|
|
echo }]
|
|
echo }
|
|
)
|
|
)
|
|
)
|
|
|
|
REM Gemini slash commands (plannotator-*.toml) are copied from the sparse
|
|
REM checkout in the git-gated skills/commands block above, not written here.
|
|
)
|
|
|
|
if "!SKIP_OPENCODE!"=="1" (
|
|
echo.
|
|
echo OpenCode: integration skipped ^(!SKIP_OPENCODE_SOURCE!^).
|
|
echo No command stubs were written and OpenCode's plugin cache was left alone.
|
|
echo Re-run without the opt-out to install the command stubs.
|
|
)
|
|
|
|
echo.
|
|
echo ==========================================
|
|
echo KIRO CLI USERS
|
|
echo ==========================================
|
|
echo.
|
|
if "!KIRO_AVAILABLE!"=="1" (
|
|
if "!SKIP_KIRO!"=="1" (
|
|
echo Kiro was detected, but the integration was skipped ^(!SKIP_KIRO_SOURCE!^).
|
|
echo No files under %USERPROFILE%\.kiro were written or removed. Re-run
|
|
echo without the opt-out to add Kiro skills.
|
|
) else if "!SKIP_SKILLS!"=="1" (
|
|
echo Kiro was detected, but skills were skipped ^(!SKIP_SKILLS_SOURCE!^), so no
|
|
echo Kiro skills or agent were installed. Re-run without the opt-out to add them.
|
|
) else (
|
|
echo Kiro skills are installed to %USERPROFILE%\.kiro\skills\
|
|
echo The Plannotator agent is installed to %USERPROFILE%\.kiro\agents\plannotator.json
|
|
echo Launch it: kiro-cli chat --agent plannotator
|
|
)
|
|
) else (
|
|
echo Kiro was not detected. After installing Kiro, rerun this installer to add Kiro skills.
|
|
)
|
|
|
|
echo.
|
|
echo Test the install:
|
|
echo echo {"tool_input":{"plan":"# Test Plan\\n\\nHello world"}} ^| plannotator
|
|
echo.
|
|
echo Then install the Claude Code plugin:
|
|
echo /plugin marketplace add backnotprop/plannotator
|
|
echo /plugin install plannotator@plannotator
|
|
echo.
|
|
echo Upgrading from an older version? Also run /plugin marketplace update
|
|
echo so the plugin drops its old plannotator:* command entries.
|
|
echo.
|
|
REM Never claim the /plannotator-* skills are ready when nothing was installed -
|
|
REM that false banner is exactly what the skills-checkout guard exists to prevent.
|
|
if "!SKIP_SKILLS!"=="1" (
|
|
echo Skills were skipped ^(!SKIP_SKILLS_SOURCE!^), so the /plannotator-review,
|
|
echo /plannotator-annotate, and /plannotator-last skills are NOT installed.
|
|
echo Re-run the installer without the opt-out to add them.
|
|
) else (
|
|
echo The /plannotator-review, /plannotator-annotate, and /plannotator-last skills are ready to use!
|
|
)
|
|
if "!SKIP_SKILLS!"=="0" if not "!EXTRAS_CHOICE!"=="yes" (
|
|
echo.
|
|
echo Optional skills ^(compound planning, setup-goal, visual explainer^):
|
|
echo npx skills add backnotprop/plannotator/apps/skills/extra --global
|
|
)
|
|
|
|
REM Warn if plannotator is configured in both settings.json hooks AND the plugin (causes double execution)
|
|
REM Only warn when the plugin is installed - manual-only users won't have overlap
|
|
if defined CLAUDE_CONFIG_DIR (
|
|
set "CLAUDE_SETTINGS=%CLAUDE_CONFIG_DIR%\settings.json"
|
|
) else (
|
|
set "CLAUDE_SETTINGS=%USERPROFILE%\.claude\settings.json"
|
|
)
|
|
if exist "!PLUGIN_HOOKS!" if exist "!CLAUDE_SETTINGS!" (
|
|
findstr /r /c:"\"command\".*plannotator" "!CLAUDE_SETTINGS!" >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
echo.
|
|
echo WARNING: DUPLICATE HOOK DETECTED
|
|
echo.
|
|
echo plannotator was found in your settings.json hooks:
|
|
echo !CLAUDE_SETTINGS!
|
|
echo.
|
|
echo This will cause plannotator to run TWICE on each plan review.
|
|
echo Remove the plannotator hook from settings.json and rely on the
|
|
echo plugin instead ^(installed automatically via marketplace^).
|
|
echo.
|
|
)
|
|
)
|
|
|
|
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!
|
|
)
|
|
echo.
|
|
echo To uninstall later: plannotator uninstall
|
|
goto :eof
|
|
|
|
REM ======================================================================
|
|
REM Optional annotate agent terminal runtime install. Non-fatal: Plannotator
|
|
REM remains installed if Node/npm or npm install is unavailable.
|
|
REM ======================================================================
|
|
:InstallAgentTerminalRuntime
|
|
if /i "!PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL!"=="1" (
|
|
echo Skipping agent terminal runtime install ^(PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
if /i "!PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL!"=="true" (
|
|
echo Skipping agent terminal runtime install ^(PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
if /i "!PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL!"=="yes" (
|
|
echo Skipping agent terminal runtime install ^(PLANNOTATOR_SKIP_AGENT_TERMINAL_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
|
|
"!INSTALL_PATH!" install-runtime agent-terminal
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Skipping agent terminal runtime install ^(plannotator install-runtime failed^)
|
|
)
|
|
goto :eof
|
|
|
|
REM ======================================================================
|
|
REM Opt-in CallDiff core install. Call flow is off by default, so a default
|
|
REM install never downloads even the pruned core. Review-specific packs
|
|
REM install in-app. Non-fatal when the opt-in install fails.
|
|
REM ======================================================================
|
|
:InstallCallFlowRuntime
|
|
if not "!INSTALL_CALL_FLOW!"=="1" (
|
|
echo Call-flow analysis: available as an in-app opt-in install ^(enable Call flow in review Settings^), or run: plannotator install-runtime call-flow
|
|
goto :eof
|
|
)
|
|
|
|
"!INSTALL_PATH!" install-runtime call-flow
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Call-flow runtime install failed; it remains available as an in-app opt-in install
|
|
)
|
|
goto :eof
|
|
|
|
REM ======================================================================
|
|
REM Optional semantic diff sidecar install. Non-fatal: Plannotator remains
|
|
REM installed if sem download, checksum, or extraction fails.
|
|
REM ======================================================================
|
|
:InstallSemSidecar
|
|
if /i "!PLANNOTATOR_SKIP_SEM_INSTALL!"=="1" (
|
|
echo Skipping semantic diff sidecar install ^(PLANNOTATOR_SKIP_SEM_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
if /i "!PLANNOTATOR_SKIP_SEM_INSTALL!"=="true" (
|
|
echo Skipping semantic diff sidecar install ^(PLANNOTATOR_SKIP_SEM_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
if /i "!PLANNOTATOR_SKIP_SEM_INSTALL!"=="yes" (
|
|
echo Skipping semantic diff sidecar install ^(PLANNOTATOR_SKIP_SEM_INSTALL is set^)
|
|
goto :eof
|
|
)
|
|
|
|
set "SEM_ASSET="
|
|
if /i "!PLATFORM!"=="win32-x64" set "SEM_ASSET=sem-windows-x86_64.zip"
|
|
if not defined SEM_ASSET (
|
|
echo Skipping semantic diff sidecar install ^(sem does not publish !PLATFORM!^)
|
|
goto :eof
|
|
)
|
|
|
|
set "SEM_DIR=!_CONFIG_DIR!\vendor\sem\!SEM_VERSION!"
|
|
set "SEM_PATH=!SEM_DIR!\sem.exe"
|
|
if exist "!SEM_PATH!" (
|
|
"!SEM_PATH!" --version 2>nul | findstr /r /c:"^sem " >nul 2>&1
|
|
if !ERRORLEVEL! equ 0 (
|
|
echo Semantic diff sidecar already installed at !SEM_PATH!
|
|
goto :eof
|
|
)
|
|
)
|
|
|
|
set "SEM_BASE_URL=https://github.com/!SEM_REPO!/releases/download/!SEM_VERSION!"
|
|
set "SEM_ARCHIVE=%TEMP%\plannotator-sem-%RANDOM%.zip"
|
|
set "SEM_CHECKSUMS=%TEMP%\plannotator-sem-checksums-%RANDOM%.txt"
|
|
set "SEM_EXTRACT=%TEMP%\plannotator-sem-%RANDOM%"
|
|
mkdir "!SEM_EXTRACT!" >nul 2>&1
|
|
|
|
REM Bounded so a slow/hung download of this optional sidecar can't wedge an
|
|
REM install where plannotator already landed. Opt out with PLANNOTATOR_SKIP_SEM_INSTALL=1.
|
|
curl -fsSL --connect-timeout 10 --max-time 120 "!SEM_BASE_URL!/!SEM_ASSET!" -o "!SEM_ARCHIVE!"
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Skipping semantic diff sidecar install ^(download failed^)
|
|
goto :sem_cleanup
|
|
)
|
|
|
|
curl -fsSL --connect-timeout 10 --max-time 60 "!SEM_BASE_URL!/checksums.txt" -o "!SEM_CHECKSUMS!"
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Skipping semantic diff sidecar install ^(checksum download failed^)
|
|
goto :sem_cleanup
|
|
)
|
|
|
|
set "EXPECTED_SEM_CHECKSUM="
|
|
for /f "usebackq tokens=1,2" %%i in ("!SEM_CHECKSUMS!") do (
|
|
if "%%j"=="!SEM_ASSET!" set "EXPECTED_SEM_CHECKSUM=%%i"
|
|
)
|
|
if not defined EXPECTED_SEM_CHECKSUM (
|
|
echo Skipping semantic diff sidecar install ^(checksum missing for !SEM_ASSET!^)
|
|
goto :sem_cleanup
|
|
)
|
|
|
|
set "ACTUAL_SEM_CHECKSUM="
|
|
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!SEM_ARCHIVE!" SHA256') do (
|
|
if not defined ACTUAL_SEM_CHECKSUM (
|
|
set "ACTUAL_SEM_CHECKSUM=%%i"
|
|
set "ACTUAL_SEM_CHECKSUM=!ACTUAL_SEM_CHECKSUM: =!"
|
|
)
|
|
)
|
|
if /i "!ACTUAL_SEM_CHECKSUM!" neq "!EXPECTED_SEM_CHECKSUM!" (
|
|
echo Skipping semantic diff sidecar install ^(checksum mismatch^)
|
|
goto :sem_cleanup
|
|
)
|
|
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "Expand-Archive -Force -Path $env:SEM_ARCHIVE -DestinationPath $env:SEM_EXTRACT"
|
|
if !ERRORLEVEL! neq 0 (
|
|
echo Skipping semantic diff sidecar install ^(extract failed^)
|
|
goto :sem_cleanup
|
|
)
|
|
set "EXTRACTED_SEM="
|
|
for /f "usebackq delims=" %%i in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem -Path $env:SEM_EXTRACT -Filter sem.exe -Recurse -File | Select-Object -First 1 -ExpandProperty FullName"`) do (
|
|
set "EXTRACTED_SEM=%%i"
|
|
)
|
|
if not defined EXTRACTED_SEM (
|
|
echo Skipping semantic diff sidecar install ^(binary missing from archive^)
|
|
goto :sem_cleanup
|
|
)
|
|
|
|
if not exist "!SEM_DIR!" mkdir "!SEM_DIR!"
|
|
copy /y "!EXTRACTED_SEM!" "!SEM_PATH!" >nul
|
|
if !ERRORLEVEL! equ 0 (
|
|
echo Semantic diff sidecar installed to !SEM_PATH!
|
|
) else (
|
|
echo Skipping semantic diff sidecar install ^(copy failed^)
|
|
)
|
|
|
|
:sem_cleanup
|
|
if exist "!SEM_ARCHIVE!" del "!SEM_ARCHIVE!"
|
|
if exist "!SEM_CHECKSUMS!" del "!SEM_CHECKSUMS!"
|
|
if exist "!SEM_EXTRACT!" rmdir /s /q "!SEM_EXTRACT!"
|
|
goto :eof
|
|
|
|
REM ======================================================================
|
|
REM Guided-install wizard (called only on interactive first runs or with
|
|
REM --reconfigure). Sets EXTRAS_CHOICE and INVOCABLE_CHOICE.
|
|
REM ======================================================================
|
|
:guided_wizard
|
|
echo.
|
|
echo ==========================================
|
|
echo PLANNOTATOR GUIDED INSTALL
|
|
echo ==========================================
|
|
echo.
|
|
if "!EXTRAS_PRESENT!"=="1" (
|
|
echo Extra skills already installed - keeping them.
|
|
set "EXTRAS_CHOICE=yes"
|
|
) else if defined EXTRAS_FLAG (
|
|
REM Flag already answered this question - don't ask and then ignore.
|
|
set "EXTRAS_CHOICE=!EXTRAS_FLAG!"
|
|
) else (
|
|
set "DEF_EXTRAS=no"
|
|
if defined SAVED_EXTRAS set "DEF_EXTRAS=!SAVED_EXTRAS!"
|
|
set "ANSWER="
|
|
set /p "ANSWER=Install the extra skills (compound planning, setup-goal, visual explainer)? [y/N] "
|
|
set "EXTRAS_CHOICE=no"
|
|
if /i "!ANSWER!"=="y" set "EXTRAS_CHOICE=yes"
|
|
if /i "!ANSWER!"=="yes" set "EXTRAS_CHOICE=yes"
|
|
if "!ANSWER!"=="" set "EXTRAS_CHOICE=!DEF_EXTRAS!"
|
|
)
|
|
if defined MODEL_INVOCABLE_FLAG (
|
|
REM Flag already answered this question - don't ask and then ignore.
|
|
set "INVOCABLE_CHOICE=!MODEL_INVOCABLE_FLAG!"
|
|
goto :eof
|
|
)
|
|
set "ANSWER="
|
|
set /p "ANSWER=Make any skills callable by the model (instead of user-invoked only)? [y/N] "
|
|
set "WANT_INVOCABLE=no"
|
|
if /i "!ANSWER!"=="y" set "WANT_INVOCABLE=yes"
|
|
if /i "!ANSWER!"=="yes" set "WANT_INVOCABLE=yes"
|
|
if "!WANT_INVOCABLE!"=="no" (
|
|
set "INVOCABLE_CHOICE=none"
|
|
goto :eof
|
|
)
|
|
set "SKILL_COUNT=3"
|
|
set "SKILL_1=plannotator-review"
|
|
set "SKILL_2=plannotator-annotate"
|
|
set "SKILL_3=plannotator-last"
|
|
if "!EXTRAS_CHOICE!"=="yes" (
|
|
set "SKILL_COUNT=6"
|
|
set "SKILL_4=plannotator-compound"
|
|
set "SKILL_5=plannotator-setup-goal"
|
|
set "SKILL_6=plannotator-visual-explainer"
|
|
)
|
|
REM Preselect previously chosen skills. NOTE: no pipes here - each side of a
|
|
REM cmd pipe runs in a child without delayed expansion, so !vars! would pass
|
|
REM through literally. A substring-replace containment test avoids that trap.
|
|
set "PRESEL=,!SAVED_INVOCABLE!,"
|
|
for /l %%I in (1,1,!SKILL_COUNT!) do (
|
|
set "SEL_%%I=0"
|
|
if defined SAVED_INVOCABLE (
|
|
for %%K in ("!SKILL_%%I!") do if not "!PRESEL:,%%~K,=!"=="!PRESEL!" set "SEL_%%I=1"
|
|
)
|
|
)
|
|
:toggle_loop
|
|
echo.
|
|
for /l %%I in (1,1,!SKILL_COUNT!) do (
|
|
set "MARK= "
|
|
if "!SEL_%%I!"=="1" set "MARK=x"
|
|
echo %%I^) [!MARK!] !SKILL_%%I!
|
|
)
|
|
set "PICK="
|
|
set /p "PICK=Toggle a number (press enter on empty input to confirm): "
|
|
if "!PICK!"=="" goto :collect_invocable
|
|
set "VALID=0"
|
|
for /l %%I in (1,1,!SKILL_COUNT!) do if "!PICK!"=="%%I" set "VALID=1"
|
|
if "!VALID!"=="0" (
|
|
echo Invalid choice: !PICK!
|
|
goto :toggle_loop
|
|
)
|
|
for %%I in (!PICK!) do (
|
|
if "!SEL_%%I!"=="1" (set "SEL_%%I=0") else (set "SEL_%%I=1")
|
|
)
|
|
goto :toggle_loop
|
|
:collect_invocable
|
|
set "INVOCABLE_CHOICE="
|
|
for /l %%I in (1,1,!SKILL_COUNT!) do (
|
|
if "!SEL_%%I!"=="1" (
|
|
if defined INVOCABLE_CHOICE (set "INVOCABLE_CHOICE=!INVOCABLE_CHOICE!,!SKILL_%%I!") else (set "INVOCABLE_CHOICE=!SKILL_%%I!")
|
|
)
|
|
)
|
|
if not defined INVOCABLE_CHOICE set "INVOCABLE_CHOICE=none"
|
|
goto :eof
|