diff --git a/README.md b/README.md index a531752..a409469 100644 --- a/README.md +++ b/README.md @@ -104,21 +104,6 @@ Trello board health audit. Queries five dimensions of card data to identify thro /audit-trello board="My Board" since=2024-07-01 ``` -### `/compile-task` - -Compile one-off task descriptions into reusable, self-contained executables so the LLM doesn't redo deterministic work each time. Picks bash for POSIX shell ops, PowerShell for Windows-native work, or single-file `uv` Python (PEP 723 inline deps) for anything needing libraries. Scripts are registered in an appdata-backed catalogue and dispatched through a `cscript` binary the skill installs to a user-writable directory on `PATH`. From the second invocation onward the agent finds the script via `cscript which`, confirms with you, and runs it directly — no regeneration. - -Ships with a `cscript` dispatcher (`list`, `which`, `run`, `show`, `edit`, `rm`, `state-dir`, `where`, `register`), a Windows `cscript.cmd` wrapper, and a smoke-test script. Verified on macOS and Linux; Windows support is implemented (subprocess dispatch + `cscript.cmd` wrapper + PowerShell language option) and awaits validation by a Windows user. - -**Arguments:** None — describe the task in natural language and the skill decides whether to compile it, match an existing script, or skip. - -**Usage:** -``` -/compile-task rename JPGs in this directory by the EXIF date they were shot -/compile-task pull all comments from GitHub PR https://github.com/foo/bar/pull/42 as markdown -/compile-task strip EXIF from every image under a folder -``` - ### `/workflow-advisor` Interview-driven process automation for software teams. Helps adopt and run spec-driven development and related practices through a progressive team interview, a versioned `.workflow/` configuration, GitHub workflow generation, event playbooks, lifecycle gates, process reports, and reconcile loops for pushes, pull requests, issues, and comments. diff --git a/skills/compile-task/README.md b/skills/compile-task/README.md deleted file mode 100644 index 83f5007..0000000 --- a/skills/compile-task/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# compile-task - -Compile one-off LLM tasks into reusable, self-contained executables so you stop paying tokens to redo deterministic work. - -## What it does - -When you describe a task ("rename these photos by their EXIF date", "pull this GitHub issue's comments as markdown"), the skill: - -1. Checks a catalogue of previously-compiled scripts and offers to re-run an existing one if it matches. -2. Otherwise picks the simplest language for the job — **bash** for POSIX shell ops, **PowerShell** for Windows-native or shell-y tasks on Windows, **single-file uv Python** (PEP 723 inline deps) as the cross-platform default for anything needing libraries. -3. Writes the script, smoke-tests it, registers it in the catalogue, and runs it. - -From then on, the script is one command: - -```sh -cscript run rename-by-exif-date ./photos -``` - -## The `cscript` dispatcher - -A single Python script installed on first use into a user-writable directory on `PATH` (on Windows, paired with a small `cscript.cmd` wrapper). It stores everything in an OS-correct appdata directory: `~/Library/Application Support/cscript/` on macOS, `~/.local/share/cscript/` on Linux, `%LOCALAPPDATA%\cscript\` on Windows. Bash scripts on Windows require Git Bash or WSL. - -``` -cscript list # show all registered scripts -cscript which "" # fuzzy-match catalogue -cscript run [--yes] [args] # execute (prompts on TTY unless --yes) -cscript show # print source + metadata -cscript edit # open in $EDITOR -cscript rm # archive and unregister -cscript state-dir # print per-script state dir -cscript where # print the data directory -``` - -## Design choices - -- **Self-contained scripts.** No project-local imports, no relative paths. A script written for one repo can run from anywhere. -- **One catalogue, one PATH entry.** Instead of dumping 30 scripts into `~/.local/bin`, you get one `cscript` binary and tab-completable subcommands. -- **Confirm before running.** The skill always shows the matched script and asks before executing, including for previously-registered scripts. Wrong matches don't clobber files. -- **Archive, never delete.** `cscript rm` and re-registration both move the prior version into `scripts/.archive/.` rather than deleting. -- **Eat your own dog food.** The dispatcher itself is a `uv` single-file script with PEP 723 inline deps — exactly the pattern it produces. - -## When the skill stays out - -It won't compile one-off explorations, judgement-laden tasks (refactoring, PR descriptions), one-line shell commands, or anything tightly coupled to the current repo. For those, the LLM answers directly. - -## Usage - -``` -/compile-task rename JPGs in this directory by the EXIF date they were shot -/compile-task pull all comments from GitHub PR https://github.com/foo/bar/pull/42 as markdown -/compile-task strip EXIF from every image under a folder -``` - -Or just describe a task and let the skill decide whether to compile it. diff --git a/skills/compile-task/SKILL.md b/skills/compile-task/SKILL.md deleted file mode 100644 index 36817b8..0000000 --- a/skills/compile-task/SKILL.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -name: compile-task -description: | - Compile a one-off task description into a reusable, self-contained - executable script so the LLM doesn't have to do the same work again. - Picks bash for trivial file/shell ops, single-file uv Python (PEP 723) - for anything needing libraries. Registers scripts in an OS-correct - appdata directory and exposes them through a `cscript` dispatcher - installed on PATH. Future invocations match the description against - the index and re-run the existing script instead of regenerating. - Use when the user says "compile this", "make a script for X", - "stash this as a script", "save this so I don't have to ask again", - or describes a task that sounds like it will recur. -user-invocable: true ---- - -# compile-task — turn prompts into reusable executables - -The point of this skill is to **stop paying an LLM to redo deterministic work**. Each time the user describes a task that could be a script, compile it once, register it in the catalogue, and run the registered script from then on. Generation is the exception; execution is the rule. - -## Hard rules - -1. **Check the catalogue first.** Before designing anything, run `cscript which ""` and `cscript list`. If a registered script matches the intent, propose running it. Never silently regenerate something that already exists. -2. **Confirm before running.** Show the matched script's name, description, and the exact command you're about to run. Wait for user confirmation. Apply this to both freshly generated and previously registered scripts. Auto-run is never on. -3. **Self-contained.** Generated scripts must run on their own — no project-local imports, no relative paths, no assumed cwd. Bash uses only POSIX tools (or tools you've checked are installed). Python uses PEP 723 inline deps so `uv run` handles everything. -4. **One file.** No sibling helpers, no companion config files. If a script needs persistent state, ask the dispatcher for its slot: `cscript state-dir ` prints (and creates on first call) a per-script directory inside the appdata dir. -5. **`--help` is mandatory.** Every script supports `-h`/`--help` and exits non-zero on missing required args with a usage message. -6. **Idempotent registration.** Re-registering the same `--name` archives the old version into `scripts/.archive/.` and replaces it. Never silently overwrite. -7. **Don't generate destructive scripts without `--dry-run`.** If a script deletes, force-pushes, drops a table, sends an email, or modifies shared state, it must support `--dry-run` and default-print-what-it-would-do for unfamiliar inputs. Pass `--read-only` to `cscript register` only for scripts that truly cannot modify state (the dispatcher tags those with `[ro]` in `list`); leave it off for everything else. - -## Workflow - -### 1. Bootstrap (first run only) - -In order, before anything else: - -1. **Check `uv` is installed.** Run `command -v uv >/dev/null 2>&1`. If missing, stop and tell the user to install it (`curl -LsSf https://astral.sh/uv/install.sh | sh` on macOS/Linux, or via Homebrew). The dispatcher's shebang is `#!/usr/bin/env -S uv run --script`, so nothing else will work without it. - -2. **Check whether the dispatcher is installed and on PATH.** Run `command -v cscript >/dev/null 2>&1`. - -3. **If `cscript` is missing, install it.** The source lives at `scripts/cscript` relative to this `SKILL.md`. Resolve the path from wherever this file was loaded; if you cannot find it, ask the user for the skill directory rather than guessing. - - Pick a destination directory that is **user-writable and already on `PATH`**: - - - Inspect existing `PATH` entries under the user's home directory and prefer one they already maintain. - - If none is on `PATH`, ask the user where to put the binary rather than picking one for them. - - Then install per OS: - - - **macOS / Linux:** copy `scripts/cscript` to `/cscript` and make it executable. - - **Windows:** copy **both** `scripts/cscript` and `scripts/cscript.cmd` into the chosen directory. Windows resolves `cscript.cmd` via `PATHEXT`; the wrapper hands the extensionless source to `uv run --script`. Note: Windows ships `cscript.exe` (Windows Script Host) in `System32`; for our `cscript.cmd` to win, the chosen directory must appear in `PATH` before `C:\Windows\System32`. Verify by running this in PowerShell (substitute the chosen directory): - - ```powershell - $paths = $env:Path -split ';' - $user = $paths.IndexOf('') - $sys = $paths.IndexOf("$env:SystemRoot\System32") - if ($user -lt 0) { 'chosen dir not on PATH' } - elseif ($sys -ge 0 -and $user -gt $sys) { 'PATH ordering would let cscript.exe shadow cscript.cmd' } - else { 'OK' } - ``` - - If the check reports anything other than `OK`, tell the user how to fix it (add/reorder the entry in User PATH) and stop until they confirm. - -4. **Verify the install worked.** After copying, run `cscript --help` in a fresh shell invocation. If it does not resolve, the chosen directory is not on `PATH` in interactive shells — tell the user how to add it for their shell and stop. Do not proceed until they confirm `cscript --help` works. - -The dispatcher itself is a uv single-file script — it bootstraps its own Python deps the first time it runs. - -### 2. Match against the catalogue - -```sh -cscript which "" -cscript list -``` - -`cscript which` prints up to five ranked candidates. Treat the top result as a **clear match** only if both hold: - -- the top hit's first column (the name) reads as a plausible verb-object for the request, *and* -- the next candidate (if any) is obviously about a different task — not just a near-duplicate score. - -If clear match: confirm the script + args with the user, then `cscript run [args...]`. Done. - -If ambiguous (multiple plausible candidates) or no hits: show what you found and either ask which to use or proceed to step 3. - -### 3. Design (only if no match) - -Pick the language using this decision rule, in order: - -1. **PowerShell** — if the task is Windows-native (registry, services, COM, Windows-specific filesystem APIs) **or** if the user is on Windows without Git Bash/WSL and the task is shell-y enough that bash would be the POSIX choice. `pwsh` (PowerShell Core) runs on macOS and Linux too, so this is also fine for cross-platform shell tasks if the user already uses PowerShell. -2. **Bash** — if the task is purely file system, git, text munging with standard Unix tools (`jq`, `awk`, `sed`, `grep`, `find`, `rsync`), or a thin wrapper around an existing CLI the user has. No HTTP, no parsing structured formats beyond what `jq`/`yq` handle. On Windows, bash scripts need Git Bash or WSL. -3. **uv Python** — anything else. HTTP, HTML/XML parsing, image processing, anything pulling a library, anything where bash quoting would make you cry. The cross-platform default when in doubt. - -Pick a name: short verb-object, kebab-case, no language suffix. `resize-pngs`, not `resize_pngs.sh`. `rename-by-exif-date`, not `photo_renamer.py`. The dispatcher hides the extension. - -Read the matching template from `references/` before writing: - -- `references/bash-template.sh` -- `references/uv-python-template.py` -- `references/powershell-template.ps1` - -All three templates have the structural skeleton (shebang, header comment with NAME/DESC/USAGE, `--help`/`-Help`, arg parsing, error handling). Fill in the implementation only. - -### 4. Write and smoke-test - -- Write the script to a temp path. -- Smoke test: run it with `--help` (must exit 0 and print usage) and with no args (must exit non-zero with usage to stderr). -- If the task is read-only and the user supplied inputs, run it once on those inputs in the temp location and show the output. -- If the task is destructive, do a `--dry-run` first. - -### 5. Register - -```sh -cscript register \ - --source \ - --name \ - --description "" \ - --language \ - --args-help "" \ - [--read-only] -``` - -The dispatcher derives the filename automatically (`.sh` for bash, `.py` for python, `.ps1` for powershell), moves the file from `--source` into the appdata `scripts/` directory, makes it executable on POSIX, archives any prior version, and prints the final path. - -### 6. Run - -Always run the freshly registered script through the dispatcher, never by direct path. Pass `--yes` since you have already confirmed the run with the user: - -```sh -cscript run --yes [args...] -``` - -Direct (human) invocations omit `--yes`; the dispatcher will then prompt before running non-read-only scripts on a TTY. This proves the dispatcher works and gives the user the muscle-memory invocation they'll use next time. - -### 7. Report - -Tell the user, in one or two sentences: - -- The script's name and what it does. -- How to invoke it next time: `cscript run ...` (and `cscript list` to see everything). -- Whether it's marked read-only. - -Do not paste the full source. They can `cscript show ` if they want it. - -## Dispatcher reference - -The dispatcher is installed wherever the user keeps personal binaries on `PATH` (see bootstrap). It stores everything under the OS appdata directory (resolved via `platformdirs.user_data_dir("cscript")`, or `$CSCRIPT_DATA_DIR` if set): - -- macOS: `~/Library/Application Support/cscript/` -- Linux: `~/.local/share/cscript/` -- Windows: `%LOCALAPPDATA%\cscript\` - -Subcommands: - -| Command | What it does | -| --- | --- | -| `cscript list` | List all registered scripts with descriptions. | -| `cscript which ` | Fuzzy match across names and descriptions. Used by this skill before generating. | -| `cscript run [--yes] [args...]` | Execute the registered script. Prompts before non-read-only runs on a TTY unless `--yes`. Args after the name are passed through. | -| `cscript show ` | Print the script's source plus its index metadata. | -| `cscript edit ` | Open the script in `$EDITOR`. | -| `cscript rm ` | Archive the file to `scripts/.archive/` and drop its index entry and state directory. | -| `cscript state-dir ` | Print (creating if missing) the per-script state directory under the appdata dir. | -| `cscript where` | Print the data directory path. | -| `cscript register …` | Used by this skill at compile time; not normally hand-invoked. | - -## Regeneration - -When the user asks to "redo", "rewrite", or "regenerate" an existing script (or when running it reveals a bug): - -1. `cscript show ` to see the current source. -2. Decide whether the rewrite is small enough to edit in place (`cscript edit`) or large enough to regenerate. -3. For full regeneration, write the new version, then `cscript register --name …`. The dispatcher archives the previous version. - -Do not invent new names like `resize-pngs-v2`. Keep one name per task; let the archive hold history. - -## When NOT to compile a task - -- **One-off explorations.** "Show me the top 10 largest files in this dir." Just answer it. If the user runs it twice, then compile. -- **Tasks that need judgement.** "Refactor this function." "Write a PR description." LLM judgement is the value; a script would be wrong. -- **Anything that's a one-line shell command.** `du -sh * | sort -h | tail` doesn't need a script. -- **Tasks tightly coupled to the current repo or cwd.** If the script wouldn't make sense in a different project, it belongs as a project script (committed in the repo), not in the global catalogue. - -If you're unsure whether a task is worth compiling, ask the user: "Want me to stash this as a `cscript` so it's one command next time?" — then proceed or not based on their answer. diff --git a/skills/compile-task/references/bash-template.sh b/skills/compile-task/references/bash-template.sh deleted file mode 100644 index e61467b..0000000 --- a/skills/compile-task/references/bash-template.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# NAME: example-name -# DESC: One-line description in present tense, no trailing period -# USAGE: example-name [--flag] -# -# Replace this header when generating. Keep NAME / DESC / USAGE — `cscript show` -# prints them above the source. The DESC line is what `cscript which` matches. - -set -euo pipefail - -usage() { - cat < [--flag] - -Description here. - -Args: - required-arg What it is - -Flags: - --flag What it does - -h, --help Show this help -EOF -} - -# Parse args explicitly. `getopts` doesn't do long flags; write the loop. -flag=0 -required="" -while (($#)); do - case "$1" in - -h|--help) usage; exit 0 ;; - --flag) flag=1; shift ;; - --) shift; break ;; - -*) echo "Unknown flag: $1" >&2; usage >&2; exit 2 ;; - *) - if [[ -z "$required" ]]; then required="$1"; shift - else echo "Unexpected arg: $1" >&2; usage >&2; exit 2 - fi ;; - esac -done - -if [[ -z "$required" ]]; then - usage >&2 - exit 2 -fi - -# --- implementation below --- diff --git a/skills/compile-task/references/powershell-template.ps1 b/skills/compile-task/references/powershell-template.ps1 deleted file mode 100644 index 0776cf1..0000000 --- a/skills/compile-task/references/powershell-template.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env pwsh -# NAME: example-name -# DESC: One-line description in present tense, no trailing period -# USAGE: example-name [-Flag] -# -# Replace this header when generating. Keep NAME / DESC / USAGE — `cscript show` -# prints them above the source. The DESC line is what `cscript which` matches. - -[CmdletBinding()] -param( - [Parameter(Position = 0)] - [string]$Required, - - [switch]$Flag, - - [Alias('h')] - [switch]$Help -) - -$ErrorActionPreference = 'Stop' - -function Show-Usage { - @' -Usage: example-name [-Flag] - -Description here. - -Args: - required-arg What it is - -Flags: - -Flag What it does - -h, -Help Show this help -'@ -} - -if ($Help) { - Show-Usage - exit 0 -} - -if (-not $Required) { - [Console]::Error.WriteLine((Show-Usage)) - exit 2 -} - -# --- implementation below --- diff --git a/skills/compile-task/references/uv-python-template.py b/skills/compile-task/references/uv-python-template.py deleted file mode 100644 index 91c8654..0000000 --- a/skills/compile-task/references/uv-python-template.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# # Add runtime deps here, e.g. "requests>=2.32", "pillow>=10.4". -# # `uv run` resolves and caches these the first time the script runs. -# ] -# /// -"""NAME: example-name -DESC: One-line description in present tense, no trailing period -USAGE: example-name [--flag] - -Keep the NAME / DESC / USAGE lines at the top of the docstring. `cscript show` -surfaces them above the source. The DESC line is what `cscript which` matches. -""" - -from __future__ import annotations - -import argparse -import sys - - -def build_parser() -> argparse.ArgumentParser: - p = argparse.ArgumentParser( - prog="example-name", - description="One-line description.", - ) - p.add_argument("required", help="What this positional is for.") - p.add_argument("--flag", action="store_true", help="What this flag does.") - return p - - -def main(argv: list[str] | None = None) -> int: - args = build_parser().parse_args(argv) - # --- implementation below --- - _ = args - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/skills/compile-task/scripts/cscript b/skills/compile-task/scripts/cscript deleted file mode 100755 index 38558b4..0000000 --- a/skills/compile-task/scripts/cscript +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.11" -# dependencies = ["platformdirs>=4.2"] -# /// -"""cscript — dispatcher for scripts created by the compile-task skill. - -Stores generated scripts in an OS-correct appdata directory and exposes -them through a small set of subcommands. See `cscript --help` for usage. -""" - -from __future__ import annotations - -import argparse -from datetime import datetime, timezone -import json -import os -from pathlib import Path -import shutil -import subprocess -import sys - -from platformdirs import user_data_dir - -APP = "cscript" -DATA = Path(os.environ.get("CSCRIPT_DATA_DIR") or user_data_dir(APP)) -SCRIPTS = DATA / "scripts" -ARCHIVE = SCRIPTS / ".archive" -STATE = DATA / "state" -INDEX = DATA / "index.json" - -EXT = {"bash": ".sh", "python": ".py", "powershell": ".ps1"} - - -def ensure_dirs() -> None: - SCRIPTS.mkdir(parents=True, exist_ok=True) - ARCHIVE.mkdir(parents=True, exist_ok=True) - if not INDEX.exists(): - INDEX.write_text("[]\n") - - -def load_index() -> list[dict]: - ensure_dirs() - raw = INDEX.read_text().strip() or "[]" - return json.loads(raw) - - -def save_index(items: list[dict]) -> None: - INDEX.write_text(json.dumps(items, indent=2) + "\n") - - -def resolve(name: str) -> dict | None: - """Find a script by exact or unambiguous partial name. - - Prints a helpful error to stderr on miss or ambiguity. Returns the - matching entry or None. - """ - items = load_index() - for item in items: - if item["name"] == name: - return item - partial = [i for i in items if name in i["name"]] - if not partial: - print(f"cscript: no script matching {name!r}", file=sys.stderr) - return None - if len(partial) == 1: - return partial[0] - print(f"cscript: {name!r} is ambiguous; candidates:", file=sys.stderr) - width = max(len(i["name"]) for i in partial) - for item in sorted(partial, key=lambda x: x["name"]): - print(f" {item['name']:<{width}} {item['description']}", file=sys.stderr) - return None - - -def cmd_list(_args: argparse.Namespace) -> int: - items = load_index() - if not items: - print("No scripts registered yet. Use the /compile-task skill to create one.") - return 0 - width = max(len(i["name"]) for i in items) - for item in sorted(items, key=lambda x: x["name"]): - tag = " [ro]" if item.get("read_only") else "" - print(f" {item['name']:<{width}} {item['description']}{tag}") - return 0 - - -def cmd_which(args: argparse.Namespace) -> int: - items = load_index() - query = args.query.lower() - tokens = [t for t in query.split() if t] - scored: list[tuple[int, dict]] = [] - for item in items: - score = 0 - hay_name = item["name"].lower() - hay_desc = item["description"].lower() - for tok in tokens: - if tok in hay_name: - score += 3 - if tok in hay_desc: - score += 1 - if score: - scored.append((score, item)) - if not scored: - return 1 - scored.sort(reverse=True, key=lambda x: x[0]) - width = max(len(s[1]["name"]) for s in scored[:5]) - for _, item in scored[:5]: - print(f" {item['name']:<{width}} {item['description']}") - return 0 - - -def confirm_run(item: dict) -> bool: - """Prompt the user before running a non-read-only script on a TTY. - - Returns True if the user agreed (or no prompt was needed), False if - they declined. - """ - if item.get("read_only"): - return True - if not sys.stdin.isatty() or not sys.stderr.isatty(): - return True - prompt = f"Run {item['name']!r} ({item['description']})? [y/N] " - try: - answer = input(prompt).strip().lower() - except EOFError: - answer = "" - return answer in ("y", "yes") - - -def build_invocation(path: Path, language: str, passthrough: list[str]) -> list[str]: - """Return the argv for running `path` as `language`, with `passthrough` appended.""" - if sys.platform == "win32": - if language == "python" or path.suffix == ".py": - return ["uv", "run", "--script", str(path), *passthrough] - if language == "bash" or path.suffix == ".sh": - # Requires Git Bash, WSL, or another bash on PATH. - return ["bash", str(path), *passthrough] - if language == "powershell" or path.suffix == ".ps1": - return [ - "pwsh", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - str(path), - *passthrough, - ] - return [str(path), *passthrough] - - -def cmd_run(args: argparse.Namespace) -> int: - item = resolve(args.name) - if item is None: - return 2 - path = Path(item["path"]) - if not path.exists(): - print(f"cscript: script file missing: {path}", file=sys.stderr) - return 2 - if not args.yes and not confirm_run(item): - print("cscript: aborted", file=sys.stderr) - return 1 - if sys.platform != "win32": - if not os.access(path, os.X_OK): - path.chmod(0o755) - os.execv(str(path), [str(path), *args.args]) - argv = build_invocation(path, item.get("language", ""), args.args) - try: - return subprocess.run(argv).returncode - except FileNotFoundError as exc: - print(f"cscript: {exc.filename or argv[0]} not found on PATH", file=sys.stderr) - return 127 - - -def cmd_show(args: argparse.Namespace) -> int: - item = resolve(args.name) - if item is None: - return 2 - print(f"# name: {item['name']}") - print(f"# description: {item['description']}") - print(f"# language: {item['language']}") - print(f"# created: {item['created']}") - print(f"# path: {item['path']}") - if item.get("args_help"): - print(f"# usage: {item['args_help']}") - print() - print(Path(item["path"]).read_text(encoding="utf-8"), end="") - return 0 - - -def cmd_edit(args: argparse.Namespace) -> int: - item = resolve(args.name) - if item is None: - return 2 - editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi" - return subprocess.call([editor, item["path"]]) - - -def cmd_rm(args: argparse.Namespace) -> int: - item = resolve(args.name) - if item is None: - return 2 - src = Path(item["path"]) - if src.exists(): - ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") - dest = ARCHIVE / f"{item['name']}.{ts}{src.suffix}" - shutil.move(str(src), str(dest)) - state_dir = STATE / item["name"] - if state_dir.exists(): - shutil.rmtree(state_dir) - items = [i for i in load_index() if i["name"] != item["name"]] - save_index(items) - print(f"Removed {item['name']} (archived under {ARCHIVE})") - return 0 - - -def cmd_state_dir(args: argparse.Namespace) -> int: - item = resolve(args.name) - if item is None: - return 2 - state_dir = STATE / item["name"] - state_dir.mkdir(parents=True, exist_ok=True) - print(state_dir) - return 0 - - -def cmd_register(args: argparse.Namespace) -> int: - ensure_dirs() - src = Path(args.source).expanduser().resolve() - if not src.is_file(): - print(f"cscript: --source not a file: {src}", file=sys.stderr) - return 2 - filename = args.filename or f"{args.name}{EXT[args.language]}" - dest = SCRIPTS / filename - if dest.exists() and dest.resolve() != src: - ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") - shutil.move(str(dest), str(ARCHIVE / f"{args.name}.{ts}{dest.suffix}")) - if src != dest: - shutil.move(str(src), str(dest)) - dest.chmod(0o755) - items = [i for i in load_index() if i["name"] != args.name] - items.append( - { - "name": args.name, - "description": args.description, - "language": args.language, - "filename": filename, - "path": str(dest), - "created": datetime.now(timezone.utc).isoformat(), - "args_help": args.args_help or "", - "read_only": bool(args.read_only), - } - ) - save_index(items) - print(dest) - return 0 - - -def cmd_where(_args: argparse.Namespace) -> int: - print(DATA) - return 0 - - -def build_parser() -> argparse.ArgumentParser: - summary = (__doc__ or "cscript").splitlines()[0] - p = argparse.ArgumentParser(prog="cscript", description=summary) - sub = p.add_subparsers(dest="cmd", required=True) - - sub.add_parser("list", help="list registered scripts") - sub.add_parser("where", help="print the data directory") - - pw = sub.add_parser("which", help="fuzzy match against names/descriptions") - pw.add_argument("query") - - pr = sub.add_parser( - "run", - help="execute a registered script", - description="Execute a registered script. Place --yes BEFORE the name; " - "anything after the name is forwarded to the script.", - ) - pr.add_argument( - "--yes", "-y", action="store_true", help="skip the confirm prompt for non-read-only scripts" - ) - pr.add_argument("name") - pr.add_argument("args", nargs=argparse.REMAINDER) - - ps = sub.add_parser("show", help="print a script's source and metadata") - ps.add_argument("name") - - pe = sub.add_parser("edit", help="open a script in $EDITOR") - pe.add_argument("name") - - prm = sub.add_parser("rm", help="archive and unregister a script") - prm.add_argument("name") - - psd = sub.add_parser( - "state-dir", help="print (creating if missing) the per-script state directory" - ) - psd.add_argument("name") - - preg = sub.add_parser("register", help="(used by the compile-task skill)") - preg.add_argument("--source", required=True, help="path to the freshly-written script file") - preg.add_argument("--name", required=True) - preg.add_argument("--description", required=True) - preg.add_argument("--language", required=True, choices=tuple(EXT)) - preg.add_argument( - "--filename", default="", help="override the auto-derived .{sh,py} filename" - ) - preg.add_argument("--args-help", default="") - preg.add_argument("--read-only", action="store_true") - - return p - - -def main(argv: list[str] | None = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - handlers = { - "list": cmd_list, - "which": cmd_which, - "run": cmd_run, - "show": cmd_show, - "edit": cmd_edit, - "rm": cmd_rm, - "state-dir": cmd_state_dir, - "register": cmd_register, - "where": cmd_where, - } - return handlers[args.cmd](args) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/skills/compile-task/scripts/cscript.cmd b/skills/compile-task/scripts/cscript.cmd deleted file mode 100644 index 877a006..0000000 --- a/skills/compile-task/scripts/cscript.cmd +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -rem Windows wrapper for the cscript dispatcher. -rem Install BOTH this file and the extensionless `cscript` source into the -rem same directory on PATH. Windows resolves cscript.cmd via PATHEXT; this -rem then hands the PEP 723 source to `uv run --script`. -uv run --script "%~dp0cscript" %* diff --git a/skills/compile-task/scripts/smoke-test.sh b/skills/compile-task/scripts/smoke-test.sh deleted file mode 100755 index 119b457..0000000 --- a/skills/compile-task/scripts/smoke-test.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash -# Smoke test for the cscript dispatcher. Exercises register/list/which/ -# state-dir/show/run/rm in an isolated data directory and exits non-zero -# on the first failure. - -set -euo pipefail - -HERE="$(cd "$(dirname "$0")" && pwd)" -CS="$HERE/cscript" -CSCRIPT_DATA_DIR="$(mktemp -d)" -export CSCRIPT_DATA_DIR -TMP="$(mktemp -d)" -trap 'rm -rf "$CSCRIPT_DATA_DIR" "$TMP"' EXIT - -fail() { echo "FAIL: $*" >&2; exit 1; } -pass() { echo "ok: $*"; } - -# 1. Empty index -out=$("$CS" list) -[[ "$out" == *"No scripts registered"* ]] || fail "empty list message" -pass "empty list" - -if "$CS" which "nothing matches" >/dev/null 2>&1; then - fail "which on empty index should exit non-zero" -fi -pass "which on empty index returns non-zero" - -# 2. Register a script -cat > "$TMP/hello.sh" <<'EOF' -#!/usr/bin/env bash -echo "hello $*" -EOF -chmod +x "$TMP/hello.sh" - -"$CS" register \ - --source "$TMP/hello.sh" \ - --name hello \ - --description "say hello (smoke test)" \ - --language bash \ - --read-only >/dev/null -pass "register" - -out=$("$CS" list) -[[ "$out" == *"hello"*"[ro]"* ]] || fail "list should show hello with [ro] tag" -pass "list shows registered script" - -out=$("$CS" which "hello") -[[ "$out" == *"hello"* ]] || fail "which should match hello" -pass "which finds match" - -# 3. State directory -state_dir=$("$CS" state-dir hello) -[[ -d "$state_dir" ]] || fail "state-dir should create the directory" -pass "state-dir creates and prints path" - -# 4. Show -out=$("$CS" show hello) -[[ "$out" == *"name:"*"hello"* ]] || fail "show should print metadata header" -[[ "$out" == *"echo \"hello"* ]] || fail "show should print source" -pass "show" - -# 5. Run (read-only script; no TTY needed since --read-only skips confirm) -# Subshell so os.execv only replaces the subshell process. -out=$( "$CS" run hello world ) -[[ "$out" == "hello world" ]] || fail "run output mismatch: '$out'" -pass "run executes and forwards args" - -# 6. Re-register archives prior version -cat > "$TMP/hello.sh" <<'EOF' -#!/usr/bin/env bash -echo "hello v2 $*" -EOF -chmod +x "$TMP/hello.sh" -"$CS" register \ - --source "$TMP/hello.sh" \ - --name hello \ - --description "say hello v2" \ - --language bash \ - --read-only >/dev/null -out=$( "$CS" run hello world ) -[[ "$out" == "hello v2 world" ]] || fail "re-registered script not active" -archived=$(find "$CSCRIPT_DATA_DIR/scripts/.archive" -name 'hello.*' | wc -l | tr -d ' ') -[[ "$archived" -ge 1 ]] || fail "prior version should be archived" -pass "re-register archives prior version" - -# 7. Ambiguous resolve -cat > "$TMP/hello-again.sh" <<'EOF' -#!/usr/bin/env bash -echo "again" -EOF -chmod +x "$TMP/hello-again.sh" -"$CS" register \ - --source "$TMP/hello-again.sh" \ - --name hello-again \ - --description "another hello" \ - --language bash \ - --read-only >/dev/null - -if "$CS" show hell >/dev/null 2>&1; then - fail "ambiguous show 'hell' should fail" -fi -err=$("$CS" show hell 2>&1 || true) -[[ "$err" == *"ambiguous"* ]] || fail "ambiguous error message missing" -pass "ambiguous match prints candidates and exits non-zero" - -# 8. rm cleans state dir -"$CS" rm hello >/dev/null -[[ ! -d "$state_dir" ]] || fail "rm should remove the state directory" -out=$("$CS" list) -[[ "$out" != *"say hello v2"* ]] || fail "list should not contain removed script" -[[ "$out" == *"hello-again"* ]] || fail "list should still contain hello-again" -pass "rm archives script and removes state dir" - -echo -echo "All checks passed."