mirror of
https://github.com/obra/superpowers.git
synced 2026-06-14 22:59:06 +08:00
Compare commits
11 Commits
codex/trim
...
codex/shel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb6bdf9dd3 | ||
|
|
d7c260a978 | ||
|
|
f3f0789c5c | ||
|
|
16a1719988 | ||
|
|
c74c22daa7 | ||
|
|
773bbf61d6 | ||
|
|
6b76158550 | ||
|
|
7fec40bb55 | ||
|
|
2a8e54735b | ||
|
|
f776394360 | ||
|
|
7301c81b4d |
38
.kimi-plugin/plugin.json
Normal file
38
.kimi-plugin/plugin.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "superpowers",
|
||||||
|
"version": "5.1.0",
|
||||||
|
"description": "An agentic skills framework and software development methodology.",
|
||||||
|
"author": {
|
||||||
|
"name": "Jesse Vincent",
|
||||||
|
"email": "jesse@fsck.com"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/obra/superpowers",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"brainstorming",
|
||||||
|
"subagent-driven-development",
|
||||||
|
"skills",
|
||||||
|
"planning",
|
||||||
|
"tdd",
|
||||||
|
"debugging",
|
||||||
|
"code-review",
|
||||||
|
"workflow"
|
||||||
|
],
|
||||||
|
"skills": "./skills/",
|
||||||
|
"sessionStart": {
|
||||||
|
"skill": "using-superpowers"
|
||||||
|
},
|
||||||
|
"skillInstructions": "Kimi Code tool mapping for Superpowers skills:\n\n- When a Superpowers skill says to ask the user, ask clarifying questions, ask one question at a time, present multiple-choice options, use the terminal for a question, or wait for the user's choice, call Kimi Code's `AskUserQuestion` tool. Do not render those choices as plain assistant text unless `AskUserQuestion` is unavailable or the session is in auto permission mode.\n- For `AskUserQuestion`, provide 1 question with 2-4 concrete options when possible. Put the recommended option first and suffix its label with `(Recommended)`.\n- When a Superpowers skill refers to `TodoWrite`, use Kimi Code's `TodoList` tool.\n- When a Superpowers skill says `Task tool (general-purpose)` or asks you to dispatch an implementer/reviewer subagent, use Kimi Code's `Agent` tool with a Kimi subagent type. Do not pass `general-purpose` as `subagent_type`.\n- For implementation, code review, spec review, quality review, and filled Superpowers subagent prompt templates, call `Agent` with `subagent_type: \"coder\"`, paste the fully filled prompt into `prompt`, and provide a short `description`.\n- For read-only codebase exploration that would take several searches, use `Agent` with `subagent_type: \"explore\"`.\n- For read-only planning or architecture design, use `Agent` with `subagent_type: \"plan\"`.\n- Keep dependent Superpowers subagent steps sequential. Use multiple `Agent` calls, or `run_in_background: true` only when the work is independent and background agents are available.\n- When a Superpowers skill refers to the `Skill` tool, use Kimi Code's native `Skill` tool.\n- Use Kimi Code's `Read`, `Write`, `Edit`, `Bash`, `Grep`, `Glob`, `FetchURL`, `WebSearch`, and MCP tools by their actual exposed names.\n- When a skill asks to search file contents, use `Grep`; when it asks to find files by path or pattern, use `Glob`; when it asks to fetch a URL, use `FetchURL`; when it asks to search the web, use `WebSearch`.",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Superpowers",
|
||||||
|
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
|
||||||
|
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
|
||||||
|
"developerName": "Jesse Vincent",
|
||||||
|
"capabilities": [
|
||||||
|
"Interactive",
|
||||||
|
"Read",
|
||||||
|
"Write"
|
||||||
|
],
|
||||||
|
"websiteURL": "https://github.com/obra/superpowers"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
{ "path": ".claude-plugin/plugin.json", "field": "version" },
|
{ "path": ".claude-plugin/plugin.json", "field": "version" },
|
||||||
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
|
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
|
||||||
{ "path": ".codex-plugin/plugin.json", "field": "version" },
|
{ "path": ".codex-plugin/plugin.json", "field": "version" },
|
||||||
|
{ "path": ".kimi-plugin/plugin.json", "field": "version" },
|
||||||
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
|
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
|
||||||
{ "path": "gemini-extension.json", "field": "version" }
|
{ "path": "gemini-extension.json", "field": "version" }
|
||||||
],
|
],
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -4,7 +4,7 @@ Superpowers is a complete software development methodology for your coding agent
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [OpenCode](#opencode), [Pi](#pi).
|
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -149,6 +149,26 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
|
|||||||
copilot plugin install superpowers@superpowers-marketplace
|
copilot plugin install superpowers@superpowers-marketplace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Kimi Code
|
||||||
|
|
||||||
|
Superpowers is available in Kimi Code's plugin marketplace.
|
||||||
|
|
||||||
|
- Open Kimi Code's plugin manager:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
- Go to `Marketplace` > `Superpowers` and install it.
|
||||||
|
|
||||||
|
- Or install directly from this repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins install https://github.com/obra/superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
- Detailed docs: [docs/README.kimi.md](docs/README.kimi.md)
|
||||||
|
|
||||||
### OpenCode
|
### OpenCode
|
||||||
|
|
||||||
OpenCode uses its own plugin install; install Superpowers separately even if you
|
OpenCode uses its own plugin install; install Superpowers separately even if you
|
||||||
|
|||||||
88
docs/README.kimi.md
Normal file
88
docs/README.kimi.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Superpowers for Kimi Code
|
||||||
|
|
||||||
|
Complete guide for using Superpowers with [Kimi Code](https://github.com/MoonshotAI/kimi-code).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Superpowers is available in Kimi Code's plugin marketplace.
|
||||||
|
|
||||||
|
Open the plugin manager:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to `Marketplace` > `Superpowers` and install it.
|
||||||
|
|
||||||
|
You can also install from this repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins install https://github.com/obra/superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
For unreleased validation against `dev`, pin the branch explicitly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins install https://github.com/obra/superpowers/tree/dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Kimi Code applies plugin changes to new sessions. After installing, updating, enabling, disabling, or reloading a plugin, start a fresh session with `/new`.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The Kimi plugin manifest lives at `.kimi-plugin/plugin.json`.
|
||||||
|
|
||||||
|
The manifest does three things:
|
||||||
|
|
||||||
|
1. Points Kimi Code at the existing `skills/` directory.
|
||||||
|
2. Loads `using-superpowers` at session start through `sessionStart.skill`.
|
||||||
|
3. Provides Kimi-specific tool mapping through `skillInstructions`.
|
||||||
|
|
||||||
|
Kimi Code reads Superpowers skills from this repository. There are no copied skills, symlinks, hooks, or extra runtime dependencies.
|
||||||
|
|
||||||
|
## Tool Mapping
|
||||||
|
|
||||||
|
Skills describe actions instead of hard-coding one runtime's tool names. On Kimi Code these resolve to:
|
||||||
|
|
||||||
|
- "Ask the user" / "ask clarifying questions" -> `AskUserQuestion`
|
||||||
|
- "Create a todo" / "mark complete in todo list" -> `TodoList`
|
||||||
|
- "Dispatch a subagent" -> `Agent`
|
||||||
|
- "Invoke a skill" -> Kimi Code's native `Skill` tool
|
||||||
|
- "Read a file" / "write a file" / "edit a file" -> `Read`, `Write`, `Edit`
|
||||||
|
- "Run a shell command" -> `Bash`
|
||||||
|
- "Search file contents" -> `Grep`
|
||||||
|
- "Find files by path or pattern" -> `Glob`
|
||||||
|
- "Fetch a URL" -> `FetchURL`
|
||||||
|
- "Search the web" -> `WebSearch`
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Use Kimi Code's plugin manager:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
Select Superpowers and update it from there. Start a fresh session with `/new` after updating.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Plugin not loading
|
||||||
|
|
||||||
|
1. Run `/plugins info superpowers` and check diagnostics.
|
||||||
|
2. Make sure the plugin is enabled.
|
||||||
|
3. Start a fresh session with `/new` after install or update.
|
||||||
|
|
||||||
|
### Direct GitHub install used an old release
|
||||||
|
|
||||||
|
Kimi Code installs the latest GitHub release for a bare repository URL when one exists. To test unreleased changes before the next Superpowers release, install the branch explicitly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/plugins install https://github.com/obra/superpowers/tree/dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skills not triggering
|
||||||
|
|
||||||
|
1. Confirm `/plugins info superpowers` shows the plugin enabled.
|
||||||
|
2. Start a fresh session with `/new`.
|
||||||
|
3. Try the acceptance prompt: `Let's make a react todo list`. A working install should load `brainstorming` before writing code.
|
||||||
@@ -675,7 +675,7 @@ it. Distribution differs per harness ecosystem — find yours:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Native plugin marketplace | Claude Code | Register in `.claude-plugin/marketplace.json`; users `/plugin install`. The external `superpowers-marketplace` repo is the source of truth users install from — see the release steps in `CLAUDE.md`. |
|
| Native plugin marketplace | Claude Code | Register in `.claude-plugin/marketplace.json`; users `/plugin install`. The external `superpowers-marketplace` repo is the source of truth users install from — see the release steps in `CLAUDE.md`. |
|
||||||
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
|
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
|
||||||
| Git-URL extension install | Gemini, OpenCode | Users install from a git URL (`gemini extensions install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
|
| Git-URL extension install | Gemini, Kimi Code, OpenCode | Users install from a git URL (`gemini extensions install …`; Kimi Code `/plugins install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
|
||||||
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
|
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
|
||||||
| Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). |
|
| Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). |
|
||||||
|
|
||||||
@@ -788,6 +788,7 @@ Use this as the live index; when in doubt, read the files, not this table.
|
|||||||
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
|
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
|
||||||
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
|
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
|
||||||
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
|
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
|
||||||
|
| Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL |
|
||||||
| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL |
|
| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL |
|
||||||
| pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields |
|
| pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields |
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Live in `tests/`. Currently:
|
|||||||
- `tests/brainstorm-server/` — node test suite for the brainstorm server JS code.
|
- `tests/brainstorm-server/` — node test suite for the brainstorm server JS code.
|
||||||
- `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration.
|
- `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration.
|
||||||
- `tests/codex-plugin-sync/` — bash sync verification.
|
- `tests/codex-plugin-sync/` — bash sync verification.
|
||||||
|
- `tests/kimi/` — bash/Python checks for Kimi plugin manifest wiring.
|
||||||
- `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests.
|
- `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests.
|
||||||
- `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior).
|
- `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior).
|
||||||
- `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, Claude Code task-tracking, and token telemetry assertions).
|
- `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, Claude Code task-tracking, and token telemetry assertions).
|
||||||
|
|||||||
211
scripts/lint-shell.sh
Executable file
211
scripts/lint-shell.sh
Executable file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Lint shell scripts in this repository.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/lint-shell.sh [--all] [--format] [--strict] [file ...]
|
||||||
|
#
|
||||||
|
# By default, runs ShellCheck and shell syntax checks on changed shell scripts.
|
||||||
|
# Use --format to format with shfmt before linting. Use --all for the full tracked
|
||||||
|
# baseline, or pass files explicitly to lint a smaller set.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_tool() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "required tool '$1' is not on PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_shell_file() {
|
||||||
|
local path="$1"
|
||||||
|
local first_line=""
|
||||||
|
|
||||||
|
[[ -f "$path" ]] || return 1
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
*.sh)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
IFS= read -r first_line <"$path" || true
|
||||||
|
[[ "$first_line" =~ ^#!.*[/[:space:]](bash|dash|ksh|sh)([[:space:]]|$) ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_git_work_tree() {
|
||||||
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||||
|
|| die "run this from inside a git work tree, or pass files explicitly"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_shell_file() {
|
||||||
|
local path
|
||||||
|
local existing
|
||||||
|
|
||||||
|
path="$1"
|
||||||
|
if ! is_shell_file "$path"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${#files[@]}" -gt 0 ]]; then
|
||||||
|
for existing in "${files[@]}"; do
|
||||||
|
if [[ "$existing" == "$path" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
files+=("$path")
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_all_shell_files() {
|
||||||
|
local path
|
||||||
|
|
||||||
|
ensure_git_work_tree
|
||||||
|
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
add_shell_file "$path"
|
||||||
|
done < <(git ls-files -z)
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_changed_shell_files() {
|
||||||
|
local path
|
||||||
|
|
||||||
|
ensure_git_work_tree
|
||||||
|
|
||||||
|
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
add_shell_file "$path"
|
||||||
|
done < <(git diff --name-only -z --diff-filter=ACMR HEAD)
|
||||||
|
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
add_shell_file "$path"
|
||||||
|
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
|
||||||
|
else
|
||||||
|
collect_all_shell_files
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
add_shell_file "$path"
|
||||||
|
done < <(git ls-files --others --exclude-standard -z)
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_requested_shell_files() {
|
||||||
|
local path
|
||||||
|
|
||||||
|
for path in "$@"; do
|
||||||
|
add_shell_file "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
syntax_shell_for() {
|
||||||
|
local path="$1"
|
||||||
|
local first_line=""
|
||||||
|
|
||||||
|
IFS= read -r first_line <"$path" || true
|
||||||
|
|
||||||
|
case "$first_line" in
|
||||||
|
*"/sh"* | *" env sh"* | *"/dash"* | *" env dash"*)
|
||||||
|
printf 'sh'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'bash'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
run_syntax_checks() {
|
||||||
|
local file
|
||||||
|
local shell_name
|
||||||
|
|
||||||
|
for file in "$@"; do
|
||||||
|
shell_name="$(syntax_shell_for "$file")"
|
||||||
|
case "$shell_name" in
|
||||||
|
sh)
|
||||||
|
sh -n "$file"
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
bash -n "$file"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "unsupported shell for syntax check: $shell_name"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
format=false
|
||||||
|
strict=false
|
||||||
|
all=false
|
||||||
|
requested_files=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--all)
|
||||||
|
all=true
|
||||||
|
;;
|
||||||
|
--format)
|
||||||
|
format=true
|
||||||
|
;;
|
||||||
|
--strict)
|
||||||
|
strict=true
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
requested_files+=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
die "unknown option: $1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
requested_files+=("$1")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
require_tool shellcheck
|
||||||
|
if [[ "$format" == true ]]; then
|
||||||
|
require_tool shfmt
|
||||||
|
fi
|
||||||
|
|
||||||
|
files=()
|
||||||
|
if [[ "${#requested_files[@]}" -gt 0 ]]; then
|
||||||
|
collect_requested_shell_files "${requested_files[@]}"
|
||||||
|
elif [[ "$all" == true ]]; then
|
||||||
|
collect_all_shell_files
|
||||||
|
else
|
||||||
|
collect_changed_shell_files
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${#files[@]}" -eq 0 ]]; then
|
||||||
|
echo "No shell files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$format" == true ]]; then
|
||||||
|
echo "Formatting ${#files[@]} shell files"
|
||||||
|
shfmt_args=(-i 2 -ci -bn)
|
||||||
|
shfmt "${shfmt_args[@]}" -w "${files[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Linting ${#files[@]} shell files"
|
||||||
|
|
||||||
|
shellcheck_args=(--severity=warning --external-sources --source-path=SCRIPTDIR)
|
||||||
|
if [[ "$strict" == true ]]; then
|
||||||
|
shellcheck_args+=("--enable=check-extra-masked-returns,check-set-e-suppressed,quote-safe-variables,deprecate-which,avoid-nullary-conditions")
|
||||||
|
fi
|
||||||
|
|
||||||
|
shellcheck "${shellcheck_args[@]}" "${files[@]}"
|
||||||
|
run_syntax_checks "${files[@]}"
|
||||||
@@ -52,6 +52,7 @@ EXCLUDES=(
|
|||||||
"/.gitattributes"
|
"/.gitattributes"
|
||||||
"/.github/"
|
"/.github/"
|
||||||
"/.gitignore"
|
"/.gitignore"
|
||||||
|
"/.kimi-plugin/"
|
||||||
"/.opencode/"
|
"/.opencode/"
|
||||||
"/.pi/"
|
"/.pi/"
|
||||||
"/.version-bump.json"
|
"/.version-bump.json"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const path = require('path');
|
|||||||
|
|
||||||
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
|
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
|
||||||
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||||
|
const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
function computeAcceptKey(clientKey) {
|
function computeAcceptKey(clientKey) {
|
||||||
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
|
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
|
||||||
@@ -53,10 +54,18 @@ function decodeFrame(buffer) {
|
|||||||
offset = 4;
|
offset = 4;
|
||||||
} else if (payloadLen === 127) {
|
} else if (payloadLen === 127) {
|
||||||
if (buffer.length < 10) return null;
|
if (buffer.length < 10) return null;
|
||||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
const extendedLen = buffer.readBigUInt64BE(2);
|
||||||
|
if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) {
|
||||||
|
throw new Error('WebSocket frame payload exceeds maximum allowed size');
|
||||||
|
}
|
||||||
|
payloadLen = Number(extendedLen);
|
||||||
offset = 10;
|
offset = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
|
||||||
|
throw new Error('WebSocket frame payload exceeds maximum allowed size');
|
||||||
|
}
|
||||||
|
|
||||||
const maskOffset = offset;
|
const maskOffset = offset;
|
||||||
const dataOffset = offset + 4;
|
const dataOffset = offset + 4;
|
||||||
const totalLen = dataOffset + payloadLen;
|
const totalLen = dataOffset + payloadLen;
|
||||||
@@ -351,4 +360,4 @@ if (require.main === module) {
|
|||||||
startServer();
|
startServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
|
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES, MAX_FRAME_PAYLOAD_BYTES };
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ if [[ -f "$PID_FILE" ]]; then
|
|||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR" || exit
|
||||||
|
|
||||||
# Resolve the harness PID (grandparent of this script).
|
# Resolve the harness PID (grandparent of this script).
|
||||||
# $PPID is the ephemeral shell the harness spawned to run us — it dies
|
# $PPID is the ephemeral shell the harness spawned to run us — it dies
|
||||||
@@ -135,7 +135,7 @@ disown "$SERVER_PID" 2>/dev/null
|
|||||||
echo "$SERVER_PID" > "$PID_FILE"
|
echo "$SERVER_PID" > "$PID_FILE"
|
||||||
|
|
||||||
# Wait for server-started message (check log file)
|
# Wait for server-started message (check log file)
|
||||||
for i in {1..50}; do
|
for _ in {1..50}; do
|
||||||
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
||||||
# Verify server is still alive after a short window (catches process reapers)
|
# Verify server is still alive after a short window (catches process reapers)
|
||||||
alive="true"
|
alive="true"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ if [[ -f "$PID_FILE" ]]; then
|
|||||||
kill "$pid" 2>/dev/null || true
|
kill "$pid" 2>/dev/null || true
|
||||||
|
|
||||||
# Wait for graceful shutdown (up to ~2s)
|
# Wait for graceful shutdown (up to ~2s)
|
||||||
for i in {1..20}; do
|
for _ in {1..20}; do
|
||||||
if ! kill -0 "$pid" 2>/dev/null; then
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ Subagent (general-purpose):
|
|||||||
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
|
||||||
- What you implemented (or what you attempted, if blocked)
|
- What you implemented (or what you attempted, if blocked)
|
||||||
- What you tested and test results
|
- What you tested and test results
|
||||||
|
- **TDD Evidence** (if TDD was required for this task):
|
||||||
|
- RED: command run, relevant failing output before implementation, and why the failure was expected
|
||||||
|
- GREEN: command run and relevant passing output after implementation
|
||||||
- Files changed
|
- Files changed
|
||||||
- Self-review findings (if any)
|
- Self-review findings (if any)
|
||||||
- Any issues or concerns
|
- Any issues or concerns
|
||||||
|
|||||||
@@ -329,6 +329,21 @@ function runTests() {
|
|||||||
assert.strictEqual(result.payload.length, 65536);
|
assert.strictEqual(result.payload.length, 65536);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects oversized 64-bit frames before payload allocation', () => {
|
||||||
|
const mask = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
const header = Buffer.alloc(14);
|
||||||
|
header[0] = 0x81; // FIN + TEXT
|
||||||
|
header[1] = 0x80 | 127; // masked, 64-bit length
|
||||||
|
header.writeBigUInt64BE(BigInt(ws.MAX_FRAME_PAYLOAD_BYTES) + 1n, 2);
|
||||||
|
mask.copy(header, 10);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => ws.decodeFrame(header),
|
||||||
|
/exceeds maximum allowed size/i,
|
||||||
|
'oversized advertised payload must be rejected from header alone'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ========== Close Frame with Status Code ==========
|
// ========== Close Frame with Status Code ==========
|
||||||
console.log('\n--- Close Frame Details ---');
|
console.log('\n--- Close Frame Details ---');
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ run_claude() {
|
|||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local timeout="${2:-60}"
|
local timeout="${2:-60}"
|
||||||
local allowed_tools="${3:-}"
|
local allowed_tools="${3:-}"
|
||||||
local output_file=$(mktemp)
|
local output_file
|
||||||
|
output_file="$(mktemp)"
|
||||||
|
|
||||||
# Build command as an argv array so timeout wraps claude directly.
|
# Build command as an argv array so timeout wraps claude directly.
|
||||||
local cmd=(claude -p "$prompt")
|
local cmd=(claude -p "$prompt")
|
||||||
@@ -74,7 +75,8 @@ assert_count() {
|
|||||||
local expected="$3"
|
local expected="$3"
|
||||||
local test_name="${4:-test}"
|
local test_name="${4:-test}"
|
||||||
|
|
||||||
local actual=$(echo "$output" | grep -c "$pattern" || echo "0")
|
local actual
|
||||||
|
actual="$(echo "$output" | grep -c "$pattern" || true)"
|
||||||
|
|
||||||
if [ "$actual" -eq "$expected" ]; then
|
if [ "$actual" -eq "$expected" ]; then
|
||||||
echo " [PASS] $test_name (found $actual instances)"
|
echo " [PASS] $test_name (found $actual instances)"
|
||||||
@@ -98,8 +100,10 @@ assert_order() {
|
|||||||
local test_name="${4:-test}"
|
local test_name="${4:-test}"
|
||||||
|
|
||||||
# Get line numbers where patterns appear
|
# Get line numbers where patterns appear
|
||||||
local line_a=$(echo "$output" | grep -n "$pattern_a" | head -1 | cut -d: -f1)
|
local line_a
|
||||||
local line_b=$(echo "$output" | grep -n "$pattern_b" | head -1 | cut -d: -f1)
|
local line_b
|
||||||
|
line_a="$(echo "$output" | grep -n "$pattern_a" | head -1 | cut -d: -f1 || true)"
|
||||||
|
line_b="$(echo "$output" | grep -n "$pattern_b" | head -1 | cut -d: -f1 || true)"
|
||||||
|
|
||||||
if [ -z "$line_a" ]; then
|
if [ -z "$line_a" ]; then
|
||||||
echo " [FAIL] $test_name: pattern A not found: $pattern_a"
|
echo " [FAIL] $test_name: pattern A not found: $pattern_a"
|
||||||
@@ -125,7 +129,8 @@ assert_order() {
|
|||||||
# Create a temporary test project directory
|
# Create a temporary test project directory
|
||||||
# Usage: test_project=$(create_test_project)
|
# Usage: test_project=$(create_test_project)
|
||||||
create_test_project() {
|
create_test_project() {
|
||||||
local test_dir=$(mktemp -d)
|
local test_dir
|
||||||
|
test_dir="$(mktemp -d)"
|
||||||
echo "$test_dir"
|
echo "$test_dir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ TEST_PROJECT=$(create_test_project)
|
|||||||
echo "Test project: $TEST_PROJECT"
|
echo "Test project: $TEST_PROJECT"
|
||||||
|
|
||||||
# Trap to cleanup
|
# Trap to cleanup
|
||||||
trap "cleanup_test_project $TEST_PROJECT" EXIT
|
cleanup_integration_test_project() {
|
||||||
|
cleanup_test_project "$TEST_PROJECT"
|
||||||
|
}
|
||||||
|
trap cleanup_integration_test_project EXIT
|
||||||
|
|
||||||
# Set up minimal Node.js project
|
# Set up minimal Node.js project
|
||||||
cd "$TEST_PROJECT"
|
cd "$TEST_PROJECT"
|
||||||
@@ -164,12 +167,19 @@ PLUGIN_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
|
|||||||
# other concurrent claude sessions.
|
# other concurrent claude sessions.
|
||||||
echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..."
|
echo "Running Claude (plugin-dir: $PLUGIN_DIR, cwd: $TEST_PROJECT)..."
|
||||||
echo "================================================================================"
|
echo "================================================================================"
|
||||||
cd "$TEST_PROJECT" && timeout 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions 2>&1 | tee "$OUTPUT_FILE" || {
|
set +e
|
||||||
|
(
|
||||||
|
cd "$TEST_PROJECT" &&
|
||||||
|
timeout 1800 claude -p "$PROMPT" --plugin-dir "$PLUGIN_DIR" --allowed-tools=all --permission-mode bypassPermissions
|
||||||
|
) 2>&1 | tee "$OUTPUT_FILE"
|
||||||
|
execution_status=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$execution_status" -ne 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "================================================================================"
|
echo "================================================================================"
|
||||||
echo "EXECUTION FAILED (exit code: $?)"
|
echo "EXECUTION FAILED (exit code: $execution_status)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
echo "================================================================================"
|
echo "================================================================================"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -47,16 +47,20 @@ assert_not_contains() {
|
|||||||
echo "=== Worktree Path Policy Test ==="
|
echo "=== Worktree Path Policy Test ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
assert_not_contains "$USING_SKILL" "~/.config/superpowers/worktrees" "using-git-worktrees does not mention old global path"
|
# Intentionally search for the literal legacy path, not the current user's home.
|
||||||
|
# shellcheck disable=SC2088
|
||||||
|
legacy_global_worktree_path="~/.config/superpowers/worktrees"
|
||||||
|
|
||||||
|
assert_not_contains "$USING_SKILL" "$legacy_global_worktree_path" "using-git-worktrees does not mention old global path"
|
||||||
assert_not_contains "$USING_SKILL" "global legacy" "using-git-worktrees does not use unclear global legacy shorthand"
|
assert_not_contains "$USING_SKILL" "global legacy" "using-git-worktrees does not use unclear global legacy shorthand"
|
||||||
assert_not_contains "$USING_SKILL" "Global path" "using-git-worktrees has no global path quick-reference row"
|
assert_not_contains "$USING_SKILL" "Global path" "using-git-worktrees has no global path quick-reference row"
|
||||||
assert_contains "$USING_SKILL" 'default to `.worktrees/` at the project root' "using-git-worktrees defaults new manual worktrees to .worktrees/"
|
assert_contains "$USING_SKILL" 'default to `.worktrees/` at the project root' "using-git-worktrees defaults new manual worktrees to .worktrees/"
|
||||||
|
|
||||||
assert_not_contains "$FINISHING_SKILL" "~/.config/superpowers/worktrees" "finishing-a-development-branch does not treat old global path as owned"
|
assert_not_contains "$FINISHING_SKILL" "$legacy_global_worktree_path" "finishing-a-development-branch does not treat old global path as owned"
|
||||||
assert_contains "$FINISHING_SKILL" '`.worktrees/` or `worktrees/`' "finishing-a-development-branch keeps project-local cleanup ownership"
|
assert_contains "$FINISHING_SKILL" '`.worktrees/` or `worktrees/`' "finishing-a-development-branch keeps project-local cleanup ownership"
|
||||||
|
|
||||||
assert_not_contains "$ROTOTILL_SPEC" "~/.config/superpowers/worktrees" "rototill spec does not preserve old global path policy"
|
assert_not_contains "$ROTOTILL_SPEC" "$legacy_global_worktree_path" "rototill spec does not preserve old global path policy"
|
||||||
assert_not_contains "$ROTOTILL_PLAN" "~/.config/superpowers/worktrees" "rototill plan does not preserve old global path policy"
|
assert_not_contains "$ROTOTILL_PLAN" "$legacy_global_worktree_path" "rototill plan does not preserve old global path policy"
|
||||||
assert_not_contains "$ROTOTILL_PLAN" "legacy path compat" "rototill plan does not advertise legacy path compatibility"
|
assert_not_contains "$ROTOTILL_PLAN" "legacy path compat" "rototill plan does not advertise legacy path compatibility"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ write_upstream_fixture() {
|
|||||||
|
|
||||||
mkdir -p \
|
mkdir -p \
|
||||||
"$repo/.codex-plugin" \
|
"$repo/.codex-plugin" \
|
||||||
|
"$repo/.kimi-plugin" \
|
||||||
"$repo/.private-journal" \
|
"$repo/.private-journal" \
|
||||||
"$repo/assets" \
|
"$repo/assets" \
|
||||||
"$repo/evals/drill" \
|
"$repo/evals/drill" \
|
||||||
@@ -210,6 +211,13 @@ EOF
|
|||||||
"name": "superpowers",
|
"name": "superpowers",
|
||||||
"version": "$MANIFEST_VERSION"
|
"version": "$MANIFEST_VERSION"
|
||||||
}
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$repo/.kimi-plugin/plugin.json" <<EOF
|
||||||
|
{
|
||||||
|
"name": "superpowers",
|
||||||
|
"version": "$MANIFEST_VERSION"
|
||||||
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
|
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
|
||||||
@@ -267,6 +275,7 @@ EOF
|
|||||||
|
|
||||||
git -C "$repo" add \
|
git -C "$repo" add \
|
||||||
.codex-plugin/plugin.json \
|
.codex-plugin/plugin.json \
|
||||||
|
.kimi-plugin/plugin.json \
|
||||||
.gitignore \
|
.gitignore \
|
||||||
assets/app-icon.png \
|
assets/app-icon.png \
|
||||||
assets/superpowers-small.svg \
|
assets/superpowers-small.svg \
|
||||||
@@ -415,10 +424,15 @@ EOF
|
|||||||
write_stale_ignored_destination_fixture() {
|
write_stale_ignored_destination_fixture() {
|
||||||
local repo="$1"
|
local repo="$1"
|
||||||
|
|
||||||
mkdir -p "$repo/plugins/superpowers/.private-journal"
|
mkdir -p \
|
||||||
|
"$repo/plugins/superpowers/.kimi-plugin" \
|
||||||
|
"$repo/plugins/superpowers/.private-journal"
|
||||||
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
|
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
|
||||||
|
printf '{"name":"stale-kimi"}\n' > "$repo/plugins/superpowers/.kimi-plugin/plugin.json"
|
||||||
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
|
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
|
||||||
git -C "$repo" add plugins/superpowers/.fixture-keep
|
git -C "$repo" add \
|
||||||
|
plugins/superpowers/.fixture-keep \
|
||||||
|
plugins/superpowers/.kimi-plugin/plugin.json
|
||||||
|
|
||||||
commit_fixture "$repo" "Initial stale ignored destination fixture"
|
commit_fixture "$repo" "Initial stale ignored destination fixture"
|
||||||
}
|
}
|
||||||
@@ -618,6 +632,7 @@ main() {
|
|||||||
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
|
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
|
||||||
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
|
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
|
||||||
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
|
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
|
||||||
|
assert_not_contains "$preview_section" ".kimi-plugin/plugin.json" "Preview excludes Kimi manifest from Codex sync"
|
||||||
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
|
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
|
||||||
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
|
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
|
||||||
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
|
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
|
||||||
@@ -644,6 +659,7 @@ main() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Convergence assertions..."
|
echo "Convergence assertions..."
|
||||||
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
|
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
|
||||||
|
assert_matches "$stale_preview_section" "\\*deleting +\\.kimi-plugin/plugin\\.json" "Preview deletes stale Kimi manifest from Codex plugin"
|
||||||
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
|
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
6
tests/kimi/run-tests.sh
Executable file
6
tests/kimi/run-tests.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
bash "$SCRIPT_DIR/test-plugin-manifest.sh"
|
||||||
86
tests/kimi/test-plugin-manifest.sh
Executable file
86
tests/kimi/test-plugin-manifest.sh
Executable file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
MANIFEST="$REPO_ROOT/.kimi-plugin/plugin.json"
|
||||||
|
|
||||||
|
python3 - "$MANIFEST" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
manifest_path = Path(sys.argv[1])
|
||||||
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
def assert_equal(actual, expected, label):
|
||||||
|
if actual != expected:
|
||||||
|
raise AssertionError(f"{label}: expected {expected!r}, got {actual!r}")
|
||||||
|
|
||||||
|
def assert_present(text, needle, label):
|
||||||
|
if needle not in text:
|
||||||
|
raise AssertionError(f"{label}: missing {needle!r}")
|
||||||
|
|
||||||
|
assert_equal(manifest.get("name"), "superpowers", "plugin name")
|
||||||
|
assert_equal(manifest.get("skills"), "./skills/", "skills path")
|
||||||
|
assert_equal(
|
||||||
|
manifest.get("sessionStart", {}).get("skill"),
|
||||||
|
"using-superpowers",
|
||||||
|
"sessionStart.skill",
|
||||||
|
)
|
||||||
|
|
||||||
|
instructions = manifest.get("skillInstructions")
|
||||||
|
if not isinstance(instructions, str) or not instructions.strip():
|
||||||
|
raise AssertionError("skillInstructions must be a non-empty string")
|
||||||
|
|
||||||
|
for token in [
|
||||||
|
"AskUserQuestion",
|
||||||
|
"TodoList",
|
||||||
|
"Agent",
|
||||||
|
"Skill",
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Bash",
|
||||||
|
"Grep",
|
||||||
|
"Glob",
|
||||||
|
"FetchURL",
|
||||||
|
"WebSearch",
|
||||||
|
]:
|
||||||
|
assert_present(instructions, token, "skillInstructions")
|
||||||
|
|
||||||
|
version_config = json.loads(
|
||||||
|
(manifest_path.parents[1] / ".version-bump.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
version_entries = version_config.get("files")
|
||||||
|
if not isinstance(version_entries, list):
|
||||||
|
raise AssertionError(".version-bump.json must contain files list")
|
||||||
|
|
||||||
|
if not any(
|
||||||
|
entry.get("path") == ".kimi-plugin/plugin.json" and entry.get("field") == "version"
|
||||||
|
for entry in version_entries
|
||||||
|
if isinstance(entry, dict)
|
||||||
|
):
|
||||||
|
raise AssertionError(
|
||||||
|
".version-bump.json must update .kimi-plugin/plugin.json version"
|
||||||
|
)
|
||||||
|
|
||||||
|
unsupported_fields = [
|
||||||
|
"tools",
|
||||||
|
"commands",
|
||||||
|
"hooks",
|
||||||
|
"apps",
|
||||||
|
"inject",
|
||||||
|
"configFile",
|
||||||
|
"config_file",
|
||||||
|
"bootstrap",
|
||||||
|
]
|
||||||
|
present_unsupported = sorted(field for field in unsupported_fields if field in manifest)
|
||||||
|
if present_unsupported:
|
||||||
|
raise AssertionError(
|
||||||
|
"unsupported Kimi runtime fields present: "
|
||||||
|
+ ", ".join(present_unsupported)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Kimi plugin manifest looks good")
|
||||||
|
PY
|
||||||
179
tests/shell-lint/test-lint-shell.sh
Normal file
179
tests/shell-lint/test-lint-shell.sh
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
SCRIPT_UNDER_TEST="$REPO_ROOT/scripts/lint-shell.sh"
|
||||||
|
|
||||||
|
FAILURES=0
|
||||||
|
TEST_ROOT="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TEST_ROOT"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
echo " [PASS] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo " [FAIL] $1"
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
|
||||||
|
pass "$description"
|
||||||
|
else
|
||||||
|
fail "$description"
|
||||||
|
echo " expected to find: $needle"
|
||||||
|
echo " in:"
|
||||||
|
printf '%s\n' "$haystack" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
|
||||||
|
fail "$description"
|
||||||
|
echo " did not expect to find: $needle"
|
||||||
|
echo " in:"
|
||||||
|
printf '%s\n' "$haystack" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
pass "$description"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_git_identity() {
|
||||||
|
local repo="$1"
|
||||||
|
|
||||||
|
git -C "$repo" config user.name "Test Bot"
|
||||||
|
git -C "$repo" config user.email "test@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_stub_tool() {
|
||||||
|
local path="$1"
|
||||||
|
local name="$2"
|
||||||
|
|
||||||
|
cat >"$path" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
{
|
||||||
|
printf '${name}:'
|
||||||
|
for arg in "\$@"; do
|
||||||
|
printf ' <%s>' "\$arg"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
} >> "\$SUPERPOWERS_SHELL_LINT_TEST_LOG"
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fixture_repo() {
|
||||||
|
local repo="$1"
|
||||||
|
|
||||||
|
git init -q -b main "$repo"
|
||||||
|
configure_git_identity "$repo"
|
||||||
|
|
||||||
|
mkdir -p "$repo/hooks"
|
||||||
|
cat >"$repo/tracked.sh" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "tracked"
|
||||||
|
EOF
|
||||||
|
cat >"$repo/hooks/session-start" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "extensionless"
|
||||||
|
EOF
|
||||||
|
cat >"$repo/README.md" <<'EOF'
|
||||||
|
# Fixture
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "not a shell script"
|
||||||
|
```
|
||||||
|
EOF
|
||||||
|
cat >"$repo/untracked.sh" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "untracked"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git -C "$repo" add tracked.sh hooks/session-start README.md
|
||||||
|
git -C "$repo" commit -q -m "fixture"
|
||||||
|
|
||||||
|
printf '\necho "changed"\n' >>"$repo/tracked.sh"
|
||||||
|
printf '\necho "changed extensionless"\n' >>"$repo/hooks/session-start"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_lint_shell() {
|
||||||
|
local repo="$1"
|
||||||
|
local fakebin="$2"
|
||||||
|
local log="$3"
|
||||||
|
shift 3
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$repo"
|
||||||
|
PATH="$fakebin:$PATH" \
|
||||||
|
SUPERPOWERS_SHELL_LINT_TEST_LOG="$log" \
|
||||||
|
bash "$SCRIPT_UNDER_TEST" "$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Shell lint script tests"
|
||||||
|
|
||||||
|
fixture="$TEST_ROOT/repo"
|
||||||
|
fakebin="$TEST_ROOT/bin"
|
||||||
|
log="$TEST_ROOT/tool.log"
|
||||||
|
mkdir -p "$fixture" "$fakebin"
|
||||||
|
: >"$log"
|
||||||
|
write_stub_tool "$fakebin/shellcheck" "shellcheck"
|
||||||
|
write_stub_tool "$fakebin/shfmt" "shfmt"
|
||||||
|
make_fixture_repo "$fixture"
|
||||||
|
|
||||||
|
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" 2>&1)"; then
|
||||||
|
pass "lint-shell check mode exits successfully with stub tools"
|
||||||
|
else
|
||||||
|
fail "lint-shell check mode exits successfully with stub tools"
|
||||||
|
printf '%s\n' "$output" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
tool_log="$(cat "$log")"
|
||||||
|
assert_contains "$output" "Linting 3 shell files" "reports changed shell file count"
|
||||||
|
assert_not_contains "$tool_log" "shfmt:" "does not run shfmt in lint mode"
|
||||||
|
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck"
|
||||||
|
assert_contains "$tool_log" "<--severity=warning>" "uses warning severity as the baseline"
|
||||||
|
assert_contains "$tool_log" "<--external-sources>" "allows ShellCheck to follow sourced files"
|
||||||
|
assert_contains "$tool_log" "<--source-path=SCRIPTDIR>" "resolves ShellCheck sources relative to each script"
|
||||||
|
assert_contains "$tool_log" "<hooks/session-start>" "includes changed extensionless shell shebang file"
|
||||||
|
assert_contains "$tool_log" "<tracked.sh>" "includes changed tracked .sh file"
|
||||||
|
assert_contains "$tool_log" "<untracked.sh>" "includes untracked shell files by default"
|
||||||
|
assert_not_contains "$tool_log" "README.md" "ignores Markdown with shell snippets"
|
||||||
|
|
||||||
|
: >"$log"
|
||||||
|
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" --all --format 2>&1)"; then
|
||||||
|
pass "lint-shell --format exits successfully with stub tools"
|
||||||
|
else
|
||||||
|
fail "lint-shell --format exits successfully with stub tools"
|
||||||
|
printf '%s\n' "$output" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
tool_log="$(cat "$log")"
|
||||||
|
assert_contains "$tool_log" "<-w>" "uses shfmt write mode with --format"
|
||||||
|
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck after --format"
|
||||||
|
assert_contains "$tool_log" "<--severity=warning>" "keeps warning severity after --format"
|
||||||
|
assert_contains "$tool_log" "<hooks/session-start>" "--all includes tracked extensionless shell shebang file"
|
||||||
|
assert_contains "$tool_log" "<tracked.sh>" "--all includes tracked .sh file"
|
||||||
|
assert_not_contains "$tool_log" "untracked.sh" "--all ignores untracked shell files"
|
||||||
|
|
||||||
|
if [[ "$FAILURES" -eq 0 ]]; then
|
||||||
|
echo "All shell lint script tests passed"
|
||||||
|
else
|
||||||
|
echo "$FAILURES shell lint script test(s) failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user