mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
fix(install): restore /plannotator-* bash execution on Claude Code + harden unattended installs (#872)
Follow-up to #850 (unreleased), fixing two pre-release regressions vs v0.19.27. - Claude Code: add apps/skills/claude/* skills using `!`plannotator <sub> $ARGUMENTS`` + allowed-tools + disable-model-invocation, so /plannotator-review|annotate|last| archive auto-run with no permission prompt and pass arguments through — matching the old slash commands. Codex keeps the prose apps/skills/core/*. Installers route ~/.claude/skills <- apps/skills/claude and ~/.agents/skills <- apps/skills/core. - Installers: gate the wizard so an unattended-but-open /dev/tty can't hang (PLANNOTATOR_PROMPT_TIMEOUT, default 30s; timeout/EOF resolves to safe "no"). Don't gate on $CI (explicit --reconfigure/--extras must win). Don't persist timed-out wizard answers as install-prefs. Keep ask_yes_no's read in a tested context so set -e never aborts on a timeout. 72 installer tests pass.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: plannotator-annotate
|
||||
description: Open Plannotator's annotation UI for a markdown file, converted HTML file, URL, or folder and then respond to the returned annotations.
|
||||
allowed-tools: Bash(plannotator:*)
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Plannotator Annotate
|
||||
|
||||
## Markdown annotations
|
||||
|
||||
!`plannotator annotate $ARGUMENTS`
|
||||
|
||||
## Your task
|
||||
|
||||
The output above will be one of:
|
||||
|
||||
1. The exact text `The user approved.`, OR a JSON object with `"decision": "approved"`. The user approved the markdown file(s). Acknowledge with a single sentence ("Approved.") and stop. Do not begin any work.
|
||||
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
|
||||
3. Plaintext annotation feedback, OR a JSON object with `"decision": "annotated"` and a `"feedback"` field. Address the feedback. The user has reviewed the markdown file(s) and provided specific annotations and comments.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: plannotator-archive
|
||||
description: Browse saved plan decisions in Plannotator's read-only archive UI in the browser.
|
||||
allowed-tools: Bash(plannotator:*)
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Plannotator Archive
|
||||
|
||||
## Plan archive
|
||||
|
||||
!`plannotator archive`
|
||||
|
||||
## Your task
|
||||
|
||||
The user has finished browsing the plan archive. Acknowledge and continue.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: plannotator-last
|
||||
description: Open Plannotator on the latest rendered assistant message and use the returned annotations to revise that message or continue.
|
||||
allowed-tools: Bash(plannotator:*)
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Plannotator Last
|
||||
|
||||
## Message annotations
|
||||
|
||||
!`plannotator annotate-last $ARGUMENTS`
|
||||
|
||||
## Your task
|
||||
|
||||
The output above will be one of:
|
||||
|
||||
1. The exact text `The user approved.`, OR a JSON object with `"decision": "approved"`. The user approved your last message. Acknowledge with a single sentence ("Approved.") and stop. Do not begin any work.
|
||||
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
|
||||
3. Plaintext annotation feedback, OR a JSON object with `"decision": "annotated"` and a `"feedback"` field. Address the feedback. The user has reviewed your last message and provided specific annotations and comments.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: plannotator-review
|
||||
description: Open Plannotator's browser-based code review UI for the current worktree or a pull request URL, then act on the feedback that comes back.
|
||||
allowed-tools: Bash(plannotator:*)
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Plannotator Review
|
||||
|
||||
## Code review feedback
|
||||
|
||||
!`plannotator review $ARGUMENTS`
|
||||
|
||||
## Your task
|
||||
|
||||
If the review above contains feedback or annotations, address them in the same conversation. If no changes were requested (an approval/LGTM-style result), acknowledge that review passed and continue.
|
||||
+15
-5
@@ -716,20 +716,30 @@ if !ERRORLEVEL! equ 0 (
|
||||
pushd "!SKILLS_TMP!\repo"
|
||||
git sparse-checkout set apps/skills apps/kiro-cli apps/opencode-plugin/commands apps/gemini/commands >nul 2>&1
|
||||
|
||||
REM Core skills -> Claude + shared agent scope (all 4, copy-if-present).
|
||||
if exist "apps\skills\core" (
|
||||
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 plannotator-archive) 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
|
||||
)
|
||||
)
|
||||
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-archive) do (
|
||||
if exist "apps\skills\core\%%S" (
|
||||
REM Replace rather than merge so files removed upstream don't linger.
|
||||
if exist "!CLAUDE_SKILLS_DIR!\%%S" rmdir /s /q "!CLAUDE_SKILLS_DIR!\%%S" >nul 2>&1
|
||||
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" "!CLAUDE_SKILLS_DIR!\%%S\" >nul 2>&1
|
||||
xcopy /s /i /y /q "apps\skills\core\%%S" "!AGENTS_SKILLS_DIR!\%%S\" >nul 2>&1
|
||||
)
|
||||
)
|
||||
echo Installed core skills to !CLAUDE_SKILLS_DIR!\ and !AGENTS_SKILLS_DIR!\
|
||||
echo Installed shared agent skills to !AGENTS_SKILLS_DIR!\
|
||||
) else (
|
||||
echo Tag !TAG! predates the core/extra skill layout — skipping core skill install
|
||||
)
|
||||
|
||||
+19
-7
@@ -644,20 +644,32 @@ try {
|
||||
try {
|
||||
git sparse-checkout set apps/skills apps/kiro-cli apps/opencode-plugin/commands apps/gemini/commands 2>$null
|
||||
|
||||
# Core skills -> ~/.claude/skills and ~/.agents/skills (all 4).
|
||||
# Claude Code and Codex consume different skill bodies. Claude Code
|
||||
# reads apps/skills/claude/* (dynamic-context injection
|
||||
# `!`plannotator … $ARGUMENTS`` + allowed-tools, so /plannotator-*
|
||||
# run with no permission prompt — like the old slash commands).
|
||||
# Codex reads apps/skills/core/* (prose the model follows via its
|
||||
# own shell). The `!`…`` injection is a Claude-Code-only extension,
|
||||
# so the two are sourced separately rather than sharing one body.
|
||||
# Route each through Copy-SkillIfPresent (which pre-removes the
|
||||
# existing target dir) so re-runs replace rather than nest,
|
||||
# matching install.sh's per-skill structure.
|
||||
if ((Test-Path "apps\skills\core") -and (Get-ChildItem "apps\skills\core" -ErrorAction SilentlyContinue)) {
|
||||
# existing target dir) so re-runs replace rather than nest.
|
||||
if ((Test-Path "apps\skills\claude") -and (Get-ChildItem "apps\skills\claude" -ErrorAction SilentlyContinue)) {
|
||||
New-Item -ItemType Directory -Force -Path $claudeSkillsDir | Out-Null
|
||||
foreach ($skill in @("plannotator-review", "plannotator-annotate", "plannotator-last", "plannotator-archive")) {
|
||||
Copy-SkillIfPresent "apps\skills\claude\$skill" $claudeSkillsDir
|
||||
}
|
||||
Write-Host "Installed Claude Code skills to $claudeSkillsDir\"
|
||||
} else {
|
||||
Write-Host "Tag $latestTag predates the per-agent skill layout — skipping Claude Code skill install"
|
||||
}
|
||||
if ((Test-Path "apps\skills\core") -and (Get-ChildItem "apps\skills\core" -ErrorAction SilentlyContinue)) {
|
||||
New-Item -ItemType Directory -Force -Path $agentsSkillsDir | Out-Null
|
||||
foreach ($skill in @("plannotator-review", "plannotator-annotate", "plannotator-last", "plannotator-archive")) {
|
||||
Copy-SkillIfPresent "apps\skills\core\$skill" $claudeSkillsDir
|
||||
Copy-SkillIfPresent "apps\skills\core\$skill" $agentsSkillsDir
|
||||
}
|
||||
Write-Host "Installed core skills to $claudeSkillsDir\ and $agentsSkillsDir\"
|
||||
Write-Host "Installed shared agent skills to $agentsSkillsDir\"
|
||||
} else {
|
||||
Write-Host "Tag $latestTag predates the core/extra skill layout — skipping core skill install"
|
||||
Write-Host "Tag $latestTag predates the core/extra skill layout — skipping shared agent skill install"
|
||||
}
|
||||
|
||||
# Kiro: hand-maintained skills (origin baked in) + two extras.
|
||||
|
||||
+73
-17
@@ -730,13 +730,29 @@ if command -v glimpseui >/dev/null 2>&1; then
|
||||
glimpse_present=1
|
||||
fi
|
||||
|
||||
# A wizard needs a real keyboard. Piped installs (curl | bash) still have a
|
||||
# terminal at /dev/tty even though stdin is the pipe; CI and scripts do not.
|
||||
# A wizard needs a real human at the keyboard. Piped installs (curl | bash)
|
||||
# still have a terminal at /dev/tty even though stdin is the pipe; CI and
|
||||
# scripts do not. Some automated contexts (docker run -t, devcontainer and
|
||||
# provisioner shells) DO expose an openable /dev/tty with nobody behind it —
|
||||
# opening /dev/tty succeeds, yet a read would block forever. The per-prompt
|
||||
# timeout below (see PROMPT_TIMEOUT / ask_yes_no) handles that: a mis-detected
|
||||
# terminal falls through to the safe non-interactive defaults (extras=no,
|
||||
# model-invocable=none, glimpse=no) instead of wedging. We deliberately do NOT
|
||||
# gate on $CI here — an exported CI var must not silently suppress an explicit
|
||||
# --reconfigure or --extras in an otherwise interactive shell.
|
||||
can_prompt=0
|
||||
if [ "$NON_INTERACTIVE" -eq 0 ] && { : < /dev/tty; } 2>/dev/null; then
|
||||
can_prompt=1
|
||||
fi
|
||||
|
||||
# Bound every interactive read so an unattended-but-open /dev/tty auto-takes
|
||||
# the default rather than hanging. Set PLANNOTATOR_PROMPT_TIMEOUT=0 to wait
|
||||
# indefinitely (restores the old unbounded behavior); non-numeric falls to 30.
|
||||
PROMPT_TIMEOUT="${PLANNOTATOR_PROMPT_TIMEOUT:-30}"
|
||||
case "$PROMPT_TIMEOUT" in
|
||||
''|*[!0-9]*) PROMPT_TIMEOUT=30 ;;
|
||||
esac
|
||||
|
||||
run_wizard=0
|
||||
if [ "$can_prompt" -eq 1 ]; then
|
||||
if [ "$RECONFIGURE" -eq 1 ] || [ ! -f "$PREFS_FILE" ]; then
|
||||
@@ -746,11 +762,32 @@ fi
|
||||
|
||||
# Ask a y/n question on the terminal. $1 prompt, $2 default (yes/no).
|
||||
ask_yes_no() {
|
||||
local prompt="$1" default="$2" answer suffix
|
||||
local prompt="$1" default="$2" answer suffix rc
|
||||
suffix="[y/N]"
|
||||
[ "$default" = "yes" ] && suffix="[Y/n]"
|
||||
printf '%s %s ' "$prompt" "$suffix" > /dev/tty
|
||||
IFS= read -r answer < /dev/tty || answer=""
|
||||
# Bounded read so an unattended-but-open /dev/tty (e.g. docker run -t with
|
||||
# no human) can't hang the install. Distinguish a human pressing Enter
|
||||
# (read succeeds with an empty answer -> use the prompt's $default) from a
|
||||
# timeout/EOF with nobody there (read fails -> use the SAFE "no", never the
|
||||
# default). Otherwise a prompt whose default is "yes" (Glimpse) would
|
||||
# silently install software on an unattended terminal.
|
||||
# Keep the read in a tested context (`|| rc=$?`) so the read itself never
|
||||
# trips `set -e` (active at the top of this script), without relying on the
|
||||
# subtle rule that -e is suppressed inside a function called in a tested
|
||||
# context. ask_yes_no still returns non-zero on timeout/EOF to signal "no
|
||||
# human", so every caller consumes it with `|| wizard_timed_out=1`.
|
||||
rc=0
|
||||
if [ "$PROMPT_TIMEOUT" -gt 0 ]; then
|
||||
IFS= read -r -t "$PROMPT_TIMEOUT" answer < /dev/tty || rc=$?
|
||||
else
|
||||
IFS= read -r answer < /dev/tty || rc=$?
|
||||
fi
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
printf '\n' > /dev/tty
|
||||
echo "no"
|
||||
return 1
|
||||
fi
|
||||
case "$answer" in
|
||||
y|Y|yes|YES|Yes) echo "yes" ;;
|
||||
n|N|no|NO|No) echo "no" ;;
|
||||
@@ -811,6 +848,9 @@ select_skills_checkbox() {
|
||||
extras_choice=""
|
||||
invocable_choice=""
|
||||
glimpse_choice=""
|
||||
# Set if any wizard prompt times out / hits EOF (no human answered). A run whose
|
||||
# answers are synthetic timeout fallbacks must not be persisted as install-prefs.
|
||||
wizard_timed_out=0
|
||||
|
||||
if [ "$run_wizard" -eq 1 ]; then
|
||||
{
|
||||
@@ -827,7 +867,7 @@ if [ "$run_wizard" -eq 1 ]; then
|
||||
# Flag already answered this question — don't ask and then ignore.
|
||||
extras_choice="$EXTRAS_FLAG"
|
||||
else
|
||||
extras_choice=$(ask_yes_no "Install the extra skills (compound planning, setup-goal, visual explainer)?" "${saved_extras:-no}")
|
||||
extras_choice=$(ask_yes_no "Install the extra skills (compound planning, setup-goal, visual explainer)?" "${saved_extras:-no}") || wizard_timed_out=1
|
||||
fi
|
||||
invocable_list="$CORE_SKILL_NAMES"
|
||||
if [ "$extras_choice" = "yes" ]; then
|
||||
@@ -837,7 +877,7 @@ if [ "$run_wizard" -eq 1 ]; then
|
||||
# Flag already answered this question — don't ask and then ignore.
|
||||
invocable_choice="$MODEL_INVOCABLE_FLAG"
|
||||
else
|
||||
want_invocable=$(ask_yes_no "Make any skills callable by the model (instead of user-invoked only)?" "no")
|
||||
want_invocable=$(ask_yes_no "Make any skills callable by the model (instead of user-invoked only)?" "no") || wizard_timed_out=1
|
||||
if [ "$want_invocable" = "yes" ]; then
|
||||
invocable_choice=$(select_skills_checkbox "$invocable_list" "$saved_invocable")
|
||||
else
|
||||
@@ -851,7 +891,7 @@ if [ "$run_wizard" -eq 1 ]; then
|
||||
# Flag already answered this question — don't ask and then ignore.
|
||||
glimpse_choice="$GLIMPSE_FLAG"
|
||||
else
|
||||
glimpse_choice=$(ask_yes_no "Install Glimpse so Plannotator opens in a native window instead of a browser tab? (npm i -g glimpseui)" "${saved_glimpse:-yes}")
|
||||
glimpse_choice=$(ask_yes_no "Install Glimpse so Plannotator opens in a native window instead of a browser tab? (npm i -g glimpseui)" "${saved_glimpse:-yes}") || wizard_timed_out=1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -863,9 +903,11 @@ fi
|
||||
[ -z "$invocable_choice" ] && invocable_choice="${saved_invocable:-none}"
|
||||
[ -z "$glimpse_choice" ] && glimpse_choice="${saved_glimpse:-no}"
|
||||
|
||||
# Persist only when the wizard ran or a flag set something — silent re-runs
|
||||
# must not clobber saved answers with defaults.
|
||||
if [ "$run_wizard" -eq 1 ] || [ -n "$EXTRAS_FLAG" ] || [ -n "$MODEL_INVOCABLE_FLAG" ] || [ -n "$GLIMPSE_FLAG" ]; then
|
||||
# Persist only when the wizard ran with real answers, or a flag set something.
|
||||
# Silent re-runs must not clobber saved answers with defaults, and a wizard that
|
||||
# timed out to synthetic fallbacks (unattended /dev/tty) must not become sticky
|
||||
# prefs that suppress the wizard on a later genuine interactive install.
|
||||
if [ "$wizard_timed_out" -eq 0 ] && { [ "$run_wizard" -eq 1 ] || [ -n "$EXTRAS_FLAG" ] || [ -n "$MODEL_INVOCABLE_FLAG" ] || [ -n "$GLIMPSE_FLAG" ]; }; then
|
||||
mkdir -p "$_config_dir"
|
||||
{
|
||||
echo "extras=$extras_choice"
|
||||
@@ -969,19 +1011,33 @@ checkout_failed=0
|
||||
# via --version may predate the core/extra layout — skip core skills
|
||||
# but keep installing the command files below (matches install.ps1 and
|
||||
# install.cmd, which guard each block independently).
|
||||
# Claude Code and Codex consume different skill bodies. Claude Code reads
|
||||
# the apps/skills/claude/* copies, which use dynamic-context injection
|
||||
# (`!`plannotator … $ARGUMENTS``) + allowed-tools so /plannotator-* run the
|
||||
# binary directly with no permission prompt — matching the old slash
|
||||
# commands. Codex (the OpenAI shared-agent path) reads apps/skills/core/*,
|
||||
# whose prose bodies the model follows via its own shell; the `!`…``
|
||||
# injection is a Claude-Code-only extension, so the two are sourced
|
||||
# separately rather than sharing one body.
|
||||
if [ -d "apps/skills/claude" ] && [ -n "$(ls -A apps/skills/claude 2>/dev/null)" ]; then
|
||||
mkdir -p "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/claude/plannotator-review "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/claude/plannotator-annotate "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/claude/plannotator-last "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/claude/plannotator-archive "$CLAUDE_SKILLS_DIR"
|
||||
echo "Installed Claude Code skills to ${CLAUDE_SKILLS_DIR}/"
|
||||
else
|
||||
echo "Tag ${latest_tag} predates the per-agent skill layout — skipping Claude Code skill install"
|
||||
fi
|
||||
if [ -d "apps/skills/core" ] && [ -n "$(ls -A apps/skills/core 2>/dev/null)" ]; then
|
||||
mkdir -p "$CLAUDE_SKILLS_DIR" "$AGENTS_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-review "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-annotate "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-last "$CLAUDE_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-archive "$CLAUDE_SKILLS_DIR"
|
||||
mkdir -p "$AGENTS_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-review "$AGENTS_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-annotate "$AGENTS_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-last "$AGENTS_SKILLS_DIR"
|
||||
copy_skill_if_present apps/skills/core/plannotator-archive "$AGENTS_SKILLS_DIR"
|
||||
echo "Installed core skills to ${CLAUDE_SKILLS_DIR}/ and shared agent skills to ${AGENTS_SKILLS_DIR}/"
|
||||
echo "Installed shared agent skills to ${AGENTS_SKILLS_DIR}/"
|
||||
else
|
||||
echo "Tag ${latest_tag} predates the core/extra skill layout — skipping core skill install"
|
||||
echo "Tag ${latest_tag} predates the core/extra skill layout — skipping shared agent skill install"
|
||||
fi
|
||||
|
||||
# OpenCode slash command stubs (the plugin intercepts execution) —
|
||||
|
||||
+25
-11
@@ -78,9 +78,12 @@ describe("install.sh", () => {
|
||||
expect(script).toContain("AGENTS_SKILLS_DIR");
|
||||
expect(script).toContain("$HOME/.agents/skills");
|
||||
expect(script).toContain("copy_skill_if_present");
|
||||
// Core skills (all 4) -> Claude Code and the official OpenAI shared-agent path.
|
||||
// Claude Code reads the injection-form skills from apps/skills/claude;
|
||||
// the OpenAI shared-agent (Codex) path reads the prose skills from
|
||||
// apps/skills/core. Sourced separately because `!`…`` injection is a
|
||||
// Claude-Code-only extension.
|
||||
for (const skill of CORE_SKILLS) {
|
||||
expect(script).toContain(`copy_skill_if_present apps/skills/core/${skill} "$CLAUDE_SKILLS_DIR"`);
|
||||
expect(script).toContain(`copy_skill_if_present apps/skills/claude/${skill} "$CLAUDE_SKILLS_DIR"`);
|
||||
expect(script).toContain(`copy_skill_if_present apps/skills/core/${skill} "$AGENTS_SKILLS_DIR"`);
|
||||
}
|
||||
// Codex no longer receives a skills install (core skills live in ~/.agents/skills).
|
||||
@@ -100,7 +103,7 @@ describe("install.sh", () => {
|
||||
// fetch or an old pinned tag never deletes commands without replacement.
|
||||
expect(script).toContain('if [ -d "$CLAUDE_SKILLS_DIR/$cmd" ] && [ -f "$CLAUDE_COMMANDS_DIR/$cmd.md" ]');
|
||||
const cleanupIndex = script.indexOf('Removed legacy Claude command');
|
||||
const installIndex = script.indexOf('copy_skill_if_present apps/skills/core/plannotator-review');
|
||||
const installIndex = script.indexOf('copy_skill_if_present apps/skills/claude/plannotator-review');
|
||||
expect(installIndex).toBeGreaterThan(0);
|
||||
expect(cleanupIndex).toBeGreaterThan(installIndex);
|
||||
});
|
||||
@@ -343,10 +346,11 @@ describe("install.ps1", () => {
|
||||
expect(script).toContain("agentsSkillsDir");
|
||||
expect(script).toContain("$env:USERPROFILE\\.agents\\skills");
|
||||
expect(script).toContain("Copy-SkillIfPresent");
|
||||
// Core skills (all 4) copied verbatim to both Claude and the shared-agent
|
||||
// scope, per-skill via Copy-SkillIfPresent so re-runs replace rather than
|
||||
// nest (PowerShell's Copy-Item -Recurse into an existing dir nests).
|
||||
expect(script).toContain('Copy-SkillIfPresent "apps\\skills\\core\\$skill" $claudeSkillsDir');
|
||||
// Claude Code reads injection-form skills (apps\skills\claude); the
|
||||
// shared-agent (Codex) scope reads the prose skills (apps\skills\core).
|
||||
// Per-skill via Copy-SkillIfPresent so re-runs replace rather than nest
|
||||
// (PowerShell's Copy-Item -Recurse into an existing dir nests).
|
||||
expect(script).toContain('Copy-SkillIfPresent "apps\\skills\\claude\\$skill" $claudeSkillsDir');
|
||||
expect(script).toContain('Copy-SkillIfPresent "apps\\skills\\core\\$skill" $agentsSkillsDir');
|
||||
expect(script).toContain('"plannotator-review", "plannotator-annotate", "plannotator-last", "plannotator-archive"');
|
||||
// Copy-SkillIfPresent pre-removes the destination to avoid nesting on upgrade.
|
||||
@@ -467,8 +471,9 @@ describe("install.cmd", () => {
|
||||
expect(script).toContain("CLAUDE_SKILLS_DIR");
|
||||
expect(script).toContain("AGENTS_SKILLS_DIR");
|
||||
expect(script).toContain("%USERPROFILE%\\.agents\\skills");
|
||||
// Core skills (all 4) copied to both Claude and shared-agent scope.
|
||||
expect(script).toContain('xcopy /s /i /y /q "apps\\skills\\core\\%%S" "!CLAUDE_SKILLS_DIR!\\%%S\\"');
|
||||
// Claude Code reads injection-form skills (apps\skills\claude); the shared
|
||||
// agent (Codex) scope reads the prose skills (apps\skills\core).
|
||||
expect(script).toContain('xcopy /s /i /y /q "apps\\skills\\claude\\%%S" "!CLAUDE_SKILLS_DIR!\\%%S\\"');
|
||||
expect(script).toContain('xcopy /s /i /y /q "apps\\skills\\core\\%%S" "!AGENTS_SKILLS_DIR!\\%%S\\"');
|
||||
expect(script).toContain("for %%S in (plannotator-review plannotator-annotate plannotator-last plannotator-archive) do");
|
||||
// No Codex skills install — only the cleanup loop references CODEX skills.
|
||||
@@ -632,8 +637,17 @@ describe("install shared behavior", () => {
|
||||
expect(cmdScript).toContain("timeout /t 0");
|
||||
expect(cmdScript).toContain('if "!CAN_PROMPT!"=="1"');
|
||||
expect(cmdScript).toContain("set /p");
|
||||
// Silent re-runs must not clobber saved answers with defaults.
|
||||
expect(sh).toContain('if [ "$run_wizard" -eq 1 ] || [ -n "$EXTRAS_FLAG" ] || [ -n "$MODEL_INVOCABLE_FLAG" ] || [ -n "$GLIMPSE_FLAG" ]');
|
||||
// Silent re-runs must not clobber saved answers with defaults, and a wizard
|
||||
// that timed out to synthetic fallbacks (unattended /dev/tty) must not be
|
||||
// persisted — ask_yes_no returns non-zero on timeout/EOF, each prompt ORs
|
||||
// that into wizard_timed_out, and the prefs write is gated on it.
|
||||
expect(sh).toContain('if [ "$wizard_timed_out" -eq 0 ] && { [ "$run_wizard" -eq 1 ] || [ -n "$EXTRAS_FLAG" ] || [ -n "$MODEL_INVOCABLE_FLAG" ] || [ -n "$GLIMPSE_FLAG" ]; }');
|
||||
expect(sh).toContain("wizard_timed_out=0");
|
||||
expect(sh).toContain("|| wizard_timed_out=1");
|
||||
expect(sh).toMatch(/echo "no"\s+return 1/);
|
||||
// The bounded read stays in a tested context (`|| rc=$?`) so `set -e` never
|
||||
// aborts ask_yes_no on a timeout/EOF, regardless of how it's called.
|
||||
expect(sh).toContain('< /dev/tty || rc=$?');
|
||||
expect(ps).toContain("if ($runWizard -or $Extras -or $NoExtras -or $ModelInvocable -or $Glimpse -or $NoGlimpse)");
|
||||
expect(cmdScript).toContain('if "!DO_PERSIST!"=="1"');
|
||||
// Glimpse question: detect-on-PATH skip + global npm install in all three.
|
||||
|
||||
Reference in New Issue
Block a user