From a2f141daf501c18aa5bf01ca353a777de5c4c032 Mon Sep 17 00:00:00 2001 From: Yi-Ting Chiu Date: Sun, 26 Jul 2026 01:22:20 -0700 Subject: [PATCH 01/40] =?UTF-8?q?fix:=20design-audit=20round=201=20?= =?UTF-8?q?=E2=80=94=20write-back=20unification,=20secret=20heuristic,=20c?= =?UTF-8?q?ontract=20repairs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from a full-codebase design audit, worst first: - The TUI add-review panel wrote parameter blocks back with read_text/write_text, silently rewriting every line ending of the just-stored copy (CRLF-ifying it on Windows) while its four sibling write-back sites each hand-rolled the correct bytes discipline. All six sites now share one pair: rewrite.read_for_block_edit / write_block_edit (surrogateescape, LF fold, newline restore, atomic + keep-mode). The python onboarding lane's strict-decode traceback on non-UTF-8 input and the --normalize lane's non-atomic write go away with it. - is_secret_name matched substrings, so --max-tokens (TOKEN) became a permanent password field on reader-reflected forms with no override anywhere. Whole-word matching (snake/kebab/camel/sentence-aware) fixes the false positives at the source for every lane. - skit remove prompted inside pipes/CI, violating the non-interactive contract its own sibling runner remove implements; it now takes --no-input and refuses with exit 2, and preset delete (unrecoverable user data, previously deleted with no ask) gets the same confirmation ceremony. SKILL.md and the docs now tell the same story. - The remembered extra-args tail replayed under two different expansion regimes (CLI literal, TUI token/glob-expanded). The tail's provenance is now recorded (argstate extra_args_raw) and every replay follows it in both faces. - The Library detail pane rebuilt the form plan on every cursor move — a synchronous pwsh spawn per RowHighlighted for PowerShell entries; plans are now mtime-cached (the old drift cache generalized), and the add-review panel memoizes its reader-modeled probe. - Ctrl+O meant three things on three sibling screens while README documents one: Choose variables moves to Ctrl+L (both screens), Preferences' squatters move to Ctrl+G/Ctrl+Y, and AGENTS.md's key grammar now lists Ctrl+O and Ctrl+L. - READMEs stop claiming uv is always consent-gated (non-tty downloads without asking, by design); params --manage dead-ends now name the --add escape; params --json rows carry an additive "binding" key so "kind" no longer puns across sibling commands; ArgSpec moves to the neutral analysis module the four non-python readers were told not to reach around. Tests/coverage follow in the next commit. --- AGENTS.md | 10 +- README.md | 2 +- README.zh-CN.md | 2 +- README.zh-TW.md | 2 +- docs/content/docs/cli.mdx | 12 +- docs/content/docs/parameters.mdx | 5 +- skills/skit/SKILL.md | 10 +- src/skit/analysis.py | 15 + src/skit/argstate.py | 20 +- src/skit/cli.py | 162 ++- src/skit/flows.py | 13 +- src/skit/langs/base.py | 3 +- src/skit/langs/fish/cli_reader.py | 2 +- src/skit/langs/javascript/cli_reader.py | 2 +- src/skit/langs/powershell/cli_reader.py | 2 +- src/skit/langs/python/argspec.py | 13 +- src/skit/langs/shell/cli_reader.py | 2 +- src/skit/locales/skit.pot | 1312 ++++++++++---------- src/skit/locales/zh_CN/LC_MESSAGES/skit.mo | Bin 100116 -> 100610 bytes src/skit/locales/zh_CN/LC_MESSAGES/skit.po | 1312 ++++++++++---------- src/skit/locales/zh_TW/LC_MESSAGES/skit.mo | Bin 100148 -> 100642 bytes src/skit/locales/zh_TW/LC_MESSAGES/skit.po | 1312 ++++++++++---------- src/skit/params.py | 23 +- src/skit/rewrite.py | 30 + src/skit/skills/skit/SKILL.md | 10 +- src/skit/store.py | 21 +- src/skit/tui.py | 66 +- src/skit/tui_add.py | 34 +- src/skit/tui_prefs.py | 17 +- src/skit/tui_settings.py | 41 +- 30 files changed, 2330 insertions(+), 2125 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9465e1d..da8d172 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,13 @@ Key grammar: a chord keeps one meaning per context class — Ctrl+E always opens on the screen's current subject, Ctrl+N always creates the screen's primary object (a script on the add step, an agent on a runner picker), Ctrl+T always inserts a value, Ctrl+R re-runs/refreshes the screen's subject (the run form runs it; Script settings -resyncs its definitions from the script), and Ctrl+S saves/commits the screen's work -(the run form's save-as-preset included). Ctrl+A (cursor-home) and — while an Input has focus — -Ctrl+E (end-of-line) belong to the Input: screen chords for them are never +resyncs its definitions from the script), Ctrl+S saves/commits the screen's work +(the run form's save-as-preset included), Ctrl+O always restores the screen's current +field to its source default (README documents it on the run form — it must never mean +anything else), and Ctrl+L always opens the screen's variable/candidate picker (the +prompt review panel and Script settings). A new screen action takes an UNCLAIMED chord, +never a second meaning for a claimed one. Ctrl+A (cursor-home) and — while an Input has +focus — Ctrl+E (end-of-line) belong to the Input: screen chords for them are never priority-bound; the chip is the path mid-edit. Never bind a text-editing chord (Ctrl+K and friends) with `priority=True` on a screen full of Inputs — the Input's own editing wins there, and the chip stays the mouse path. diff --git a/README.md b/README.md index 21c3ff6..22d5196 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ skit run review --runner codex --set target=src/app.py --no-input ## Install -skit is built on [uv](https://docs.astral.sh/uv/) (tested against 0.11.26). Don't have it? skit asks first, then downloads a pinned uv into its own private directory — your `PATH` and global environment stay untouched. A system-wide [install](https://docs.astral.sh/uv/getting-started/installation/) is still preferred. +skit is built on [uv](https://docs.astral.sh/uv/) (tested against 0.11.26). Don't have it? skit asks first, then downloads a pinned uv into its own private directory — your `PATH` and global environment stay untouched. (Non-interactive runs — CI, pipes — skip the question and download right away.) A system-wide [install](https://docs.astral.sh/uv/getting-started/installation/) is still preferred. ```bash # Install skit with uv tool from PyPI (the package is named skit-cli; the command is skit) diff --git a/README.zh-CN.md b/README.zh-CN.md index 98a783e..4b7cae0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -96,7 +96,7 @@ skit run review --runner codex --set target=src/app.py --no-input ## 安装 -skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版本测试)。还没装 uv?skit 会先征得你同意,再把锁定版本的 uv 下载到自己的私有目录——不碰你的 `PATH`,也不碰全局环境。当然,参考[官方文档](https://docs.astral.sh/uv/getting-started/installation/) 安装 uv 会更好。 +skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版本测试)。还没装 uv?skit 会先征得你同意,再把锁定版本的 uv 下载到自己的私有目录——不碰你的 `PATH`,也不碰全局环境(在 CI、pipe 这类非交互环境下不会问,直接下载)。当然,参考[官方文档](https://docs.astral.sh/uv/getting-started/installation/) 安装 uv 会更好。 ```bash # 用 uv tool 从 PyPI 安装 skit(包名是 skit-cli,装好的命令是 skit) diff --git a/README.zh-TW.md b/README.zh-TW.md index d40ac16..ca61773 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -96,7 +96,7 @@ skit run review --runner codex --set target=src/app.py --no-input ## 安裝 -skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版測試)。還沒裝 uv?skit 會先徵求你同意,再把釘定版本的 uv 下載到自己的私有目錄——不碰你的 `PATH`,也不碰全域環境。當然,參考[官方文檔](https://docs.astral.sh/uv/getting-started/installation/) 安裝 uv 會更好。 +skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版測試)。還沒裝 uv?skit 會先徵求你同意,再把釘定版本的 uv 下載到自己的私有目錄——不碰你的 `PATH`,也不碰全域環境(在 CI、pipe 這類非互動環境下不會問,直接下載)。當然,參考[官方文檔](https://docs.astral.sh/uv/getting-started/installation/) 安裝 uv 會更好。 ```bash # 用 uv tool 從 PyPI 安裝 skit(套件名是 skit-cli,裝好的指令是 skit) diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 785a5d6..b7a4946 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -10,8 +10,9 @@ the same actions, deterministic behavior. - **`--json`** on every read command: `list`, `show`, `params`, `deps`, `preset list`, `runner list`, `doctor`, `config`. -- **`--no-input`** on the commands that could prompt: `add`, `run`, - `runner remove`. Read commands never prompt and don't take it. +- **`--no-input`** on the commands that could prompt: `add`, `run`, and the + destructive trio `remove`, `preset delete`, `runner remove` (which confirm + unless `--yes`). Read commands never prompt and don't take it. - **`--dry-run`** on `run` only: prints the exact command — tokens and globs expanded — and runs nothing. - **Non-interactive contract**: in a pipe, in CI, or under `--no-input`, skit @@ -95,8 +96,9 @@ never replays them. ## skit remove / rename / describe / edit -- `skit remove NAME [--yes]` — remove an entry (the original file is untouched; - copy mode deletes skit's copy). Confirms unless `--yes`. +- `skit remove NAME [--yes] [--no-input]` — remove an entry (the original file + is untouched; copy mode deletes skit's copy). Confirms unless `--yes`; + non-interactively, a missing `--yes` is a clean exit-2 refusal. - `skit rename NAME NEW` — presets, remembered values, and history follow. - `skit describe NAME TEXT` — set the description; empty text clears it. - `skit edit NAME` — open the stored source (the original, in reference mode) in @@ -150,7 +152,7 @@ Package deps apply to Python and JS/TS entries; `--need` applies to every kind. - `skit preset save NAME PRESET [--from-last]` — `--from-last` snapshots the last run's values without asking. - `skit preset list NAME [--json]` -- `skit preset delete NAME PRESET` +- `skit preset delete NAME PRESET [--yes] [--no-input]` — confirms unless `--yes`. ## skit runner diff --git a/docs/content/docs/parameters.mdx b/docs/content/docs/parameters.mdx index 19aa1dd..dbacf84 100644 --- a/docs/content/docs/parameters.mdx +++ b/docs/content/docs/parameters.mdx @@ -45,7 +45,10 @@ per-field `delivers_empty` in `skit show --json`. a preset: presets store the run verbatim, cleared fields included. - **The `--` tail** (extra arguments after `--`) is remembered per entry and replays on the next argument-less run, with a note on stderr. `--forget-args` - clears it; `--raw` never replays it. + clears it; `--raw` never replays it. A replayed tail keeps the semantics it + was captured with: a tail typed into the launch menu's extra field re-expands + its tokens and globs on each run, while a tail your shell already processed + replays literally — in both the CLI and the TUI. - **Run history**: each entry's state file records the last run's time, exit code, and exact values. - All of this lives in skit's state directory (one TOML per entry) — see diff --git a/skills/skit/SKILL.md b/skills/skit/SKILL.md index 7d42923..bb8e92a 100644 --- a/skills/skit/SKILL.md +++ b/skills/skit/SKILL.md @@ -31,8 +31,10 @@ is *the user's curated space*: treat it like their dotfiles. Propose `skit add` when you've written something reusable; don't add it silently. 5. **Pass `--no-input` on every `skit run` and `skit add`.** It guarantees those never block on a prompt; if information is missing, skit fails fast with a named error - instead. `skit remove` confirms instead of taking `--no-input` — pass `-y`. The - read commands (`list`, `show`, `params`, …) never prompt and don't take the flag. + instead. The destructive commands (`remove`, `preset delete`, `runner remove`) + confirm unless you pass `-y`; non-interactively, a missing `-y` is a clean exit-2 + refusal, never a hang. The read commands (`list`, `show`, `params`, …) never prompt + and don't take the flag. ## Discover entries @@ -200,7 +202,7 @@ Named value sets per entry, ideal for recurring jobs: skit run --set a=1 --set b=2 --save-preset nightly --dry-run --no-input # create without running skit preset list --json skit run -p nightly --no-input -skit preset delete nightly +skit preset delete nightly -y # confirms without -y ``` ## Prompts & runners @@ -249,7 +251,7 @@ skit params --no-interpolate # switch insertion off; --interpolate turns skit runner list --json # [{"name": …, "argv": […]}] skit runner add mycli -- mycli run {{prompt}} # each word = one argument, no shell skit runner add mycli --force -- mycli run --model opus {{prompt}} # --force replaces an existing runner (edit) -skit runner remove mycli -y # confirms without -y, like skit remove +skit runner remove mycli -y # confirms without -y, like skit remove and preset delete ``` - The agent's own per-run flags pass through after `--`: diff --git a/src/skit/analysis.py b/src/skit/analysis.py index ad60343..e4c1f76 100644 --- a/src/skit/analysis.py +++ b/src/skit/analysis.py @@ -31,6 +31,21 @@ from collections.abc import Callable, Mapping +@dataclass +class ArgSpec: + """A script's own statically-read CLI surface (argparse/click/typer, parseArgs, + getopts, fish argparse, PowerShell param()). Lives HERE with the other neutral + result types — Candidate/Analysis/Report — for the same reason they do: every + language's cli_reader returns one, and a shell reader must not import the Python + analyzer package just to name its result type.""" + + # Each field is a delivery=flag ParamDecl (binding="none": the script owns the parser, + # skit only reflects it). List position carries declaration order — there is no order field. + fields: list[ParamDecl] = field(default_factory=list) + ok: bool = True # False -> whole-parser degradation (passthrough escape only) + reason: str = "" # symbolic: "subparsers" | "dynamic" (UI owns the wording) + + @dataclass class Candidate: """A candidate parameter. const/envdefault are keyed by variable name; input by call diff --git a/src/skit/argstate.py b/src/skit/argstate.py index 1c255af..4be7e11 100644 --- a/src/skit/argstate.py +++ b/src/skit/argstate.py @@ -79,14 +79,21 @@ def _strip_secrets(values: dict[str, str], secret_names: Iterable[str]) -> dict[ def load_state(slug: str) -> dict[str, Any]: - """Return {"values": {…}, "extra_args": […], "presets": {name: {…}}, "last_run": {…}}. + """Return {"values": {…}, "extra_args": […], "extra_args_raw": bool, + "presets": {name: {…}}, "last_run": {…}}. last_run is {"at": ISO-8601 str, "exit": int} after the first recorded run, else {}. + extra_args_raw says HOW the remembered tail was captured: True = raw intent text + (the TUI's extra field — tokens/globs expand on replay), False/absent = already + shell-processed (the CLI's `-- args` — replays literally, in both faces). Without + it, one stored tail replayed under two different expansion regimes depending on + which face happened to rerun it. """ doc = _load_doc(slug) return { "values": dict(doc.get("values", {})), "extra_args": list(doc.get("extra_args", [])), + "extra_args_raw": bool(doc.get("extra_args_raw", False)), "presets": {k: dict(v) for k, v in doc.get("presets", {}).items()}, "last_run": dict(doc.get("last_run", {})), } @@ -107,6 +114,7 @@ def save_last( *, values: dict[str, str] | None = None, extra_args: list[str] | None = None, + extra_args_raw: bool = False, secret_names: Iterable[str] = (), ) -> None: """Remember last-used (read-modify-write, keeping presets). Secret keys are stripped (C3). @@ -116,6 +124,10 @@ def save_last( check made cleared extra args resurrect forever: the form saved nothing, the next run re-read the old value, reused it, and wrote it back.) + extra_args_raw records the tail's provenance (see load_state) and travels WITH the + tail: it is written or cleared exactly when extra_args is, so a marker can never + describe a tail it didn't come with. + Even on a call that carries no new values, any name in secret_names is dropped from the previously-stored values — a value saved while a parameter was public must not survive on disk after it becomes secret. @@ -129,6 +141,12 @@ def save_last( doc["values"] = _strip_secrets(doc.get("values", {}), banned) if extra_args is not None: doc["extra_args"] = extra_args + if extra_args and extra_args_raw: + doc["extra_args_raw"] = True + else: + # _save_doc prunes falsy values, so False is stored as absence; pop so a + # cleared/processed tail never inherits a stale raw marker. + doc.pop("extra_args_raw", None) _save_doc(slug, doc) diff --git a/src/skit/cli.py b/src/skit/cli.py index d5b2fba..1553cf2 100644 --- a/src/skit/cli.py +++ b/src/skit/cli.py @@ -42,7 +42,7 @@ from .i18n import gettext, ngettext from .langs.registry import KNOWN_KINDS, spec_for from .params import ParamDecl, declared_from_meta, edit_declared, is_secret_name -from .rewrite import detect_newline, restore_newline +from .rewrite import detect_newline, read_for_block_edit, write_block_edit if TYPE_CHECKING: from collections.abc import Callable @@ -637,19 +637,11 @@ def _onboard_script_params(entry: store.Entry, kind_spec: LangSpec, no_input: bo if not specs: return [] copy_path = entry.script_path - # This is a write-back path, so it must be byte-lossless (same discipline as _edit_params). - # Shell/fish scripts may legitimately contain arbitrary bytes; surrogateescape lets the - # comment-only metadata edit round-trip them instead of replacing each with U+FFFD. Fold to - # LF for the LF-based block engine, then restore the copy's own line-ending style so a CRLF - # script stays CRLF and the edit stays confined to the block it inserted. - raw = copy_path.read_bytes() - newline = detect_newline(raw) - current = raw.decode("utf-8", errors="surrogateescape") # pragma: no mutate — codec alias - current = current.replace("\r\n", "\n").replace("\r", "\n") - written = restore_newline(kind_spec.params_io.write(current, specs), newline) - copy_path.write_bytes( - written.encode("utf-8", errors="surrogateescape") - ) # pragma: no mutate — codec alias + # Byte-lossless write-back through the one shared pair (rewrite.read_for_block_edit / + # write_block_edit): surrogateescape bytes, LF fold for the block engine, the copy's own + # newline style restored, atomic + mode-preserving. + current, newline = read_for_block_edit(copy_path) + write_block_edit(copy_path, kind_spec.params_io.write(current, specs), newline) return [s.name for s in specs] @@ -694,16 +686,12 @@ def _onboard_python( params_specs = _onboard_params(text, entry.meta.name, no_input) if params_specs: copy_path = entry.script_path - # Byte-lossless write-back (same discipline as _edit_params): fold to LF for the - # LF-based PEP 723 block engine, then restore the copy's own line-ending style so a - # CRLF script stays CRLF instead of being re-expanded to the host os.linesep. - raw = copy_path.read_bytes() - newline = detect_newline(raw) - current = ( - raw.decode("utf-8").replace("\r\n", "\n").replace("\r", "\n") - ) # pragma: no mutate — codec alias - new_text = restore_newline(metawriter.write_params(current, params_specs), newline) - copy_path.write_bytes(new_text.encode("utf-8")) # pragma: no mutate — codec alias + # The shared byte-lossless pair (rewrite.py) — surrogateescape included: the old + # strict decode here let a non-UTF-8 python file escape as a raw UnicodeDecodeError + # traceback after the entry was already stored, where the shell lane degraded + # gracefully on the same input. + current, newline = read_for_block_edit(copy_path) + write_block_edit(copy_path, metawriter.write_params(current, params_specs), newline) managed = [s.name for s in params_specs] secrets = [s.name for s in params_specs if s.secret] return entry, final_deps, managed, secrets @@ -2535,12 +2523,20 @@ def show( def remove( name: str = _SCRIPT_ARG, yes: bool = typer.Option(False, "--yes", "-y", help=gettext("Skip confirmation")), + no_input: bool = typer.Option(False, "--no-input", help=gettext("Never prompt")), ) -> None: """Remove a script (copy mode deletes the copy in the store; the original is untouched).""" try: entry = store.resolve(name) except store.NotFoundError as exc: raise _fail(str(exc), 1) from exc + # The non-interactive contract, same guard as runner_remove: in a pipe/CI or under + # --no-input, a missing -y is a worded exit-2 refusal — never a typer.confirm that + # eats a line of piped stdin or dies as click's bare "Aborted.". + if not yes and (no_input or not _is_interactive()): + raise _fail( + gettext("Confirmation is required; pass --yes to remove the entry."), EXIT_USAGE + ) if not yes: entry_spec = spec_for(entry.meta.kind) if entry_spec is not None and not entry_spec.has_original_file: @@ -3146,17 +3142,24 @@ def run( # Every other kind reuses its remembered tail — a script replays its argv, a prompt or # command its agent/command flags (a remembered `--model` is config, like the pinned # runner), matching the run form and the TUI's `r` rerun. + # This run's own `-- args` came through the user's shell: never re-expanded + # (extra_raw=False). A replayed tail instead follows its recorded provenance, so a + # tail typed into the TUI form (raw intent — {today}, globs) expands here exactly + # as it does under `r`, and the two faces launch the same argv from the same state. + extra_raw = False if not extra and not raw: - last_extra = argstate.load_state(entry.slug)["extra_args"] + state = argstate.load_state(entry.slug) + last_extra = state["extra_args"] if last_extra: extra = last_extra + extra_raw = state["extra_args_raw"] # stderr, like the drift banner: skit chrome must not pollute the script's # own stdout, and agents watch stderr for skit-side signals (SKILL.md). err_console.print( f"[dim]{gettext('Reusing your last arguments: %(args)s') % {'args': ' '.join(escape(a) for a in extra)}}[/dim]" ) try: - asm = flows.assemble(plan, values, extra, cwd=Path.cwd(), expand_extra=False) + asm = flows.assemble(plan, values, extra, cwd=Path.cwd(), expand_extra=extra_raw) except flows.FormError as exc: raise _fail(str(exc), EXIT_SKIT) from exc @@ -3241,7 +3244,9 @@ def _persist_preset() -> None: # The run stamp still lands — Library sorting and `r` treat it as a run. argstate.record_run(entry.slug, code, at=models.now_iso()) else: - flows.save_after_run(entry.slug, plan, values, extra, code, at=models.now_iso()) + flows.save_after_run( + entry.slug, plan, values, extra, code, at=models.now_iso(), extra_raw=extra_raw + ) if code != 0: err_console.print( f"[yellow]{gettext('Run exited with code %(code)s') % {'code': code}}[/yellow]" @@ -3671,31 +3676,55 @@ def preset_list( def preset_delete( name: str = _SCRIPT_ARG, preset_name: str = typer.Argument(..., help=gettext("Preset name")), + yes: bool = typer.Option(False, "--yes", "-y", help=gettext("Skip confirmation")), + no_input: bool = typer.Option(False, "--no-input", help=gettext("Never prompt")), ) -> None: """Delete a named preset.""" try: entry = store.resolve(name) except store.NotFoundError as exc: raise _fail(str(exc), 1) from exc + # Unknown-name feedback comes BEFORE the ask: confirming a deletion that then turns + # out to target nothing is a wasted question. delete_preset re-checks under the lock, + # so a preset that vanishes between the two reads still lands in the same error. + if preset_name not in argstate.load_state(entry.slug)["presets"]: + _fail_unknown_preset(entry, preset_name) + # A preset is unrecoverable user data; its deletion gets the same ask (and the same + # non-interactive refusal) as removing an entry or a runner — the trivially + # re-creatable config row must not be better guarded than the thing users typed in. + if not yes and (no_input or not _is_interactive()): + raise _fail( + gettext("Confirmation is required; pass --yes to delete the preset."), EXIT_USAGE + ) + if not yes: + typer.confirm( + gettext('Delete preset "%(preset)s" from %(name)s?') + % {"preset": preset_name, "name": entry.meta.name}, + abort=True, + ) if argstate.delete_preset(entry.slug, preset_name): console.print( gettext('Preset "%(preset)s" deleted from %(name)s.') % {"preset": escape(preset_name), "name": escape(entry.meta.name)} ) else: - err_console.print( - "[red]" - + gettext('Unknown preset "%(preset)s". Available: %(presets)s') - % { - "preset": escape(preset_name), - "presets": ", ".join( - escape(p) for p in sorted(argstate.load_state(entry.slug)["presets"]) - ) - or "—", - } - + _RED_CLOSE - ) - raise typer.Exit(1) + _fail_unknown_preset(entry, preset_name) + + +def _fail_unknown_preset(entry: store.Entry, preset_name: str) -> NoReturn: + err_console.print( + "[red]" + + gettext('Unknown preset "%(preset)s". Available: %(presets)s') + % { + "preset": escape(preset_name), + "presets": ", ".join( + escape(p) for p in sorted(argstate.load_state(entry.slug)["presets"]) + ) + or "—", + } + + _RED_CLOSE + ) + raise typer.Exit(1) # -------------------------------------------------------------------------- @@ -3903,7 +3932,12 @@ def _show_params(entry: store.Entry, as_json: bool) -> None: declared = declared_from_meta(entry.meta.parameters) if as_json: payload = { - "params": [s.to_block_dict() for s in specs], + # to_block_dict is the FROZEN on-disk row (its "kind" key carries the + # binding — const/input — for back-compat with existing user files). The + # JSON projection adds "binding" alongside it so an agent reading `show + # --json` (where "kind" is the entry's language) and this command never + # meets one key name carrying two different axes. + "params": [{**s.to_block_dict(), "binding": s.binding} for s in specs], # The SOURCE's current default per managed name (additive key). "params" # stays the verbatim stored record — an agent that wants the value a run # would actually prefill reads it here, same truth as the human table. @@ -4489,13 +4523,17 @@ def _edit_params( console = _maybe_quiet(quiet) entry_spec = spec_for(entry.meta.kind) if entry_spec is None or entry_spec.params_io is None or entry_spec.analyzer is None: - raise _fail( - gettext( - "%(name)s has no managed parameters — its kind has no analyzer to read them from." - ) - % {"name": entry.meta.name}, - 1, - ) + message = gettext( + "%(name)s has no managed parameters — its kind has no analyzer to read them from." + ) % {"name": entry.meta.name} + if entry_spec is not None and entry_spec.params_io is None: + # The declared [[parameters]] lane IS this kind's parameter home (exe, command, + # prompt, the Tier-0 interpreted kinds) — a refusal that names no way forward + # would hide the exact door that was built for it. + message += " " + gettext("Declare one instead: skit params %(name)s --add PARAM") % { + "name": entry.meta.name + } + raise _fail(message, 1) if entry.meta.mode == "reference": raise _fail( gettext( @@ -4537,22 +4575,12 @@ def _edit_params( err_console.print(f"[yellow]{escape(analysis.render_warning(w))}[/yellow]") for w in _apply_env_sources(result.specs, env_sources): err_console.print(f"[yellow]{escape(w)}[/yellow]") - # This is a write-back path, so it must be byte-lossless (same discipline as - # _onboard_script_params and _normalize_params): `text` above was read with errors="replace" - # for the analyzer, but writing THAT text back would bake U+FFFD over every non-UTF-8 byte. - # Re-read the raw bytes with surrogateescape — params_io.write only touches the comment block, - # so unrelated bytes round-trip. Fold to LF for the LF-based block engine, then restore the - # copy's own line-ending style: writing the folded LF back would rewrite every line of a CRLF - # script even though only the [tool.skit] block changed (and write_text, the old path, was - # worse still — it re-expanded \n to the HOST os.linesep, CRLF-ifying a copy on Windows). - raw = copy_path.read_bytes() - newline = detect_newline(raw) - current = raw.decode("utf-8", errors="surrogateescape") # pragma: no mutate — codec alias - current = current.replace("\r\n", "\n").replace("\r", "\n") - written = restore_newline(entry_spec.params_io.write(current, result.specs), newline) - copy_path.write_bytes( - written.encode("utf-8", errors="surrogateescape") - ) # pragma: no mutate — codec alias + # `text` above was read with errors="replace" for the analyzer; writing THAT text back + # would bake U+FFFD over every non-UTF-8 byte. Re-read through the shared byte-lossless + # pair (rewrite.py) — params_io.write only touches the comment block, so unrelated bytes + # round-trip and the copy's own line-ending style survives. + current, newline = read_for_block_edit(copy_path) + write_block_edit(copy_path, entry_spec.params_io.write(current, result.specs), newline) secret_now = {s.name for s in result.specs if s.secret} purged = argstate.purge_secret(entry.slug, secret_now) if purged: @@ -4694,8 +4722,10 @@ def _normalize_params( else s for s in entry_spec.params_io.read(result.text) ] - new_text = restore_newline(entry_spec.params_io.write(result.text, specs), newline) - copy_path.write_bytes(new_text.encode("utf-8")) # pragma: no mutate — codec alias + # The shared write half (atomic + mode-preserving); the strict read above stays — a + # normalize that can't decode is refused whole, unlike the comment-only edits' + # surrogateescape round-trip. + write_block_edit(copy_path, entry_spec.params_io.write(result.text, specs), newline) console.print( f"[green]{gettext('Normalized %(names)s in %(name)s: delivered as environment variables from now on (no temporary copy, and $0 stays your real file).') % {'names': ', '.join(escape(n) for n in result.normalized), 'name': escape(entry.meta.name)}}[/green]" ) diff --git a/src/skit/flows.py b/src/skit/flows.py index b8f8c19..40ce0f1 100644 --- a/src/skit/flows.py +++ b/src/skit/flows.py @@ -613,6 +613,9 @@ def assemble( # expand_extra=False: the CLI's `-- args` already went through the user's shell — # a second token/glob pass would rewrite what they deliberately quoted (and --raw # must be genuinely raw). The TUI's extra-args field has no shell, so it expands. + # A REPLAYED tail follows its recorded provenance (argstate's extra_args_raw), not + # the face replaying it: the caller passes that bit here, so a TUI-saved {today} + # still expands under `skit run` and a CLI-quoted *.png never globs under `r`. if expand_extra: expanded_extra: list[str] = [] for item in extra_args: @@ -823,9 +826,14 @@ def save_after_run( exit_code: int, *, at: str, + extra_raw: bool, ) -> None: - """Persist intent (raw token/glob text), never expansion; secrets structurally - stripped by argstate (C3); stamp the run for Library sorting and the r key.""" + """Persist intent for field values (raw token/glob text), never expansion; secrets + structurally stripped by argstate (C3); stamp the run for Library sorting and the r + key. extra_raw is the tail's provenance — True for the TUI form's raw intent text, + False for a CLI tail the user's shell already processed — recorded so every later + replay (either face) re-expands exactly the tails that were captured raw, and only + those (see argstate.load_state).""" # Retroactive C3 scrub: a placeholder/param that is secret NOW must not keep old # plaintext in values or presets from the days it wasn't (purge is idempotent). if plan.secret_names: @@ -834,6 +842,7 @@ def save_after_run( slug, values=remembered_values(plan, values), extra_args=list(extra_args), + extra_args_raw=extra_raw, secret_names=plan.secret_names, ) argstate.record_run( diff --git a/src/skit/langs/base.py b/src/skit/langs/base.py index 8f88b64..aa0366e 100644 --- a/src/skit/langs/base.py +++ b/src/skit/langs/base.py @@ -29,11 +29,10 @@ from collections.abc import Callable from pathlib import Path - from ..analysis import Analysis, Report + from ..analysis import Analysis, ArgSpec, Report from ..config import PromptRunner from ..models import Entry from ..params import ParamDecl - from .python.argspec import ArgSpec class LaunchError(Exception): diff --git a/src/skit/langs/fish/cli_reader.py b/src/skit/langs/fish/cli_reader.py index b0c920a..624b22a 100644 --- a/src/skit/langs/fish/cli_reader.py +++ b/src/skit/langs/fish/cli_reader.py @@ -22,8 +22,8 @@ from __future__ import annotations +from ...analysis import ArgSpec from ...params import ParamDecl, is_secret_name -from ..python.argspec import ArgSpec from . import analyzer # argparse's own options that consume a following value (so their value is not a spec). Attached diff --git a/src/skit/langs/javascript/cli_reader.py b/src/skit/langs/javascript/cli_reader.py index 5198438..e945c5e 100644 --- a/src/skit/langs/javascript/cli_reader.py +++ b/src/skit/langs/javascript/cli_reader.py @@ -24,8 +24,8 @@ from tree_sitter import Parser +from ...analysis import ArgSpec from ...params import ParamDecl, is_secret_name -from ..python.argspec import ArgSpec from .analyzer import ( _const_candidates, _literal_value, diff --git a/src/skit/langs/powershell/cli_reader.py b/src/skit/langs/powershell/cli_reader.py index 1455479..dd9f9fd 100644 --- a/src/skit/langs/powershell/cli_reader.py +++ b/src/skit/langs/powershell/cli_reader.py @@ -41,8 +41,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from ...analysis import ArgSpec from ...params import ParamType, is_secret_name -from ..python.argspec import ArgSpec if TYPE_CHECKING: from ...params import ParamDecl diff --git a/src/skit/langs/python/argspec.py b/src/skit/langs/python/argspec.py index 74bef9e..a3cc5d8 100644 --- a/src/skit/langs/python/argspec.py +++ b/src/skit/langs/python/argspec.py @@ -20,8 +20,10 @@ from __future__ import annotations import ast -from dataclasses import dataclass, field +# ArgSpec's home is the neutral analysis module (with Candidate/Analysis/Report) — +# re-exported here so python-internal callers keep their import path. +from ...analysis import ArgSpec from ...params import ParamDecl, ParamType, is_secret_name from .analyzer import _bound_names, _const_candidates, _literal_value @@ -82,15 +84,6 @@ def _resolve_literal(node: ast.expr, env: ConstEnv) -> tuple[bool, str | int | f _CLICK_TYPES: dict[str, ParamType] = {"INT": "int", "FLOAT": "float", "STRING": "str"} -@dataclass -class ArgSpec: - # Each field is a delivery=flag ParamDecl (binding="none": the script owns the parser, - # skit only reflects it). List position carries declaration order — there is no order field. - fields: list[ParamDecl] = field(default_factory=list) - ok: bool = True # False -> whole-parser degradation (passthrough escape only) - reason: str = "" # symbolic: "subparsers" | "dynamic" (UI owns the wording) - - def read_cli(text: str) -> ArgSpec | None: """The unified entry point: the first CLI surface that reads statically wins. argparse first (the overwhelmingly common case in AI-written scripts), then click, diff --git a/src/skit/langs/shell/cli_reader.py b/src/skit/langs/shell/cli_reader.py index 73ea950..afd76e2 100644 --- a/src/skit/langs/shell/cli_reader.py +++ b/src/skit/langs/shell/cli_reader.py @@ -22,8 +22,8 @@ from tree_sitter import Parser +from ...analysis import ArgSpec from ...params import ParamDecl, is_secret_name -from ..python.argspec import ArgSpec from .analyzer import _LANGUAGE, _arguments, _literal_text, _text, _walk if TYPE_CHECKING: diff --git a/src/skit/locales/skit.pot b/src/skit/locales/skit.pot index 402c6fc..6d21ccf 100644 --- a/src/skit/locales/skit.pot +++ b/src/skit/locales/skit.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: skit 0.4.1.dev0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-07-23 22:54-0700\n" +"POT-Creation-Date: 2026-07-26 01:20-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,61 +17,61 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/skit/analysis.py:147 +#: src/skit/analysis.py:162 #, python-format msgid "The parameter definitions for %(name)s have drifted from the script:" msgstr "" -#: src/skit/analysis.py:156 +#: src/skit/analysis.py:171 #, python-brace-format, python-format msgid "%(name)s is no longer read from the environment (its ${...:-default} was removed or overridden by a plain assignment) — your value would be silently ignored. Re-add or resync." msgstr "" -#: src/skit/analysis.py:166 +#: src/skit/analysis.py:181 #, python-format msgid "%(name)s: injection target no longer exists (dropped from this run's form)" msgstr "" -#: src/skit/analysis.py:174 +#: src/skit/analysis.py:189 #, python-format msgid "%(name)s: type changed from %(old)s to %(new)s in the source (still injected — double-check the value)" msgstr "" -#: src/skit/analysis.py:182 +#: src/skit/analysis.py:197 #, python-format msgid "%(name)s: its prompt no longer matches a unique input/read call; falling back to position (still injected — double-check this lands on the right question, especially if it's a secret)" msgstr "" -#: src/skit/analysis.py:190 +#: src/skit/analysis.py:205 #, python-format msgid "To refresh the definitions, run: skit params %(name)s --resync" msgstr "" -#: src/skit/analysis.py:204 +#: src/skit/analysis.py:219 #, python-format msgid "%(name)s isn't a managed parameter; skipped." msgstr "" -#: src/skit/analysis.py:205 +#: src/skit/analysis.py:220 #, python-format msgid "Dropped %(name)s: it no longer exists in the script." msgstr "" -#: src/skit/analysis.py:206 +#: src/skit/analysis.py:221 #, python-format msgid "%(name)s is already managed; skipped." msgstr "" -#: src/skit/analysis.py:208 +#: src/skit/analysis.py:223 #, python-format msgid "%(name)s isn't a detectable parameter in the current script; skipped." msgstr "" -#: src/skit/analysis.py:211 +#: src/skit/analysis.py:226 msgid "Could not parse the script (syntax error); resync skipped. Parameter definitions are unchanged." msgstr "" -#: src/skit/analysis.py:215 +#: src/skit/analysis.py:230 #, python-format msgid "%(name)s: re-anchored to its current position after its prompt stopped matching uniquely; double-check the prompt/secret assignment is still correct." msgstr "" @@ -128,19 +128,19 @@ msgstr "" msgid "A Python constraint doesn't apply to %(kind)s scripts." msgstr "" -#: src/skit/cli.py:390 src/skit/cli.py:775 src/skit/cli.py:1213 -#: src/skit/cli.py:2012 +#: src/skit/cli.py:390 src/skit/cli.py:763 src/skit/cli.py:1201 +#: src/skit/cli.py:2000 msgid "Name in skit" msgstr "" -#: src/skit/cli.py:396 src/skit/cli.py:1483 src/skit/cli.py:2018 -#: src/skit/tui_add.py:245 +#: src/skit/cli.py:396 src/skit/cli.py:1471 src/skit/cli.py:2006 +#: src/skit/tui_add.py:246 msgid "Description (optional)" msgstr "" #: src/skit/cli.py:420 src/skit/cli.py:428 src/skit/store.py:224 #: src/skit/store.py:355 src/skit/store.py:427 src/skit/store.py:639 -#: src/skit/tui_add.py:356 +#: src/skit/tui_add.py:357 #, python-format msgid "File not found: %(path)s" msgstr "" @@ -150,8 +150,8 @@ msgstr "" msgid "Not a file: %(path)s" msgstr "" -#: src/skit/cli.py:457 src/skit/cli.py:1131 src/skit/cli.py:2646 -#: src/skit/tui_add.py:1415 +#: src/skit/cli.py:457 src/skit/cli.py:1119 src/skit/cli.py:2640 +#: src/skit/tui_add.py:1431 msgid " (secret)" msgstr "" @@ -169,7 +169,7 @@ msgstr "" msgid "⚠ looks like a loop accumulator — probably not a parameter" msgstr "" -#: src/skit/cli.py:491 src/skit/tui_add.py:1039 +#: src/skit/cli.py:491 src/skit/tui_add.py:1049 msgid "This script reads command-line arguments; the run form has an extra-arguments field for them." msgstr "" @@ -178,14 +178,14 @@ msgstr "" msgid "💡 %(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (skit edit %(script)s)." msgstr "" -#: src/skit/cli.py:521 src/skit/tui_add.py:981 +#: src/skit/cli.py:521 src/skit/tui_add.py:991 #, python-format msgid "✓ skit read this script's own arguments (%(count)s field). Running it opens a form — nothing to memorize." msgid_plural "✓ skit read this script's own arguments (%(count)s fields). Running it opens a form — nothing to memorize." msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:531 src/skit/tui_add.py:996 +#: src/skit/cli.py:531 src/skit/tui_add.py:1006 #, python-format msgid "This script parses its own arguments (%(names)s); skit couldn't model them statically, so the run form offers an extra-arguments field." msgstr "" @@ -205,1724 +205,1742 @@ msgstr "" msgid "Reference mode never touches the original file, so parameter setup was skipped." msgstr "" -#: src/skit/cli.py:719 +#: src/skit/cli.py:707 #, python-format msgid "The draft's #! names no interpreter skit knows — add it with: skit add %(path)s --kind " msgstr "" -#: src/skit/cli.py:732 +#: src/skit/cli.py:720 #, python-format msgid "--dep/--python are python flags, but the draft's shebang names %(kind)s — drop them, or keep the python shebang." msgstr "" -#: src/skit/cli.py:760 +#: src/skit/cli.py:748 msgid "--edit opens your editor, which --no-input forbids — pipe the script in instead: skit add - -n NAME" msgstr "" -#: src/skit/cli.py:768 +#: src/skit/cli.py:756 msgid "Writing a new script in an editor needs an interactive terminal." msgstr "" -#: src/skit/cli.py:777 src/skit/cli.py:1215 src/skit/cli.py:3385 -#: src/skit/cli.py:3429 src/skit/config.py:620 src/skit/store.py:1135 -#: src/skit/tui_add.py:446 src/skit/tui_runner.py:160 +#: src/skit/cli.py:765 src/skit/cli.py:1203 src/skit/cli.py:3388 +#: src/skit/cli.py:3432 src/skit/config.py:620 src/skit/store.py:1133 +#: src/skit/tui_add.py:447 src/skit/tui_runner.py:160 msgid "A name is required." msgstr "" -#: src/skit/cli.py:788 src/skit/cli.py:1224 src/skit/store.py:707 +#: src/skit/cli.py:776 src/skit/cli.py:1212 src/skit/store.py:707 #, python-format msgid "The name %(name)s is already taken — pick another name." msgstr "" -#: src/skit/cli.py:800 src/skit/cli.py:1239 +#: src/skit/cli.py:788 src/skit/cli.py:1227 msgid "Opening your editor…" msgstr "" -#: src/skit/cli.py:808 src/skit/cli.py:1109 src/skit/cli.py:1245 -#: src/skit/cli.py:2152 src/skit/editor.py:105 src/skit/langs/launch.py:712 -#: src/skit/store.py:445 src/skit/tui_add.py:531 src/skit/tui_add.py:747 -#: src/skit/tui_add.py:1079 src/skit/tui_add.py:1265 src/skit/tui_add.py:1543 -#: src/skit/tui_settings.py:364 +#: src/skit/cli.py:796 src/skit/cli.py:1097 src/skit/cli.py:1233 +#: src/skit/cli.py:2140 src/skit/editor.py:105 src/skit/langs/launch.py:712 +#: src/skit/store.py:445 src/skit/tui_add.py:532 src/skit/tui_add.py:749 +#: src/skit/tui_add.py:1089 src/skit/tui_add.py:1281 src/skit/tui_add.py:1559 +#: src/skit/tui_settings.py:359 #, python-format msgid "Can't read %(path)s: %(error)s" msgstr "" -#: src/skit/cli.py:813 +#: src/skit/cli.py:801 msgid "Nothing was written, so no script was added." msgstr "" -#: src/skit/cli.py:883 src/skit/cli.py:936 src/skit/cli.py:1285 +#: src/skit/cli.py:871 src/skit/cli.py:924 src/skit/cli.py:1273 msgid "Reading the script from stdin needs an explicit --name." msgstr "" -#: src/skit/cli.py:893 src/skit/cli.py:943 src/skit/cli.py:1305 +#: src/skit/cli.py:881 src/skit/cli.py:931 src/skit/cli.py:1293 msgid "Nothing arrived on stdin, so there is nothing to add." msgstr "" -#: src/skit/cli.py:991 +#: src/skit/cli.py:979 msgid "# New prompt" msgstr "" -#: src/skit/cli.py:1007 +#: src/skit/cli.py:995 msgid "Custom agents: skit runner add mycli -- mycli run {{prompt}} (see skit runner --help)" msgstr "" -#: src/skit/cli.py:1031 src/skit/cli.py:3482 +#: src/skit/cli.py:1019 src/skit/cli.py:3485 #, python-format msgid "Unknown runner: %(runner)s. Configured runners: %(names)s" msgstr "" -#: src/skit/cli.py:1046 +#: src/skit/cli.py:1034 #, python-format msgid "The draft is no longer at %(path)s, so nothing was added." msgstr "" -#: src/skit/cli.py:1050 +#: src/skit/cli.py:1038 #, python-format msgid "Your draft was kept at %(path)s — fix the problem and add it with: skit add %(path)s" msgstr "" -#: src/skit/cli.py:1054 src/skit/tui_add.py:545 src/skit/tui_add.py:571 +#: src/skit/cli.py:1042 src/skit/tui_add.py:546 src/skit/tui_add.py:572 #, python-format msgid "Your draft was kept at %(path)s" msgstr "" -#: src/skit/cli.py:1071 +#: src/skit/cli.py:1059 msgid "- = no pin (the run form asks each time)" msgstr "" -#: src/skit/cli.py:1074 src/skit/cli.py:2887 +#: src/skit/cli.py:1062 src/skit/cli.py:2881 msgid "Run this prompt with which agent?" msgstr "" -#: src/skit/cli.py:1129 src/skit/cli.py:2644 +#: src/skit/cli.py:1117 src/skit/cli.py:2638 msgid "Detected placeholders (each becomes a form field):" msgstr "" -#: src/skit/cli.py:1136 src/skit/cli.py:2651 src/skit/tui_add.py:295 -#: src/skit/tui_add.py:1423 src/skit/tui_settings.py:704 +#: src/skit/cli.py:1124 src/skit/cli.py:2645 src/skit/tui_add.py:296 +#: src/skit/tui_add.py:1439 src/skit/tui_settings.py:699 #, python-format msgid "…and %(count)s more" msgstr "" -#: src/skit/cli.py:1140 +#: src/skit/cli.py:1128 msgid "Manage which? (all / none / off / numbers like 1,3)" msgstr "" -#: src/skit/cli.py:1169 +#: src/skit/cli.py:1157 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written (turn it on with: skit params %(name)s --interpolate)" msgstr "" -#: src/skit/cli.py:1174 +#: src/skit/cli.py:1162 #, python-format msgid "Detected %(count)s placeholders — too many to manage automatically, so none were. Manage the ones you need with: skit params %(name)s --add NAME, or turn insertion off with --no-interpolate." msgstr "" -#: src/skit/cli.py:1178 +#: src/skit/cli.py:1166 #, python-format msgid "Runs with %(runner)s (change it with: skit params %(name)s --runner NAME)" msgstr "" -#: src/skit/cli.py:1206 +#: src/skit/cli.py:1194 msgid "--prompt with no path opens your editor, which --no-input forbids — pipe the body in instead: skit add - --prompt -n NAME" msgstr "" -#: src/skit/cli.py:1250 +#: src/skit/cli.py:1238 msgid "Nothing was written, so no prompt was added." msgstr "" -#: src/skit/cli.py:1356 +#: src/skit/cli.py:1344 #, python-format msgid "Unknown kind: %(kind)s. Choose from: %(kinds)s" msgstr "" -#: src/skit/cli.py:1376 src/skit/tui_add.py:120 +#: src/skit/cli.py:1364 src/skit/tui_add.py:121 #, python-format msgid "The #! in %(file)s names no interpreter skit knows. What is it?" msgstr "" -#: src/skit/cli.py:1378 src/skit/tui_add.py:122 +#: src/skit/cli.py:1366 src/skit/tui_add.py:123 #, python-format msgid "What is %(file)s? skit can't tell from the name." msgstr "" -#: src/skit/cli.py:1383 +#: src/skit/cli.py:1371 msgid "- = cancel" msgstr "" -#: src/skit/cli.py:1385 src/skit/cli.py:1465 +#: src/skit/cli.py:1373 src/skit/cli.py:1453 msgid "Which one?" msgstr "" -#: src/skit/cli.py:1399 +#: src/skit/cli.py:1387 #, python-format msgid "Detected parameters: %(names)s (the run form asks for them; your last values are remembered)" msgstr "" -#: src/skit/cli.py:1459 +#: src/skit/cli.py:1447 msgid "What would you like to add?" msgstr "" -#: src/skit/cli.py:1460 +#: src/skit/cli.py:1448 msgid "A file you already have — a script, program, or prompt" msgstr "" -#: src/skit/cli.py:1461 +#: src/skit/cli.py:1449 msgid "A new script, written in your editor" msgstr "" -#: src/skit/cli.py:1462 +#: src/skit/cli.py:1450 msgid "A new AI-agent prompt, written in your editor" msgstr "" -#: src/skit/cli.py:1463 +#: src/skit/cli.py:1451 #, python-brace-format msgid "A command template (e.g. ffmpeg -i {input})" msgstr "" -#: src/skit/cli.py:1476 +#: src/skit/cli.py:1464 msgid "Command template" msgstr "" -#: src/skit/cli.py:1479 src/skit/tui_add.py:242 +#: src/skit/cli.py:1467 src/skit/tui_add.py:243 msgid "Name for the command" msgstr "" -#: src/skit/cli.py:1489 +#: src/skit/cli.py:1477 msgid "Path to the file" msgstr "" -#: src/skit/cli.py:1496 +#: src/skit/cli.py:1484 msgid "Add a script, executable, prompt, or command to skit." msgstr "" -#: src/skit/cli.py:1498 +#: src/skit/cli.py:1486 #, python-brace-format msgid "Examples: skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" msgstr "" -#: src/skit/cli.py:1505 +#: src/skit/cli.py:1493 msgid "Path to a script, executable, or prompt, or '-' to read one from stdin (omit it in a terminal and skit asks what to add)" msgstr "" -#: src/skit/cli.py:1510 +#: src/skit/cli.py:1498 msgid "Name / alias (defaults to the file name)" msgstr "" -#: src/skit/cli.py:1516 +#: src/skit/cli.py:1504 msgid "Description (inferred from the source when possible)" msgstr "" -#: src/skit/cli.py:1519 +#: src/skit/cli.py:1507 msgid "Write a brand-new script in your editor, then add it" msgstr "" -#: src/skit/cli.py:1524 +#: src/skit/cli.py:1512 msgid "Reference mode: link to the original file instead of copying it" msgstr "" -#: src/skit/cli.py:1529 +#: src/skit/cli.py:1517 msgid "Force the executable kind (normally inferred from the file itself)" msgstr "" -#: src/skit/cli.py:1534 +#: src/skit/cli.py:1522 msgid "Force the language kind (e.g. shell, js) for an extensionless file" msgstr "" -#: src/skit/cli.py:1537 +#: src/skit/cli.py:1525 #, python-brace-format msgid "Register a command template, e.g. --cmd 'ffmpeg -i {input}'" msgstr "" -#: src/skit/cli.py:1543 +#: src/skit/cli.py:1531 msgid "Add the file as a prompt for an AI agent (with no path: draft one in your editor)" msgstr "" -#: src/skit/cli.py:1549 +#: src/skit/cli.py:1537 msgid "Pin the agent a prompt entry runs with (see skit runner list)" msgstr "" -#: src/skit/cli.py:1555 +#: src/skit/cli.py:1543 msgid "Prompt only: no variable insertion at all — the body travels exactly as written" msgstr "" -#: src/skit/cli.py:1561 +#: src/skit/cli.py:1549 msgid "A dependency (repeat for more; skips the interactive question)" msgstr "" -#: src/skit/cli.py:1564 src/skit/cli.py:4884 +#: src/skit/cli.py:1552 src/skit/cli.py:4912 msgid "Python version constraint, e.g. \">=3.11\"" msgstr "" -#: src/skit/cli.py:1567 +#: src/skit/cli.py:1555 msgid "Never prompt; accept the detected suggestions" msgstr "" -#: src/skit/cli.py:1576 +#: src/skit/cli.py:1564 msgid "--prompt names the kind outright — drop --edit/--exe/--kind/--cmd." msgstr "" -#: src/skit/cli.py:1589 +#: src/skit/cli.py:1577 msgid "stdin ('-')" msgstr "" -#: src/skit/cli.py:1590 +#: src/skit/cli.py:1578 msgid "a file path" msgstr "" -#: src/skit/cli.py:1598 +#: src/skit/cli.py:1586 #, python-format msgid "%(flags)s each pick a different way to add — use exactly one (nothing was added)." msgstr "" -#: src/skit/cli.py:1639 +#: src/skit/cli.py:1627 msgid "a --cmd template takes only --name/--description" msgstr "" -#: src/skit/cli.py:1642 +#: src/skit/cli.py:1630 msgid "stdin authors a brand-new copy, and --ref/--exe need an existing file" msgstr "" -#: src/skit/cli.py:1644 +#: src/skit/cli.py:1632 msgid "--edit drafts a fresh script: its kind comes from the shebang you write (e.g. #!/usr/bin/env bash), --ref/--exe need an existing file, and a prompt is drafted with skit add --prompt" msgstr "" -#: src/skit/cli.py:1649 +#: src/skit/cli.py:1637 msgid "a drafted prompt takes only --name/--description/--runner/--no-interpolate" msgstr "" -#: src/skit/cli.py:1657 +#: src/skit/cli.py:1645 #, python-format msgid "%(flags)s can't apply here — %(hint)s (nothing was added)." msgstr "" -#: src/skit/cli.py:1670 src/skit/cli.py:1988 +#: src/skit/cli.py:1658 src/skit/cli.py:1976 msgid "--no-interpolate only applies to prompt entries — add one with --prompt." msgstr "" -#: src/skit/cli.py:1675 src/skit/cli.py:1983 +#: src/skit/cli.py:1663 src/skit/cli.py:1971 msgid "--runner only applies to prompt entries — add one with --prompt." msgstr "" -#: src/skit/cli.py:1690 +#: src/skit/cli.py:1678 msgid "--exe needs an existing program on disk — stdin and the editor author scripts." msgstr "" -#: src/skit/cli.py:1699 +#: src/skit/cli.py:1687 msgid "--dep/--python are script flags — a prompt has no dependencies." msgstr "" -#: src/skit/cli.py:1708 +#: src/skit/cli.py:1696 msgid "--runner/--no-interpolate only apply to prompt entries — pipe one with --prompt." msgstr "" -#: src/skit/cli.py:1726 +#: src/skit/cli.py:1714 msgid "The piped text's #! names no interpreter skit knows — pass --kind to choose one." msgstr "" -#: src/skit/cli.py:1739 +#: src/skit/cli.py:1727 #, python-format msgid "--dep/--python don't apply to a %(kind)s entry (piped text's kind)." msgstr "" -#: src/skit/cli.py:1759 +#: src/skit/cli.py:1747 msgid "A --cmd entry needs a --name" msgstr "" -#: src/skit/cli.py:1779 +#: src/skit/cli.py:1767 msgid "Provide a source path — or pipe the text in (skit add - -n NAME; add --prompt for an AI-agent prompt), or register a command template with --cmd." msgstr "" -#: src/skit/cli.py:1811 +#: src/skit/cli.py:1799 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …), or pick a lane outright with %(lanes)s (nothing was added)." msgstr "" -#: src/skit/cli.py:1818 +#: src/skit/cli.py:1806 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …) (nothing was added)." msgstr "" -#: src/skit/cli.py:1840 +#: src/skit/cli.py:1828 msgid "Use --kind or --exe, not both." msgstr "" -#: src/skit/cli.py:1868 +#: src/skit/cli.py:1856 #, python-format msgid "%(file)s is a directory — pass --exe to add it as a program that runs directly." msgstr "" -#: src/skit/cli.py:1876 +#: src/skit/cli.py:1864 #, python-format msgid "%(file)s is a directory. Add it as a program that runs directly?" msgstr "" -#: src/skit/cli.py:1910 +#: src/skit/cli.py:1898 #, python-format msgid "%(file)s is one of skit's own kept drafts — a resumed draft is always added as a copy (and consumed on success), which a reference or program entry can't be. Drop %(flags)s." msgstr "" -#: src/skit/cli.py:1917 +#: src/skit/cli.py:1905 #, python-format msgid "%(file)s is one of skit's own kept drafts, and a draft is always added as a script or prompt copy — pass --kind to name its language." msgstr "" -#: src/skit/cli.py:1941 +#: src/skit/cli.py:1929 #, python-format msgid "%(file)s looks like a prompt. Add it as one?" msgstr "" -#: src/skit/cli.py:2034 +#: src/skit/cli.py:2022 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one." msgstr "" -#: src/skit/cli.py:2039 +#: src/skit/cli.py:2027 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one, or --exe to run it directly." msgstr "" -#: src/skit/cli.py:2051 +#: src/skit/cli.py:2039 #, python-format msgid "%(file)s is a kept draft skit can't classify — pass --kind to add it as a script, or --prompt for an AI-agent prompt." msgstr "" -#: src/skit/cli.py:2060 +#: src/skit/cli.py:2048 #, python-format msgid "%(file)s isn't a script or an executable — pass --kind for an extensionless script, --prompt for an AI-agent prompt, --exe for a program, or --cmd for a command template." msgstr "" -#: src/skit/cli.py:2210 +#: src/skit/cli.py:2198 #, python-format msgid "(%(mode)s mode)" msgstr "" -#: src/skit/cli.py:2215 +#: src/skit/cli.py:2203 #, python-format msgid "Added: %(name)s" msgstr "" -#: src/skit/cli.py:2219 +#: src/skit/cli.py:2207 #, python-format msgid "Description: %(desc)s" msgstr "" -#: src/skit/cli.py:2223 src/skit/cli.py:2381 +#: src/skit/cli.py:2211 src/skit/cli.py:2369 #, python-format msgid "Dependencies: %(deps)s" msgstr "" -#: src/skit/cli.py:2227 +#: src/skit/cli.py:2215 #, python-format msgid "Managed parameters: %(names)s" msgstr "" -#: src/skit/cli.py:2229 src/skit/cli.py:2422 +#: src/skit/cli.py:2217 src/skit/cli.py:2410 #, python-format msgid "Run it: skit run %(name)s" msgstr "" -#: src/skit/cli.py:2232 +#: src/skit/cli.py:2220 #, python-format msgid "Secret parameter values are never saved by skit: %(names)s" msgstr "" -#: src/skit/cli.py:2238 +#: src/skit/cli.py:2226 msgid "When this prompt runs, the selected agent receives those values as plaintext and may log or sync them." msgstr "" -#: src/skit/cli.py:2250 +#: src/skit/cli.py:2238 msgid "List every registered entry." msgstr "" -#: src/skit/cli.py:2252 src/skit/cli.py:2463 src/skit/cli.py:3247 -#: src/skit/cli.py:3624 src/skit/cli.py:4896 src/skit/cli.py:5123 -#: src/skit/cli.py:5467 +#: src/skit/cli.py:2240 src/skit/cli.py:2451 src/skit/cli.py:3250 +#: src/skit/cli.py:3627 src/skit/cli.py:4924 src/skit/cli.py:5151 +#: src/skit/cli.py:5495 msgid "Output as JSON" msgstr "" -#: src/skit/cli.py:2275 +#: src/skit/cli.py:2263 msgid "No entries yet. Add one with: skit add " msgstr "" -#: src/skit/cli.py:2280 src/skit/tui.py:370 src/skit/tui.py:1059 -#: src/skit/tui_add.py:638 src/skit/tui_add.py:855 src/skit/tui_add.py:1328 +#: src/skit/cli.py:2268 src/skit/tui.py:377 src/skit/tui.py:1088 +#: src/skit/tui_add.py:639 src/skit/tui_add.py:865 src/skit/tui_add.py:1344 msgid "Name" msgstr "" -#: src/skit/cli.py:2281 src/skit/cli.py:3942 src/skit/tui.py:370 -#: src/skit/tui.py:1059 +#: src/skit/cli.py:2269 src/skit/cli.py:3974 src/skit/tui.py:377 +#: src/skit/tui.py:1088 msgid "Kind" msgstr "" -#: src/skit/cli.py:2282 src/skit/tui_add.py:640 src/skit/tui_add.py:857 -#: src/skit/tui_add.py:1333 +#: src/skit/cli.py:2270 src/skit/tui_add.py:641 src/skit/tui_add.py:867 +#: src/skit/tui_add.py:1349 msgid "Description" msgstr "" -#: src/skit/cli.py:2307 src/skit/cli.py:2308 src/skit/cli.py:2448 -#: src/skit/cli.py:3689 src/skit/cli.py:3690 +#: src/skit/cli.py:2295 src/skit/cli.py:2296 src/skit/cli.py:2436 +#: src/skit/cli.py:3716 src/skit/cli.py:3717 msgid "yes" msgstr "" -#: src/skit/cli.py:2363 +#: src/skit/cli.py:2351 #, python-format msgid "Source: %(path)s" msgstr "" -#: src/skit/cli.py:2366 +#: src/skit/cli.py:2354 #, python-format msgid "Working directory: %(dir)s" msgstr "" -#: src/skit/cli.py:2370 +#: src/skit/cli.py:2358 #, python-format msgid "Interpreter: %(program)s" msgstr "" -#: src/skit/cli.py:2385 src/skit/cli.py:4862 +#: src/skit/cli.py:2373 src/skit/cli.py:4890 #, python-format msgid "Python constraint: %(python)s" msgstr "" -#: src/skit/cli.py:2389 +#: src/skit/cli.py:2377 #, python-format msgid "Needs: %(needs)s" msgstr "" -#: src/skit/cli.py:2393 +#: src/skit/cli.py:2381 #, python-format msgid "Command template: %(template)s" msgstr "" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 #, python-format msgid "Runner: %(runner)s" msgstr "" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 msgid "(asks at run time)" msgstr "" -#: src/skit/cli.py:2400 +#: src/skit/cli.py:2388 msgid "Variable insertion: off (the body travels as written)" msgstr "" -#: src/skit/cli.py:2404 src/skit/cli.py:3067 +#: src/skit/cli.py:2392 src/skit/cli.py:3061 msgid "skit could not model this script's own arguments; pass them after -- instead." msgstr "" -#: src/skit/cli.py:2410 +#: src/skit/cli.py:2398 msgid "No form fields — arguments after -- go to the selected agent." msgstr "" -#: src/skit/cli.py:2412 +#: src/skit/cli.py:2400 msgid "No form fields — arguments after -- are appended to the command." msgstr "" -#: src/skit/cli.py:2415 +#: src/skit/cli.py:2403 msgid "No form fields — arguments after -- pass straight through to the script." msgstr "" -#: src/skit/cli.py:2420 +#: src/skit/cli.py:2408 #, python-format msgid "Presets: %(names)s" msgstr "" -#: src/skit/cli.py:2429 src/skit/cli.py:3728 src/skit/cli.py:3941 +#: src/skit/cli.py:2417 src/skit/cli.py:3755 src/skit/cli.py:3973 msgid "Parameter" msgstr "" -#: src/skit/cli.py:2430 src/skit/cli.py:3730 src/skit/cli.py:3943 +#: src/skit/cli.py:2418 src/skit/cli.py:3757 src/skit/cli.py:3975 msgid "Type" msgstr "" -#: src/skit/cli.py:2431 +#: src/skit/cli.py:2419 msgid "Required" msgstr "" -#: src/skit/cli.py:2432 src/skit/cli.py:3731 src/skit/cli.py:3944 +#: src/skit/cli.py:2420 src/skit/cli.py:3758 src/skit/cli.py:3976 msgid "Default" msgstr "" -#: src/skit/cli.py:2433 +#: src/skit/cli.py:2421 msgid "Choices" msgstr "" -#: src/skit/cli.py:2434 src/skit/cli.py:3732 src/skit/cli.py:3945 +#: src/skit/cli.py:2422 src/skit/cli.py:3759 src/skit/cli.py:3977 msgid "Secret" msgstr "" -#: src/skit/cli.py:2435 src/skit/tui.py:340 src/skit/tui.py:486 +#: src/skit/cli.py:2423 src/skit/tui.py:343 src/skit/tui.py:493 msgid "Help" msgstr "" -#: src/skit/cli.py:2440 src/skit/cli.py:3696 src/skit/cli.py:3704 -#: src/skit/cli.py:3714 src/skit/cli.py:3949 src/skit/cli.py:3954 +#: src/skit/cli.py:2428 src/skit/cli.py:3723 src/skit/cli.py:3731 +#: src/skit/cli.py:3741 src/skit/cli.py:3981 src/skit/cli.py:3986 msgid "•••" msgstr "" -#: src/skit/cli.py:2458 +#: src/skit/cli.py:2446 msgid "Show everything about one entry: metadata, dependencies, parameters, presets." msgstr "" -#: src/skit/cli.py:2459 +#: src/skit/cli.py:2447 msgid "Examples: skit show resize · skit show resize --json" msgstr "" -#: src/skit/cli.py:2520 +#: src/skit/cli.py:2508 msgid "Remove a registered entry (an original source file is left untouched)." msgstr "" -#: src/skit/cli.py:2523 src/skit/cli.py:3421 +#: src/skit/cli.py:2511 src/skit/cli.py:3424 src/skit/cli.py:3655 msgid "Skip confirmation" msgstr "" -#: src/skit/cli.py:2533 src/skit/tui.py:149 +#: src/skit/cli.py:2512 src/skit/cli.py:3425 src/skit/cli.py:3656 +msgid "Never prompt" +msgstr "" + +#: src/skit/cli.py:2523 +msgid "Confirmation is required; pass --yes to remove the entry." +msgstr "" + +#: src/skit/cli.py:2527 src/skit/tui.py:149 #, python-format msgid "Remove \"%(name)s\"?" msgstr "" -#: src/skit/cli.py:2535 +#: src/skit/cli.py:2529 #, python-format msgid "Remove \"%(name)s\"? Your original file will not be deleted." msgstr "" -#: src/skit/cli.py:2546 +#: src/skit/cli.py:2540 #, python-format msgid "Removed: %(name)s" msgstr "" -#: src/skit/cli.py:2549 +#: src/skit/cli.py:2543 msgid "Rename an entry (presets, remembered values and history survive)." msgstr "" -#: src/skit/cli.py:2552 +#: src/skit/cli.py:2546 msgid "The new name" msgstr "" -#: src/skit/cli.py:2561 +#: src/skit/cli.py:2555 #, python-format msgid "Renamed to %(name)s." msgstr "" -#: src/skit/cli.py:2565 +#: src/skit/cli.py:2559 msgid "Set an entry's description (shown in the Library and skit list)." msgstr "" -#: src/skit/cli.py:2568 +#: src/skit/cli.py:2562 msgid "The description (empty text clears it)" msgstr "" -#: src/skit/cli.py:2578 +#: src/skit/cli.py:2572 #, python-format msgid "Description updated for %(name)s." msgstr "" -#: src/skit/cli.py:2582 +#: src/skit/cli.py:2576 #, python-format msgid "Description cleared for %(name)s." msgstr "" -#: src/skit/cli.py:2590 +#: src/skit/cli.py:2584 #, python-format msgid "No editable entry named %(name)s." msgstr "" -#: src/skit/cli.py:2594 +#: src/skit/cli.py:2588 #, python-format msgid "No editable entry named \"%(name)s\". Create a script now?" msgstr "" -#: src/skit/cli.py:2622 src/skit/cli.py:3812 +#: src/skit/cli.py:2616 src/skit/cli.py:3839 #, python-format msgid "Detected but not yet managed: %(names)s … and %(count)d more candidate (use --add to manage them)" msgid_plural "Detected but not yet managed: %(names)s … and %(count)d more candidates (use --add to manage them)" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:2630 src/skit/cli.py:3820 +#: src/skit/cli.py:2624 src/skit/cli.py:3847 #, python-format msgid "Detected but not yet managed: %(names)s (use --add to manage them)" msgstr "" -#: src/skit/cli.py:2655 +#: src/skit/cli.py:2649 msgid "Manage which? (all / none / numbers like 1,3)" msgstr "" -#: src/skit/cli.py:2671 src/skit/tui.py:930 +#: src/skit/cli.py:2665 src/skit/tui.py:959 #, python-format msgid "Now managed: %(names)s" msgstr "" -#: src/skit/cli.py:2677 +#: src/skit/cli.py:2671 msgid "Open a script or prompt source in your editor (offers to create a script if the name is new)." msgstr "" -#: src/skit/cli.py:2679 +#: src/skit/cli.py:2673 msgid "Example: skit edit resize" msgstr "" -#: src/skit/cli.py:2691 src/skit/tui.py:884 +#: src/skit/cli.py:2685 src/skit/tui.py:913 #, python-format msgid "%(name)s has no editable source (programs and command templates run as-is)." msgstr "" -#: src/skit/cli.py:2699 +#: src/skit/cli.py:2693 #, python-format msgid "%(name)s: the referenced source file is gone: %(path)s" msgstr "" -#: src/skit/cli.py:2704 +#: src/skit/cli.py:2698 #, python-format msgid "Editing the original file (reference mode): %(path)s" msgstr "" -#: src/skit/cli.py:2711 src/skit/cli.py:4484 src/skit/cli.py:4626 +#: src/skit/cli.py:2705 src/skit/cli.py:4520 src/skit/cli.py:4652 #, python-format msgid "%(name)s has no stored copy to edit." msgstr "" -#: src/skit/cli.py:2718 +#: src/skit/cli.py:2712 #, python-format msgid "Saved %(name)s." msgstr "" -#: src/skit/cli.py:2726 +#: src/skit/cli.py:2720 #, python-format msgid "skit reconciles parameter drift at run time; review managed parameters with: skit params %(name)s" msgstr "" -#: src/skit/cli.py:2742 src/skit/cli.py:3666 +#: src/skit/cli.py:2736 src/skit/cli.py:3693 #, python-format msgid "Unknown preset \"%(preset)s\". Available: %(presets)s" msgstr "" -#: src/skit/cli.py:2772 +#: src/skit/cli.py:2766 #, python-format msgid "Malformed --set (expected NAME=VALUE): %(items)s" msgstr "" -#: src/skit/cli.py:2782 +#: src/skit/cli.py:2776 #, python-format msgid "Unknown parameter for --set: %(names)s. This entry's parameters: %(valid)s" msgstr "" -#: src/skit/cli.py:2833 +#: src/skit/cli.py:2827 #, python-format msgid "Parameters for %(name)s (press Enter to keep the value shown):" msgstr "" -#: src/skit/cli.py:2876 src/skit/cli.py:4378 +#: src/skit/cli.py:2870 src/skit/cli.py:4410 msgid "--runner only applies to prompt entries." msgstr "" -#: src/skit/cli.py:2896 src/skit/cli.py:3280 src/skit/cli.py:3312 +#: src/skit/cli.py:2890 src/skit/cli.py:3283 src/skit/cli.py:3315 #: src/skit/langs/launch.py:663 msgid "No agents are configured. Add one with: skit runner add mycli -- mycli run {{prompt}}" msgstr "" -#: src/skit/cli.py:2904 src/skit/langs/launch.py:669 +#: src/skit/cli.py:2898 src/skit/langs/launch.py:669 #, python-format msgid "No runner selected for %(name)s. Pass --runner NAME, or pin one with: skit params %(name)s --runner NAME" msgstr "" -#: src/skit/cli.py:2914 src/skit/cli.py:3106 src/skit/cli.py:4385 +#: src/skit/cli.py:2908 src/skit/cli.py:3100 src/skit/cli.py:4417 #: src/skit/langs/launch.py:678 #, python-format msgid "The runner %(runner)s isn't configured (known: %(names)s). Manage runners with: skit runner list" msgstr "" -#: src/skit/cli.py:2926 +#: src/skit/cli.py:2920 msgid "Run a registered entry in the terminal." msgstr "" -#: src/skit/cli.py:2928 +#: src/skit/cli.py:2922 msgid "Examples: skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" msgstr "" -#: src/skit/cli.py:2933 +#: src/skit/cli.py:2927 msgid "Arguments passed through after --" msgstr "" -#: src/skit/cli.py:2935 +#: src/skit/cli.py:2929 msgid "Never prompt; reuse last values and defaults" msgstr "" -#: src/skit/cli.py:2941 +#: src/skit/cli.py:2935 msgid "Named preset of parameter values to prefill the form with" msgstr "" -#: src/skit/cli.py:2948 +#: src/skit/cli.py:2942 #, python-brace-format msgid "Set a parameter value by name, as NAME=VALUE (repeatable; values may use tokens like {cwd} or {env:VAR}; the form no longer asks for a field you set)" msgstr "" -#: src/skit/cli.py:2952 +#: src/skit/cli.py:2946 msgid "Save this run's values as a named preset" msgstr "" -#: src/skit/cli.py:2955 +#: src/skit/cli.py:2949 msgid "Line-by-line prompts instead of the inline form" msgstr "" -#: src/skit/cli.py:2961 +#: src/skit/cli.py:2955 msgid "Skip the parameter form and injection and run the script as-is (escape hatch)" msgstr "" -#: src/skit/cli.py:2968 +#: src/skit/cli.py:2962 msgid "Print the exact command that would run (tokens and globs expanded), then exit" msgstr "" -#: src/skit/cli.py:2974 +#: src/skit/cli.py:2968 msgid "Run a prompt entry with this agent (overrides its pin for one run)" msgstr "" -#: src/skit/cli.py:2981 +#: src/skit/cli.py:2975 msgid "Forget the remembered extra arguments before this run (they are otherwise reused when you pass none)" msgstr "" -#: src/skit/cli.py:2998 +#: src/skit/cli.py:2992 msgid "--raw runs the script as-is; --set, --preset, and --save-preset do not apply." msgstr "" -#: src/skit/cli.py:3013 +#: src/skit/cli.py:3007 msgid "--raw doesn't apply to a prompt entry: its {{placeholders}} are the artifact itself — there is no as-is without them." msgstr "" -#: src/skit/cli.py:3018 +#: src/skit/cli.py:3012 #, python-brace-format, python-format msgid "--raw doesn't apply to a %(kind)s entry: its {placeholders} are the artifact itself — there is no as-is without them." msgstr "" -#: src/skit/cli.py:3045 +#: src/skit/cli.py:3039 msgid "Raw mode: skipping the parameter form and injection." msgstr "" -#: src/skit/cli.py:3059 src/skit/cli.py:3562 src/skit/tui_form.py:935 +#: src/skit/cli.py:3053 src/skit/cli.py:3565 src/skit/tui_form.py:935 #, python-format msgid "%(name)s has no form fields, so there's nothing to save." msgstr "" -#: src/skit/cli.py:3139 +#: src/skit/cli.py:3140 #, python-format msgid "Reusing your last arguments: %(args)s" msgstr "" -#: src/skit/cli.py:3166 src/skit/cli.py:3617 +#: src/skit/cli.py:3167 src/skit/cli.py:3620 #, python-format msgid "Preset \"%(preset)s\" saved for %(name)s." msgstr "" -#: src/skit/cli.py:3227 +#: src/skit/cli.py:3230 #, python-format msgid "Run exited with code %(code)s" msgstr "" -#: src/skit/cli.py:3237 +#: src/skit/cli.py:3240 msgid "Manage the agents (runners) that prompt entries run with." msgstr "" -#: src/skit/cli.py:3244 +#: src/skit/cli.py:3247 msgid "List the configured runners (seeds them into config on first use)." msgstr "" -#: src/skit/cli.py:3251 +#: src/skit/cli.py:3254 msgid "Include malformed raw rows and their repair indexes" msgstr "" -#: src/skit/cli.py:3288 +#: src/skit/cli.py:3291 msgid "Row" msgstr "" -#: src/skit/cli.py:3289 src/skit/cli.py:3320 +#: src/skit/cli.py:3292 src/skit/cli.py:3323 msgid "Runner" msgstr "" -#: src/skit/cli.py:3290 src/skit/cli.py:3321 src/skit/kindnames.py:29 +#: src/skit/cli.py:3293 src/skit/cli.py:3324 src/skit/kindnames.py:29 msgid "Command" msgstr "" -#: src/skit/cli.py:3291 +#: src/skit/cli.py:3294 msgid "Status" msgstr "" -#: src/skit/cli.py:3294 +#: src/skit/cli.py:3297 msgid "container" msgstr "" -#: src/skit/cli.py:3328 +#: src/skit/cli.py:3331 msgid "The built-in amp preset uses amp -x and runs the prompt once; it does not open an interactive session." msgstr "" -#: src/skit/cli.py:3345 +#: src/skit/cli.py:3348 msgid "A runner needs a command — e.g. skit runner add mycli mycli run {{prompt}}" msgstr "" -#: src/skit/cli.py:3348 +#: src/skit/cli.py:3351 msgid "A runner command must contain the {{prompt}} slot exactly once — that's where the rendered prompt lands." msgstr "" -#: src/skit/cli.py:3352 src/skit/config.py:614 src/skit/tui_runner.py:42 +#: src/skit/cli.py:3355 src/skit/config.py:614 src/skit/tui_runner.py:42 msgid "{{prompt}} can't be the command itself — the first word must be the program to run." msgstr "" -#: src/skit/cli.py:3355 src/skit/config.py:617 src/skit/tui_runner.py:45 +#: src/skit/cli.py:3358 src/skit/config.py:617 src/skit/tui_runner.py:45 msgid "Runner commands take only the {{prompt}} slot — single-brace text is literal, and other {{holes}} aren't supported." msgstr "" -#: src/skit/cli.py:3364 +#: src/skit/cli.py:3367 msgid "Register a runner: skit runner add NAME COMMAND… ({{prompt}} marks where the rendered prompt goes; each shell word becomes one argument, no shell involved)" msgstr "" -#: src/skit/cli.py:3373 +#: src/skit/cli.py:3376 msgid "Runner name (e.g. claude)" msgstr "" -#: src/skit/cli.py:3375 +#: src/skit/cli.py:3378 msgid "The command, word by word, with one {{prompt}} slot" msgstr "" -#: src/skit/cli.py:3380 +#: src/skit/cli.py:3383 msgid "Replace the runner if the name already exists (the edit path)" msgstr "" -#: src/skit/cli.py:3395 +#: src/skit/cli.py:3398 #, python-format msgid "The runner %(name)s already exists — pass --force to replace its command." msgstr "" -#: src/skit/cli.py:3403 +#: src/skit/cli.py:3406 #, python-format msgid "Runner %(name)s updated: %(command)s" msgstr "" -#: src/skit/cli.py:3407 +#: src/skit/cli.py:3410 #, python-format msgid "Runner %(name)s added: %(command)s" msgstr "" -#: src/skit/cli.py:3411 +#: src/skit/cli.py:3414 msgid "Remove a configured runner." msgstr "" -#: src/skit/cli.py:3414 +#: src/skit/cli.py:3417 msgid "Runner name" msgstr "" -#: src/skit/cli.py:3419 -msgid "Remove one raw row index from 'runner list --all' (or 'container')" -msgstr "" - #: src/skit/cli.py:3422 -msgid "Never prompt" +msgid "Remove one raw row index from 'runner list --all' (or 'container')" msgstr "" -#: src/skit/cli.py:3425 +#: src/skit/cli.py:3428 msgid "Pass exactly one runner name or --row INDEX." msgstr "" -#: src/skit/cli.py:3445 src/skit/cli.py:3449 +#: src/skit/cli.py:3448 src/skit/cli.py:3452 msgid "--row must be a non-negative index or 'container'." msgstr "" -#: src/skit/cli.py:3454 +#: src/skit/cli.py:3457 #, python-format msgid "Unknown runner row: %(row)s. Inspect with: skit runner list --all" msgstr "" -#: src/skit/cli.py:3462 +#: src/skit/cli.py:3465 #, python-format msgid "Runner row %(row)s is valid. Remove the agent by name instead: skit runner remove \"%(name)s\"" msgstr "" -#: src/skit/cli.py:3471 src/skit/tui_runner.py:315 +#: src/skit/cli.py:3474 src/skit/tui_runner.py:315 msgid "Remove the malformed prompt runner container?" msgstr "" -#: src/skit/cli.py:3473 +#: src/skit/cli.py:3476 #, python-format msgid "Remove runner row %(row)d (\"%(name)s\")?" msgstr "" -#: src/skit/cli.py:3486 src/skit/tui_runner.py:321 +#: src/skit/cli.py:3489 src/skit/tui_runner.py:321 #, python-format msgid "Remove the agent \"%(name)s\"?" msgstr "" -#: src/skit/cli.py:3496 src/skit/tui_runner.py:326 +#: src/skit/cli.py:3499 src/skit/tui_runner.py:326 #, python-format msgid "%(count)d prompt pins this runner and will need another runner before it can run again." msgid_plural "%(count)d prompts pin this runner and will need another runner before they can run again." msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:3505 +#: src/skit/cli.py:3508 msgid "Confirmation is required; pass --yes to remove the runner." msgstr "" -#: src/skit/cli.py:3520 +#: src/skit/cli.py:3523 msgid "The runner row changed before it could be removed; inspect again." msgstr "" -#: src/skit/cli.py:3523 +#: src/skit/cli.py:3526 #, python-format msgid "Runner %(name)s removed." msgstr "" -#: src/skit/cli.py:3526 +#: src/skit/cli.py:3529 msgid "Malformed prompt runner container removed." msgstr "" -#: src/skit/cli.py:3529 +#: src/skit/cli.py:3532 #, python-format msgid "Malformed runner row %(row)d removed." msgstr "" -#: src/skit/cli.py:3538 +#: src/skit/cli.py:3541 msgid "Manage named parameter presets for an entry." msgstr "" -#: src/skit/cli.py:3543 +#: src/skit/cli.py:3546 msgid "Save a set of parameter values as a named preset." msgstr "" -#: src/skit/cli.py:3546 src/skit/cli.py:3651 src/skit/tui_form.py:343 +#: src/skit/cli.py:3549 src/skit/cli.py:3654 src/skit/tui_form.py:343 msgid "Preset name" msgstr "" -#: src/skit/cli.py:3550 +#: src/skit/cli.py:3553 msgid "Save the last run's values without asking (automation-friendly)" msgstr "" -#: src/skit/cli.py:3570 src/skit/cli.py:3586 +#: src/skit/cli.py:3573 src/skit/cli.py:3589 #, python-format msgid "%(name)s has no remembered values yet — run it once first." msgstr "" -#: src/skit/cli.py:3604 +#: src/skit/cli.py:3607 #, python-format msgid "Secret values are never stored in presets; skipped: %(names)s" msgstr "" -#: src/skit/cli.py:3621 +#: src/skit/cli.py:3624 msgid "List an entry's saved presets." msgstr "" -#: src/skit/cli.py:3638 +#: src/skit/cli.py:3641 #, python-format msgid "No presets for %(name)s yet. Create one with: skit run %(name)s --save-preset " msgstr "" -#: src/skit/cli.py:3648 +#: src/skit/cli.py:3651 msgid "Delete a named preset from an entry." msgstr "" -#: src/skit/cli.py:3660 +#: src/skit/cli.py:3673 +msgid "Confirmation is required; pass --yes to delete the preset." +msgstr "" + +#: src/skit/cli.py:3677 +#, python-format +msgid "Delete preset \"%(preset)s\" from %(name)s?" +msgstr "" + +#: src/skit/cli.py:3683 #, python-format msgid "Preset \"%(preset)s\" deleted from %(name)s." msgstr "" -#: src/skit/cli.py:3715 +#: src/skit/cli.py:3742 #, python-format msgid "default %(value)s" msgstr "" -#: src/skit/cli.py:3717 +#: src/skit/cli.py:3744 msgid "optional" msgstr "" -#: src/skit/cli.py:3719 +#: src/skit/cli.py:3746 msgid "secret" msgstr "" -#: src/skit/cli.py:3729 +#: src/skit/cli.py:3756 msgid "Delivery" msgstr "" -#: src/skit/cli.py:3733 src/skit/cli.py:3946 +#: src/skit/cli.py:3760 src/skit/cli.py:3978 msgid "Last value" msgstr "" -#: src/skit/cli.py:3777 +#: src/skit/cli.py:3804 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written. Turn it on with: skit params %(name)s --interpolate" msgstr "" -#: src/skit/cli.py:3788 src/skit/cli.py:3917 src/skit/cli.py:3926 +#: src/skit/cli.py:3815 src/skit/cli.py:3949 src/skit/cli.py:3958 #, python-format msgid "%(name)s has no managed parameters." msgstr "" -#: src/skit/cli.py:3793 +#: src/skit/cli.py:3820 msgid "Prompt placeholders (the run form asks for them):" msgstr "" -#: src/skit/cli.py:3795 +#: src/skit/cli.py:3822 msgid "Command template placeholders (the run form asks for them):" msgstr "" -#: src/skit/cli.py:3802 +#: src/skit/cli.py:3829 msgid "Declared environment variables (set on the run):" msgstr "" -#: src/skit/cli.py:3825 +#: src/skit/cli.py:3852 #, python-format msgid "No longer in the prompt (the value would be ignored): %(names)s — remove with --rm, or edit the body." msgstr "" -#: src/skit/cli.py:3932 +#: src/skit/cli.py:3964 #, python-format msgid "%(name)s has no managed parameters. Use --manage to bring a detected candidate under management." msgstr "" -#: src/skit/cli.py:3970 +#: src/skit/cli.py:4002 #, python-format msgid "Detected but not yet managed: %(names)s" msgstr "" -#: src/skit/cli.py:3977 +#: src/skit/cli.py:4009 msgid "Reference mode: skit never writes the original file — manage parameters by editing its [tool.skit] block in the source directly." msgstr "" -#: src/skit/cli.py:3985 +#: src/skit/cli.py:4017 #, python-format msgid "Detected but not yet managed: %(names)s (use --manage to manage them)" msgstr "" -#: src/skit/cli.py:3990 +#: src/skit/cli.py:4022 #, python-brace-format, python-format msgid "This script locates itself ($0 / BASH_SOURCE). Injecting a constant runs it from a temporary copy, so it would see that copy path instead. Rewriting the constant as NAME=\"${NAME:-value}\" delivers the value through the environment with no copy at all — `skit params %(name)s --normalize NAME` does the rewrite for you on the stored copy." msgstr "" -#: src/skit/cli.py:4009 +#: src/skit/cli.py:4041 msgid "Show or edit an entry's managed or declared parameters." msgstr "" -#: src/skit/cli.py:4011 +#: src/skit/cli.py:4043 msgid "Examples: skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" msgstr "" -#: src/skit/cli.py:4019 +#: src/skit/cli.py:4051 msgid "Prune definitions that no longer match the script and refresh changed types" msgstr "" -#: src/skit/cli.py:4024 +#: src/skit/cli.py:4056 msgid "Bring a currently detected candidate under management (repeatable)" msgstr "" -#: src/skit/cli.py:4027 +#: src/skit/cli.py:4059 msgid "Drop a managed parameter (repeatable)" msgstr "" -#: src/skit/cli.py:4032 +#: src/skit/cli.py:4064 msgid "Declare a new parameter on an exe/command entry, by name (repeatable)" msgstr "" -#: src/skit/cli.py:4035 +#: src/skit/cli.py:4067 msgid "Remove a declared parameter, by name (repeatable)" msgstr "" -#: src/skit/cli.py:4040 +#: src/skit/cli.py:4072 msgid "Set a declared parameter's type, as NAME=str|int|float|bool|choice|path" msgstr "" -#: src/skit/cli.py:4043 +#: src/skit/cli.py:4075 msgid "Set a declared parameter's default, as NAME=VALUE" msgstr "" -#: src/skit/cli.py:4048 +#: src/skit/cli.py:4080 msgid "Set a declared parameter's choices, as NAME=a,b,c (comma separated)" msgstr "" -#: src/skit/cli.py:4054 +#: src/skit/cli.py:4086 msgid "Set how a declared parameter reaches the program, as NAME=env|flag|placeholder" msgstr "" -#: src/skit/cli.py:4060 +#: src/skit/cli.py:4092 msgid "Set a declared flag parameter's option, as NAME=--out (empty = positional)" msgstr "" -#: src/skit/cli.py:4063 +#: src/skit/cli.py:4095 msgid "Mark a declared parameter as required (repeatable)" msgstr "" -#: src/skit/cli.py:4066 +#: src/skit/cli.py:4098 msgid "Mark a declared parameter as optional (repeatable)" msgstr "" -#: src/skit/cli.py:4069 +#: src/skit/cli.py:4101 msgid "Set a declared parameter's help text, as NAME=text" msgstr "" -#: src/skit/cli.py:4072 +#: src/skit/cli.py:4104 msgid "Mark a managed parameter as secret (repeatable)" msgstr "" -#: src/skit/cli.py:4077 +#: src/skit/cli.py:4109 msgid "Remove the secret mark from a managed parameter (repeatable)" msgstr "" -#: src/skit/cli.py:4080 +#: src/skit/cli.py:4112 msgid "Set a parameter's form prompt, as NAME=text (repeatable)" msgstr "" -#: src/skit/cli.py:4086 +#: src/skit/cli.py:4118 msgid "Read a secret parameter from an environment variable at run time, as NAME=ENVVAR (empty ENVVAR clears it; repeatable)" msgstr "" -#: src/skit/cli.py:4093 +#: src/skit/cli.py:4125 #, python-brace-format msgid "Shell only: rewrite a constant into the ${NAME:-default} idiom in the stored copy, so its value is delivered as an environment variable instead of a rewritten temporary copy (repeatable)" msgstr "" -#: src/skit/cli.py:4100 +#: src/skit/cli.py:4132 msgid "Prompt only: pin the agent this prompt runs with (empty value clears the pin)" msgstr "" -#: src/skit/cli.py:4108 +#: src/skit/cli.py:4140 msgid "Prompt only: turn variable insertion on/off for this prompt (off = the body travels exactly as written)" msgstr "" -#: src/skit/cli.py:4116 +#: src/skit/cli.py:4148 msgid "Set where the entry runs: origin (its own folder), store, invoke (where you run skit from), or an absolute path" msgstr "" -#: src/skit/cli.py:4123 +#: src/skit/cli.py:4155 #, python-brace-format msgid "Command only: rewrite the template ({placeholders} are re-read from it)" msgstr "" -#: src/skit/cli.py:4129 +#: src/skit/cli.py:4161 msgid "Pin the interpreter/runtime an interpreted entry runs with (e.g. zsh, bun; empty value returns to automatic)" msgstr "" -#: src/skit/cli.py:4133 +#: src/skit/cli.py:4165 msgid "Output the read view as JSON" msgstr "" -#: src/skit/cli.py:4195 +#: src/skit/cli.py:4227 #, python-format msgid "%(op)s is its own operation — run it in a separate skit params call (nothing was changed)." msgstr "" -#: src/skit/cli.py:4265 +#: src/skit/cli.py:4297 #, python-format msgid "%(name)s manages its parameters from the script itself — use --manage / --unmanage, or edit the [tool.skit] block." msgstr "" -#: src/skit/cli.py:4353 +#: src/skit/cli.py:4385 #, python-format msgid "Template updated. Placeholders: %(names)s" msgstr "" -#: src/skit/cli.py:4357 +#: src/skit/cli.py:4389 #, python-format msgid "%(name)s now runs in: %(dir)s" msgstr "" -#: src/skit/cli.py:4362 +#: src/skit/cli.py:4394 #, python-format msgid "%(name)s now runs with: %(program)s" msgstr "" -#: src/skit/cli.py:4366 +#: src/skit/cli.py:4398 #, python-format msgid "%(name)s is back to automatic interpreter detection." msgstr "" -#: src/skit/cli.py:4397 +#: src/skit/cli.py:4429 #, python-format msgid "%(name)s now runs with %(runner)s." msgstr "" -#: src/skit/cli.py:4401 +#: src/skit/cli.py:4433 #, python-format msgid "Cleared the runner pin — %(name)s asks at run time." msgstr "" -#: src/skit/cli.py:4410 +#: src/skit/cli.py:4442 msgid "--interpolate only applies to prompt entries." msgstr "" -#: src/skit/cli.py:4417 +#: src/skit/cli.py:4449 #, python-format msgid "Variable insertion is on — %(name)s fills its managed placeholders again." msgstr "" -#: src/skit/cli.py:4421 +#: src/skit/cli.py:4453 #, python-format msgid "Variable insertion is off — %(name)s travels to the agent exactly as written." msgstr "" -#: src/skit/cli.py:4433 +#: src/skit/cli.py:4465 #, python-format msgid "%(name)s isn't a managed parameter; --env-source skipped." msgstr "" -#: src/skit/cli.py:4440 src/skit/cli.py:4695 +#: src/skit/cli.py:4472 src/skit/cli.py:4723 #, python-format msgid "%(name)s isn't secret; --env-source only applies to secret parameters (mark it with --secret first)." msgstr "" -#: src/skit/cli.py:4468 +#: src/skit/cli.py:4499 #, python-format msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from." msgstr "" -#: src/skit/cli.py:4476 +#: src/skit/cli.py:4506 +#, python-format +msgid "Declare one instead: skit params %(name)s --add PARAM" +msgstr "" + +#: src/skit/cli.py:4512 #, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Edit the [tool.skit] block in the source directly." msgstr "" -#: src/skit/cli.py:4497 +#: src/skit/cli.py:4533 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=text)." msgstr "" -#: src/skit/cli.py:4536 src/skit/cli.py:4820 +#: src/skit/cli.py:4562 src/skit/cli.py:4848 #, python-format msgid "Removed previously stored plaintext value(s) for now-secret parameter(s): %(names)s" msgstr "" -#: src/skit/cli.py:4543 +#: src/skit/cli.py:4569 #, python-format msgid "Updated %(name)s. Managed parameters: %(names)s" msgstr "" -#: src/skit/cli.py:4549 +#: src/skit/cli.py:4575 #, python-format msgid "The run form now asks for the managed parameters — the script's own command-line form (%(frameworks)s) is set aside until they are removed (--unmanage)." msgstr "" -#: src/skit/cli.py:4565 +#: src/skit/cli.py:4591 #, python-format msgid "%(name)s isn't a plain constant with a literal value, so there's nothing to normalize; skipped." msgstr "" -#: src/skit/cli.py:4568 +#: src/skit/cli.py:4594 #, python-format msgid "%(name)s is assigned more than once at the top level; normalizing it would change which value wins. Skipped." msgstr "" -#: src/skit/cli.py:4571 +#: src/skit/cli.py:4597 #, python-format msgid "%(name)s is readonly, so the script could never take a value from the environment; skipped." msgstr "" -#: src/skit/cli.py:4573 +#: src/skit/cli.py:4599 #, python-format msgid "%(name)s already reads from the environment; nothing to do." msgstr "" -#: src/skit/cli.py:4575 +#: src/skit/cli.py:4601 #, python-format msgid "%(name)s's value contains a character that can't be moved into ${...:-...} safely (one of } \" ` $ \\ or a newline); skipped — it keeps being injected into a temporary copy." msgstr "" -#: src/skit/cli.py:4579 +#: src/skit/cli.py:4605 msgid "Could not parse the script (syntax error); nothing was normalized." msgstr "" -#: src/skit/cli.py:4610 +#: src/skit/cli.py:4636 #, python-brace-format, python-format msgid "%(name)s has no --normalize: it is a shell idiom (VAR=value -> VAR=\"${VAR:-value}\")." msgstr "" -#: src/skit/cli.py:4618 +#: src/skit/cli.py:4644 #, python-brace-format, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Change the line to VAR=\"${VAR:-value}\" in the source directly." msgstr "" -#: src/skit/cli.py:4637 +#: src/skit/cli.py:4663 #, python-format msgid "%(name)s isn't valid UTF-8, so --normalize can't rewrite it safely; nothing was changed — its constants keep being injected into a temporary copy." msgstr "" -#: src/skit/cli.py:4674 +#: src/skit/cli.py:4702 #, python-format msgid "Normalized %(names)s in %(name)s: delivered as environment variables from now on (no temporary copy, and $0 stays your real file)." msgstr "" -#: src/skit/cli.py:4684 +#: src/skit/cli.py:4712 #, python-format msgid "%(name)s isn't a declared parameter; skipped." msgstr "" -#: src/skit/cli.py:4685 +#: src/skit/cli.py:4713 #, python-format msgid "%(name)s is already declared; skipped." msgstr "" -#: src/skit/cli.py:4686 +#: src/skit/cli.py:4714 #, python-format msgid "%(name)s: that delivery isn't available for this kind; skipped." msgstr "" -#: src/skit/cli.py:4688 +#: src/skit/cli.py:4716 #, python-format msgid "%(name)s isn't a template placeholder, so it can't use placeholder delivery; skipped." msgstr "" -#: src/skit/cli.py:4691 +#: src/skit/cli.py:4719 #, python-format msgid "%(name)s: unknown type; skipped (use str, int, float, bool, choice, or path)." msgstr "" -#: src/skit/cli.py:4693 +#: src/skit/cli.py:4721 #, python-format msgid "%(name)s: the default doesn't fit its type; skipped." msgstr "" -#: src/skit/cli.py:4698 +#: src/skit/cli.py:4726 #, python-format msgid "%(name)s: a choice parameter needs choices; set --choices %(name)s=a,b,c." msgstr "" -#: src/skit/cli.py:4701 +#: src/skit/cli.py:4729 #, python-format msgid "%(name)s is on by default, so its flag could only ever turn it on again. Declare the flag that turns it OFF instead (--no-%(name)s and the like), with default false." msgstr "" -#: src/skit/cli.py:4757 +#: src/skit/cli.py:4785 #, python-format msgid "Variable insertion is off for %(name)s — turn it on first with: skit params %(name)s --interpolate" msgstr "" -#: src/skit/cli.py:4771 +#: src/skit/cli.py:4799 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=VALUE)." msgstr "" -#: src/skit/cli.py:4827 +#: src/skit/cli.py:4855 #, python-format msgid "Updated %(name)s. Declared parameters: %(names)s" msgstr "" -#: src/skit/cli.py:4855 +#: src/skit/cli.py:4883 #, python-format msgid "Dependencies of %(name)s: %(deps)s" msgstr "" -#: src/skit/cli.py:4864 +#: src/skit/cli.py:4892 #, python-format msgid "External commands needed by %(name)s: %(needs)s" msgstr "" -#: src/skit/cli.py:4871 +#: src/skit/cli.py:4899 msgid "View or update an entry's package dependencies, Python constraint, and needed commands." msgstr "" -#: src/skit/cli.py:4874 +#: src/skit/cli.py:4902 msgid "Examples: skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" msgstr "" -#: src/skit/cli.py:4880 +#: src/skit/cli.py:4908 msgid "A dependency (repeat for more; replaces the whole list)" msgstr "" -#: src/skit/cli.py:4882 +#: src/skit/cli.py:4910 msgid "Remove every dependency" msgstr "" -#: src/skit/cli.py:4890 +#: src/skit/cli.py:4918 msgid "An external command the entry needs on PATH (repeat; replaces the whole list)" msgstr "" -#: src/skit/cli.py:4894 +#: src/skit/cli.py:4922 msgid "Remove every needed external command" msgstr "" -#: src/skit/cli.py:4914 +#: src/skit/cli.py:4942 #, python-format msgid "%(name)s doesn't take package dependencies — only --need applies here." msgstr "" -#: src/skit/cli.py:4920 +#: src/skit/cli.py:4948 msgid "Use --dep to set the list or --clear to empty it — not both." msgstr "" -#: src/skit/cli.py:4925 +#: src/skit/cli.py:4953 msgid "Use --need to set the list or --clear-needs to empty it — not both." msgstr "" -#: src/skit/cli.py:4960 +#: src/skit/cli.py:4988 #, python-format msgid "Dependencies of %(name)s updated: %(deps)s" msgstr "" -#: src/skit/cli.py:4964 +#: src/skit/cli.py:4992 #, python-format msgid "Python constraint of %(name)s updated: %(value)s" msgstr "" -#: src/skit/cli.py:4974 +#: src/skit/cli.py:5002 #, python-format msgid "Needs of %(name)s updated: %(needs)s" msgstr "" -#: src/skit/cli.py:4987 +#: src/skit/cli.py:5015 msgid "Connect skit to AI agents: install the official Agent Skill." msgstr "" -#: src/skit/cli.py:5002 +#: src/skit/cli.py:5030 #, python-format msgid "Could not write the skill there: %(error)s" msgstr "" -#: src/skit/cli.py:5005 src/skit/tui_prefs.py:195 +#: src/skit/cli.py:5033 src/skit/tui_prefs.py:196 #, python-format msgid "Installed the skit Agent Skill: %(path)s" msgstr "" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "user" msgstr "" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "project" msgstr "" -#: src/skit/cli.py:5012 +#: src/skit/cli.py:5040 msgid "Agent directories on this machine:" msgstr "" -#: src/skit/cli.py:5016 +#: src/skit/cli.py:5044 msgid "Install where?" msgstr "" -#: src/skit/cli.py:5023 +#: src/skit/cli.py:5051 #, python-format msgid "Write the skill into %(path)s?" msgstr "" -#: src/skit/cli.py:5033 +#: src/skit/cli.py:5061 msgid "Install skit's Agent Skill into an AI agent's skills directory." msgstr "" -#: src/skit/cli.py:5035 +#: src/skit/cli.py:5063 msgid "Examples: skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" msgstr "" -#: src/skit/cli.py:5042 +#: src/skit/cli.py:5070 msgid "Where to install: claude, codex, or agents (the cross-agent ./.agents directory)" msgstr "" -#: src/skit/cli.py:5048 +#: src/skit/cli.py:5076 msgid "Install into this skills directory instead of a named target" msgstr "" -#: src/skit/cli.py:5054 +#: src/skit/cli.py:5082 msgid "Install into the current project (./.claude, ./.codex) instead of your home directory" msgstr "" -#: src/skit/cli.py:5063 +#: src/skit/cli.py:5091 msgid "Use a named target (with optional --project) or --to — not both." msgstr "" -#: src/skit/cli.py:5075 +#: src/skit/cli.py:5103 #, python-format msgid "Unknown target %(name)s. Valid targets: claude, codex, agents." msgstr "" -#: src/skit/cli.py:5084 +#: src/skit/cli.py:5112 msgid "Nothing installed: name a target (claude, codex, agents) or pass --to DIR." msgstr "" -#: src/skit/cli.py:5091 +#: src/skit/cli.py:5119 msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Pass --to DIR to choose one yourself." msgstr "" -#: src/skit/cli.py:5097 +#: src/skit/cli.py:5125 msgid "Cancelled — nothing was written." msgstr "" -#: src/skit/cli.py:5118 +#: src/skit/cli.py:5146 msgid "Check that uv is available and the entry library is intact." msgstr "" -#: src/skit/cli.py:5121 +#: src/skit/cli.py:5149 msgid "Rebuild the index from each entry's meta.toml" msgstr "" -#: src/skit/cli.py:5137 src/skit/tui_health.py:172 +#: src/skit/cli.py:5165 src/skit/tui_health.py:172 #, python-format msgid "Index rebuilt: %(count)s entry" msgid_plural "Index rebuilt: %(count)s entries" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:5183 src/skit/tui_health.py:62 +#: src/skit/cli.py:5211 src/skit/tui_health.py:62 #, python-format msgid "uv: %(path)s" msgstr "" -#: src/skit/cli.py:5186 src/skit/tui_health.py:65 +#: src/skit/cli.py:5214 src/skit/tui_health.py:65 msgid "uv: not found. Install it from https://docs.astral.sh/uv/getting-started/installation/" msgstr "" -#: src/skit/cli.py:5190 src/skit/tui_health.py:72 +#: src/skit/cli.py:5218 src/skit/tui_health.py:72 #, python-format msgid "%(count)s entry registered" msgid_plural "%(count)s entries registered" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:5195 +#: src/skit/cli.py:5223 #, python-format msgid "%(name)s: the launch target is gone from disk" msgstr "" -#: src/skit/cli.py:5199 +#: src/skit/cli.py:5227 #, python-format msgid "%(name)s: form definitions are out of sync — run: skit params %(name)s --resync" msgstr "" -#: src/skit/cli.py:5203 +#: src/skit/cli.py:5231 #, python-format msgid "%(name)s: missing external command(s): %(tools)s" msgstr "" -#: src/skit/cli.py:5209 +#: src/skit/cli.py:5237 #, python-format msgid "%(name)s: a run would refuse to start — %(reason)s" msgstr "" -#: src/skit/cli.py:5213 +#: src/skit/cli.py:5241 #, python-format msgid "Ignored malformed runner row(s) in config: %(rows)s. Inspect and repair with: skit runner list --all" msgstr "" -#: src/skit/cli.py:5217 src/skit/tui_health.py:134 +#: src/skit/cli.py:5245 src/skit/tui_health.py:134 #, python-format msgid "Library: %(path)s (%(count)s · %(size)s)" msgstr "" -#: src/skit/cli.py:5235 +#: src/skit/cli.py:5263 #, python-format msgid "auto (%(locale)s)" msgstr "" -#: src/skit/cli.py:5272 +#: src/skit/cli.py:5300 msgid "default ($VISUAL / $EDITOR)" msgstr "" -#: src/skit/cli.py:5288 +#: src/skit/cli.py:5316 msgid "auto (bash on PATH)" msgstr "" -#: src/skit/cli.py:5292 +#: src/skit/cli.py:5320 msgid "auto (deno > bun > node)" msgstr "" -#: src/skit/cli.py:5327 +#: src/skit/cli.py:5355 msgid "Mirrors are switched off — run `skit config mirror on` to activate them." msgstr "" -#: src/skit/cli.py:5340 +#: src/skit/cli.py:5368 msgid "Nothing to enable: no mirror URLs are saved. Set an axis first: mirror.pypi / mirror.github / mirror.npm." msgstr "" -#: src/skit/cli.py:5345 +#: src/skit/cli.py:5373 #, python-format msgid "Unknown mirror value: %(name)s. \"mirror\" is the master switch (on / off); mirrors are picked per ecosystem: mirror.pypi (%(pypi)s), mirror.github (%(github)s), mirror.npm (%(npm)s) — each also takes a URL or \"off\"." msgstr "" -#: src/skit/cli.py:5361 +#: src/skit/cli.py:5389 #, python-format msgid "Unknown %(key)s value: %(name)s. Choose from: %(names)s, off — or give a full URL." msgstr "" -#: src/skit/cli.py:5385 +#: src/skit/cli.py:5413 #, python-format msgid "Unknown mirror.github value: %(name)s. Choose from: %(names)s, off — or give an https:// github-release base URL (the uv binary is downloaded and executed, so https:// is required)." msgstr "" -#: src/skit/cli.py:5402 +#: src/skit/cli.py:5430 #, python-format msgid "Unknown language: %(tag)s. Available: %(locales)s" msgstr "" -#: src/skit/cli.py:5419 +#: src/skit/cli.py:5447 #, python-format msgid "Unknown form style: %(value)s. Choose from: tui, plain" msgstr "" -#: src/skit/cli.py:5429 src/skit/tui_prefs.py:482 +#: src/skit/cli.py:5457 src/skit/tui_prefs.py:483 #, python-format msgid "No such file: %(path)s" msgstr "" -#: src/skit/cli.py:5436 +#: src/skit/cli.py:5464 #, python-format msgid "Unknown JS runner: %(value)s. Choose from: %(names)s" msgstr "" -#: src/skit/cli.py:5443 +#: src/skit/cli.py:5471 #, python-format msgid "Unknown after-run behavior: %(value)s. Choose from: exit, stay" msgstr "" -#: src/skit/cli.py:5451 +#: src/skit/cli.py:5479 msgid "Read or set skit's settings (language, editor, mirror, form style, after-run)." msgstr "" -#: src/skit/cli.py:5453 +#: src/skit/cli.py:5481 msgid "Examples: skit config · skit config lang zh-TW · skit config mirror.pypi tsinghua" msgstr "" -#: src/skit/cli.py:5460 +#: src/skit/cli.py:5488 msgid "Setting name: lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" msgstr "" -#: src/skit/cli.py:5465 +#: src/skit/cli.py:5493 msgid "New value (omit to read; lang also accepts \"auto\")" msgstr "" -#: src/skit/cli.py:5482 +#: src/skit/cli.py:5510 #, python-format msgid "Unknown setting: %(key)s. Available: %(keys)s" msgstr "" -#: src/skit/cli.py:5514 src/skit/tui_prefs.py:412 src/skit/tui_prefs.py:452 +#: src/skit/cli.py:5542 src/skit/tui_prefs.py:413 src/skit/tui_prefs.py:453 msgid "A custom choice needs a URL." msgstr "" -#: src/skit/cli.py:5521 src/skit/tui_prefs.py:347 +#: src/skit/cli.py:5549 src/skit/tui_prefs.py:348 msgid "github-release mirror base URL" msgstr "" -#: src/skit/cli.py:5527 src/skit/tui_prefs.py:457 +#: src/skit/cli.py:5555 src/skit/tui_prefs.py:458 #, python-format msgid "The uv binary is downloaded and executed, so the github-release base URL must use https:// (got: %(url)s)." msgstr "" -#: src/skit/cli.py:5546 src/skit/tui_prefs.py:336 +#: src/skit/cli.py:5574 src/skit/tui_prefs.py:337 msgid "PyPI index (Python packages)" msgstr "" -#: src/skit/cli.py:5548 src/skit/tui_prefs.py:340 +#: src/skit/cli.py:5576 src/skit/tui_prefs.py:341 msgid "PyPI index URL" msgstr "" -#: src/skit/cli.py:5552 src/skit/tui_prefs.py:341 +#: src/skit/cli.py:5580 src/skit/tui_prefs.py:342 msgid "GitHub releases (Python builds, the uv binary)" msgstr "" -#: src/skit/cli.py:5561 src/skit/tui_prefs.py:350 +#: src/skit/cli.py:5589 src/skit/tui_prefs.py:351 msgid "npm registry (JS/TS packages)" msgstr "" -#: src/skit/cli.py:5563 src/skit/tui_prefs.py:354 +#: src/skit/cli.py:5591 src/skit/tui_prefs.py:355 msgid "npm registry URL" msgstr "" -#: src/skit/cli.py:5579 +#: src/skit/cli.py:5607 msgid "Network to PyPI / GitHub looks slow or blocked." msgstr "" -#: src/skit/cli.py:5581 +#: src/skit/cli.py:5609 msgid "Configure mirrors for faster installs (mainland China)?" msgstr "" @@ -2023,59 +2041,59 @@ msgstr "" msgid "%(name)s must be one of: %(choices)s" msgstr "" -#: src/skit/flows.py:709 +#: src/skit/flows.py:712 #, python-format msgid "%(name)s reads from the environment variable %(env)s, but it isn't set." msgstr "" -#: src/skit/flows.py:906 +#: src/skit/flows.py:915 #, python-format msgid "→ inject: %(pairs)s" msgstr "" -#: src/skit/flows.py:909 +#: src/skit/flows.py:918 msgid " (written to a temporary copy, deleted after the run; your original file is untouched)" msgstr "" -#: src/skit/flows.py:969 +#: src/skit/flows.py:978 msgid "Secret-marked values are never saved by skit, but this prompt sends them to the selected agent as plaintext; the agent may log or sync them." msgstr "" -#: src/skit/flows.py:980 +#: src/skit/flows.py:989 #, python-format msgid "%(name)s can't contain a line break: a shell `read` takes ONE line, so everything after the break would be thrown away." msgstr "" -#: src/skit/flows.py:984 +#: src/skit/flows.py:993 #, python-format msgid "%(name)s is read on the same line as other values, so its value can't contain spaces or tabs — the shell would split it across the other fields. Only the LAST value on a `read` line may contain spaces." msgstr "" -#: src/skit/flows.py:989 +#: src/skit/flows.py:998 #, python-format msgid "%(name)s starts or ends with a space or tab, which a shell `read` strips off the line — the script would receive it trimmed. Remove the surrounding whitespace." msgstr "" -#: src/skit/flows.py:1051 +#: src/skit/flows.py:1060 msgid "The built-in amp runner is one-shot: amp -x runs this prompt once and does not open an interactive session." msgstr "" -#: src/skit/flows.py:1098 +#: src/skit/flows.py:1107 #, python-format msgid "%(value)s isn't a valid %(type)s for %(param)s." msgstr "" -#: src/skit/flows.py:1110 +#: src/skit/flows.py:1119 #, python-format msgid "%(empty)s is empty, but %(filled)s is filled and they are read on the same line — a shell `read` would hand your value to %(empty)s. Fill %(empty)s in, or clear %(filled)s." msgstr "" -#: src/skit/flows.py:1124 +#: src/skit/flows.py:1133 #, python-format msgid "skit refused to run its own injected copy: %(detail)s" msgstr "" -#: src/skit/flows.py:1132 +#: src/skit/flows.py:1141 #, python-format msgid "The script and its form definitions don't match anymore: %(detail)s. Run `skit params %(name)s --resync` to fix it." msgstr "" @@ -2355,7 +2373,7 @@ msgstr "" msgid "%(name)s has no stored copy — store doesn't apply to its kind." msgstr "" -#: src/skit/store.py:551 src/skit/tui_settings.py:519 +#: src/skit/store.py:551 src/skit/tui_settings.py:514 msgid "The working directory must be origin, store, invoke, or an absolute path." msgstr "" @@ -2369,7 +2387,7 @@ msgstr "" msgid "%(name)s isn't a command entry." msgstr "" -#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:947 +#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:942 msgid "Command template must not be empty" msgstr "" @@ -2403,22 +2421,22 @@ msgstr "" msgid "%(name)s's stored copy isn't valid UTF-8, so skit can't rewrite the script's own dependency block — and that block is what uv reads. Edit it in the script itself: skit edit %(name)s" msgstr "" -#: src/skit/store.py:1150 src/skit/tui_settings.py:1013 +#: src/skit/store.py:1148 src/skit/tui_settings.py:1008 #, python-format msgid "The name %(name)s is already taken." msgstr "" -#: src/skit/store.py:1192 +#: src/skit/store.py:1190 #, python-format msgid "%(slug)s: meta.toml is missing; skipped" msgstr "" -#: src/skit/store.py:1198 +#: src/skit/store.py:1196 #, python-format msgid "%(slug)s: meta.toml is corrupt (%(error)s); skipped" msgstr "" -#: src/skit/store.py:1204 +#: src/skit/store.py:1202 #, python-format msgid "%(slug)s: the referenced source file is gone: %(path)s" msgstr "" @@ -2432,7 +2450,7 @@ msgstr "" msgid "Runner picked on the run form" msgstr "" -#: src/skit/tui.py:81 src/skit/tui_settings.py:556 src/skit/tui_settings.py:579 +#: src/skit/tui.py:81 src/skit/tui_settings.py:551 src/skit/tui_settings.py:574 #, python-format msgid "%(runner)s (no longer configured)" msgstr "" @@ -2462,14 +2480,14 @@ msgid "%(days)s d ago" msgstr "" #: src/skit/tui.py:125 src/skit/tui.py:162 src/skit/tui.py:204 -#: src/skit/tui.py:330 src/skit/tui.py:476 src/skit/tui_runner.py:210 +#: src/skit/tui.py:333 src/skit/tui.py:483 src/skit/tui_runner.py:210 #: src/skit/tui_runner.py:262 src/skit/tui_runner.py:287 #: src/skit/tui_runner.py:334 msgid "Remove" msgstr "" -#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:157 -#: src/skit/tui_add.py:181 src/skit/tui_runner.py:288 +#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:158 +#: src/skit/tui_add.py:182 src/skit/tui_runner.py:288 #: src/skit/tui_runner.py:335 msgid "Keep" msgstr "" @@ -2482,8 +2500,8 @@ msgstr "" msgid "Close" msgstr "" -#: src/skit/tui.py:199 src/skit/tui.py:333 src/skit/tui.py:462 -#: src/skit/tui.py:471 src/skit/tui_form.py:554 src/skit/tui_form.py:555 +#: src/skit/tui.py:199 src/skit/tui.py:336 src/skit/tui.py:469 +#: src/skit/tui.py:478 src/skit/tui_form.py:554 src/skit/tui_form.py:555 #: src/skit/tui_form.py:723 msgid "Run" msgstr "" @@ -2492,203 +2510,203 @@ msgstr "" msgid "Rerun with last values" msgstr "" -#: src/skit/tui.py:201 src/skit/tui.py:335 src/skit/tui.py:474 +#: src/skit/tui.py:201 src/skit/tui.py:338 src/skit/tui.py:481 msgid "Entry settings" msgstr "" -#: src/skit/tui.py:202 src/skit/tui.py:336 src/skit/tui.py:479 -#: src/skit/tui_settings.py:801 +#: src/skit/tui.py:202 src/skit/tui.py:339 src/skit/tui.py:486 +#: src/skit/tui_settings.py:796 msgid "Presets" msgstr "" -#: src/skit/tui.py:203 src/skit/tui.py:331 src/skit/tui.py:332 -#: src/skit/tui.py:475 +#: src/skit/tui.py:203 src/skit/tui.py:334 src/skit/tui.py:335 +#: src/skit/tui.py:482 msgid "Edit source" msgstr "" -#: src/skit/tui.py:205 src/skit/tui.py:337 src/skit/tui.py:478 +#: src/skit/tui.py:205 src/skit/tui.py:340 src/skit/tui.py:485 msgid "Add entry" msgstr "" -#: src/skit/tui.py:206 src/skit/tui.py:341 src/skit/tui.py:480 +#: src/skit/tui.py:206 src/skit/tui.py:344 src/skit/tui.py:487 msgid "Search" msgstr "" -#: src/skit/tui.py:207 src/skit/tui.py:345 src/skit/tui.py:372 -#: src/skit/tui.py:483 src/skit/tui.py:1063 +#: src/skit/tui.py:207 src/skit/tui.py:348 src/skit/tui.py:379 +#: src/skit/tui.py:490 src/skit/tui.py:1092 msgid "Detail pane" msgstr "" -#: src/skit/tui.py:208 src/skit/tui.py:338 src/skit/tui.py:484 -#: src/skit/tui_prefs.py:155 +#: src/skit/tui.py:208 src/skit/tui.py:341 src/skit/tui.py:491 +#: src/skit/tui_prefs.py:156 msgid "Preferences" msgstr "" -#: src/skit/tui.py:209 src/skit/tui.py:339 src/skit/tui.py:485 +#: src/skit/tui.py:209 src/skit/tui.py:342 src/skit/tui.py:492 #: src/skit/tui_health.py:55 msgid "Health check" msgstr "" -#: src/skit/tui.py:210 src/skit/tui.py:322 src/skit/tui.py:323 +#: src/skit/tui.py:210 src/skit/tui.py:325 src/skit/tui.py:326 msgid "Quit" msgstr "" -#: src/skit/tui.py:256 src/skit/tui.py:371 src/skit/tui.py:1056 -#: src/skit/tui.py:1062 +#: src/skit/tui.py:259 src/skit/tui.py:378 src/skit/tui.py:1085 +#: src/skit/tui.py:1091 msgid "Library" msgstr "" -#: src/skit/tui.py:334 src/skit/tui.py:473 +#: src/skit/tui.py:337 src/skit/tui.py:480 msgid "Rerun" msgstr "" -#: src/skit/tui.py:381 src/skit/tui.py:1058 +#: src/skit/tui.py:388 src/skit/tui.py:1087 msgid "/ to search names and descriptions…" msgstr "" -#: src/skit/tui.py:436 src/skit/tui.py:519 +#: src/skit/tui.py:443 src/skit/tui.py:534 msgid "Your entries will appear here." msgstr "" -#: src/skit/tui.py:439 +#: src/skit/tui.py:446 #, python-format msgid "%(shown)s/%(total)s entry" msgid_plural "%(shown)s/%(total)s entries" msgstr[0] "" msgstr[1] "" -#: src/skit/tui.py:463 +#: src/skit/tui.py:470 msgid "Back to list" msgstr "" -#: src/skit/tui.py:521 +#: src/skit/tui.py:536 msgid "Press a to add the first one," msgstr "" -#: src/skit/tui.py:522 +#: src/skit/tui.py:537 msgid "or run: skit add in a terminal." msgstr "" -#: src/skit/tui.py:538 +#: src/skit/tui.py:553 msgid "The copy is kept by skit; your original file is never modified." msgstr "" -#: src/skit/tui.py:542 src/skit/tui_settings.py:436 +#: src/skit/tui.py:557 src/skit/tui_settings.py:431 #, python-format msgid "Linked to the original: %(path)s" msgstr "" -#: src/skit/tui.py:552 +#: src/skit/tui.py:567 msgid "(no description — add one in Entry settings)" msgstr "" -#: src/skit/tui.py:571 +#: src/skit/tui.py:586 #, python-format msgid "Parameters %(summary)s" msgstr "" -#: src/skit/tui.py:574 +#: src/skit/tui.py:589 #, python-format msgid "Presets %(names)s" msgstr "" -#: src/skit/tui.py:583 +#: src/skit/tui.py:598 #, python-format msgid "Depends on %(deps)s" msgstr "" -#: src/skit/tui.py:589 +#: src/skit/tui.py:604 msgid "finished" msgstr "" -#: src/skit/tui.py:591 +#: src/skit/tui.py:606 #, python-format msgid "failed (code %(code)s)" msgstr "" -#: src/skit/tui.py:594 +#: src/skit/tui.py:609 #, python-format msgid "Last run %(when)s · %(outcome)s" msgstr "" -#: src/skit/tui.py:598 +#: src/skit/tui.py:613 msgid "Not run yet" msgstr "" -#: src/skit/tui.py:604 +#: src/skit/tui.py:619 msgid "The script changed — skit checks the form against it before every run." msgstr "" -#: src/skit/tui.py:692 +#: src/skit/tui.py:707 msgid "Press Ctrl+C again to quit" msgstr "" -#: src/skit/tui.py:712 src/skit/tui.py:743 src/skit/tui.py:755 -#: src/skit/tui.py:793 src/skit/tui.py:820 src/skit/tui.py:844 -#: src/skit/tui.py:898 src/skit/tui.py:963 src/skit/tui.py:1087 +#: src/skit/tui.py:727 src/skit/tui.py:758 src/skit/tui.py:770 +#: src/skit/tui.py:808 src/skit/tui.py:843 src/skit/tui.py:871 +#: src/skit/tui.py:927 src/skit/tui.py:992 src/skit/tui.py:1116 #, python-format msgid "Error: %(error)s" msgstr "" -#: src/skit/tui.py:723 +#: src/skit/tui.py:738 msgid "A prompt needs a configured agent to run with." msgstr "" -#: src/skit/tui.py:744 +#: src/skit/tui.py:759 msgid "The runner is no longer configured." msgstr "" -#: src/skit/tui.py:781 +#: src/skit/tui.py:796 #, python-format msgid "%(name)s hasn't run yet — press Enter to fill the form first." msgstr "" -#: src/skit/tui.py:829 src/skit/tui.py:1070 src/skit/tui_form.py:653 +#: src/skit/tui.py:856 src/skit/tui.py:1099 src/skit/tui_form.py:653 #, python-format msgid "Run %(name)s" msgstr "" -#: src/skit/tui.py:852 +#: src/skit/tui.py:879 #, python-format msgid "Last: %(name)s ✗ couldn't launch" msgstr "" -#: src/skit/tui.py:858 +#: src/skit/tui.py:887 #, python-format msgid "Last: %(name)s ✓ finished" msgstr "" -#: src/skit/tui.py:860 +#: src/skit/tui.py:889 #, python-format msgid "Last: %(name)s ✗ failed (code %(code)s)" msgstr "" -#: src/skit/tui.py:867 +#: src/skit/tui.py:896 msgid "✓ finished" msgstr "" -#: src/skit/tui.py:869 +#: src/skit/tui.py:898 #, python-format msgid "✗ failed (code %(code)s)" msgstr "" -#: src/skit/tui.py:870 +#: src/skit/tui.py:899 msgid "✗ couldn't launch" msgstr "" -#: src/skit/tui.py:902 src/skit/tui.py:922 +#: src/skit/tui.py:931 src/skit/tui.py:951 #, python-format msgid "Edited %(name)s." msgstr "" -#: src/skit/tui.py:974 +#: src/skit/tui.py:1003 msgid "✓ added" msgstr "" -#: src/skit/tui_add.py:70 src/skit/tui_add.py:139 src/skit/tui_add.py:197 -#: src/skit/tui_add.py:320 src/skit/tui_add.py:601 src/skit/tui_add.py:654 -#: src/skit/tui_add.py:680 src/skit/tui_add.py:900 src/skit/tui_add.py:1207 -#: src/skit/tui_add.py:1364 src/skit/tui_form.py:326 src/skit/tui_form.py:348 +#: src/skit/tui_add.py:71 src/skit/tui_add.py:140 src/skit/tui_add.py:198 +#: src/skit/tui_add.py:321 src/skit/tui_add.py:602 src/skit/tui_add.py:655 +#: src/skit/tui_add.py:681 src/skit/tui_add.py:910 src/skit/tui_add.py:1219 +#: src/skit/tui_add.py:1380 src/skit/tui_form.py:326 src/skit/tui_form.py:348 #: src/skit/tui_form.py:392 src/skit/tui_form.py:447 src/skit/tui_form.py:481 #: src/skit/tui_form.py:511 src/skit/tui_form.py:547 src/skit/tui_form.py:737 #: src/skit/tui_pathpick.py:285 src/skit/tui_pathpick.py:341 @@ -2698,236 +2716,236 @@ msgstr "" msgid "Cancel" msgstr "" -#: src/skit/tui_add.py:156 src/skit/tui_add.py:180 +#: src/skit/tui_add.py:157 src/skit/tui_add.py:181 msgid "Delete" msgstr "" -#: src/skit/tui_add.py:176 +#: src/skit/tui_add.py:177 #, python-format msgid "Delete the draft \"%(name)s\"? It is the only copy." msgstr "" -#: src/skit/tui_add.py:200 +#: src/skit/tui_add.py:201 msgid "Write a new script" msgstr "" -#: src/skit/tui_add.py:201 +#: src/skit/tui_add.py:202 msgid "Draft a prompt" msgstr "" -#: src/skit/tui_add.py:204 +#: src/skit/tui_add.py:205 msgid "Delete a kept draft" msgstr "" -#: src/skit/tui_add.py:226 +#: src/skit/tui_add.py:227 msgid "Add an entry" msgstr "" -#: src/skit/tui_add.py:234 +#: src/skit/tui_add.py:235 msgid "Path to a script, executable, or prompt:" msgstr "" -#: src/skit/tui_add.py:238 +#: src/skit/tui_add.py:239 #, python-brace-format msgid "…or register a command template below (e.g. ffmpeg -i {input}):" msgstr "" -#: src/skit/tui_add.py:287 +#: src/skit/tui_add.py:288 msgid "…or resume a kept draft:" msgstr "" -#: src/skit/tui_add.py:300 +#: src/skit/tui_add.py:301 msgid "Delete draft…" msgstr "" -#: src/skit/tui_add.py:305 +#: src/skit/tui_add.py:306 msgid "…or start from a blank page:" msgstr "" -#: src/skit/tui_add.py:310 +#: src/skit/tui_add.py:311 msgid "Write a script…" msgstr "" -#: src/skit/tui_add.py:311 +#: src/skit/tui_add.py:312 msgid "Draft a prompt…" msgstr "" -#: src/skit/tui_add.py:319 +#: src/skit/tui_add.py:320 msgid "Continue" msgstr "" -#: src/skit/tui_add.py:496 +#: src/skit/tui_add.py:497 #, python-format msgid "Deleted the draft %(name)s." msgstr "" -#: src/skit/tui_add.py:538 +#: src/skit/tui_add.py:539 msgid "Nothing was written, so nothing was added." msgstr "" -#: src/skit/tui_add.py:602 src/skit/tui_add.py:653 src/skit/tui_add.py:684 -#: src/skit/tui_add.py:888 src/skit/tui_add.py:1210 src/skit/tui_add.py:1361 +#: src/skit/tui_add.py:603 src/skit/tui_add.py:654 src/skit/tui_add.py:685 +#: src/skit/tui_add.py:898 src/skit/tui_add.py:1222 src/skit/tui_add.py:1377 msgid "Add" msgstr "" -#: src/skit/tui_add.py:631 src/skit/tui_add.py:805 src/skit/tui_add.py:1295 +#: src/skit/tui_add.py:632 src/skit/tui_add.py:815 src/skit/tui_add.py:1311 #, python-format msgid "Add %(name)s" msgstr "" -#: src/skit/tui_add.py:643 +#: src/skit/tui_add.py:644 msgid "(shown in the Library — you can write one line)" msgstr "" -#: src/skit/tui_add.py:647 +#: src/skit/tui_add.py:648 msgid "The program runs from its own location; skit never copies a binary." msgstr "" -#: src/skit/tui_add.py:683 src/skit/tui_add.py:899 +#: src/skit/tui_add.py:684 src/skit/tui_add.py:909 msgid "Edit script" msgstr "" -#: src/skit/tui_add.py:833 +#: src/skit/tui_add.py:843 msgid "Link the original: skit never writes to the file." msgstr "" -#: src/skit/tui_add.py:836 +#: src/skit/tui_add.py:846 msgid "Link the original: parameter setup is skipped — skit never writes to the file." msgstr "" -#: src/skit/tui_add.py:841 +#: src/skit/tui_add.py:851 msgid "npm dependencies apply to stored copies only, so none are recorded." msgstr "" -#: src/skit/tui_add.py:860 +#: src/skit/tui_add.py:870 msgid "(the script has no docstring — you can write one line)" msgstr "" -#: src/skit/tui_add.py:865 src/skit/tui_add.py:1373 -#: src/skit/tui_settings.py:427 +#: src/skit/tui_add.py:875 src/skit/tui_add.py:1389 +#: src/skit/tui_settings.py:422 msgid "Storage" msgstr "" -#: src/skit/tui_add.py:872 src/skit/tui_add.py:1377 +#: src/skit/tui_add.py:882 src/skit/tui_add.py:1393 msgid "Keep a copy — skit stores it; your original file is never modified" msgstr "" -#: src/skit/tui_add.py:878 +#: src/skit/tui_add.py:888 msgid "Link the original — edits take effect immediately, but skit won't write to the file, so parameter definitions are yours to maintain" msgstr "" -#: src/skit/tui_add.py:897 src/skit/tui_add.py:1362 +#: src/skit/tui_add.py:907 src/skit/tui_add.py:1378 msgid "Toggle" msgstr "" -#: src/skit/tui_add.py:916 src/skit/tui_settings.py:835 +#: src/skit/tui_add.py:926 src/skit/tui_settings.py:830 msgid "Dependencies" msgstr "" -#: src/skit/tui_add.py:921 +#: src/skit/tui_add.py:931 msgid "comma separated, e.g. chalk, @scope/pkg" msgstr "" -#: src/skit/tui_add.py:925 src/skit/tui_add.py:951 +#: src/skit/tui_add.py:935 src/skit/tui_add.py:961 msgid "detected from the script's imports — edit freely" msgstr "" -#: src/skit/tui_add.py:932 +#: src/skit/tui_add.py:942 msgid "The script declares its own dependencies (PEP 723):" msgstr "" -#: src/skit/tui_add.py:935 +#: src/skit/tui_add.py:945 #, python-format msgid "needs Python %(python)s" msgstr "" -#: src/skit/tui_add.py:938 +#: src/skit/tui_add.py:948 #, python-format msgid "installs %(dep)s" msgstr "" -#: src/skit/tui_add.py:940 +#: src/skit/tui_add.py:950 msgid "(none declared)" msgstr "" -#: src/skit/tui_add.py:947 src/skit/tui_settings.py:838 +#: src/skit/tui_add.py:957 src/skit/tui_settings.py:833 msgid "comma separated, e.g. requests>=2,<3, rich" msgstr "" -#: src/skit/tui_add.py:959 +#: src/skit/tui_add.py:969 msgid "(automatic)" msgstr "" -#: src/skit/tui_add.py:964 +#: src/skit/tui_add.py:974 msgid "Python version (requires-python) — prefilled from the #! line when it pins one; empty means automatic" msgstr "" -#: src/skit/tui_add.py:973 +#: src/skit/tui_add.py:983 msgid "Parameters" msgstr "" -#: src/skit/tui_add.py:1003 src/skit/tui_add.py:1413 +#: src/skit/tui_add.py:1013 src/skit/tui_add.py:1429 msgid "Tick the ones the run form should ask for:" msgstr "" -#: src/skit/tui_add.py:1008 +#: src/skit/tui_add.py:1018 #, python-format msgid "input() #%(n)s: %(prompt)s" msgstr "" -#: src/skit/tui_add.py:1018 +#: src/skit/tui_add.py:1028 msgid "looks like a loop accumulator — probably not a parameter" msgstr "" -#: src/skit/tui_add.py:1026 +#: src/skit/tui_add.py:1036 #, python-format msgid "%(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (Ctrl+E edits it now)." msgstr "" -#: src/skit/tui_add.py:1209 src/skit/tui_add.py:1363 +#: src/skit/tui_add.py:1221 src/skit/tui_add.py:1379 msgid "Edit prompt" msgstr "" -#: src/skit/tui_add.py:1211 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 -#: src/skit/tui_settings.py:270 +#: src/skit/tui_add.py:1223 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 +#: src/skit/tui_settings.py:269 msgid "New agent" msgstr "" -#: src/skit/tui_add.py:1215 src/skit/tui_add.py:1430 -#: src/skit/tui_settings.py:274 src/skit/tui_settings.py:712 +#: src/skit/tui_add.py:1231 src/skit/tui_add.py:1446 +#: src/skit/tui_settings.py:275 src/skit/tui_settings.py:707 msgid "Choose variables…" msgstr "" -#: src/skit/tui_add.py:1336 +#: src/skit/tui_add.py:1352 msgid "(taken from the first line — you can write your own)" msgstr "" -#: src/skit/tui_add.py:1341 +#: src/skit/tui_add.py:1357 msgid "Variable insertion" msgstr "" -#: src/skit/tui_add.py:1343 +#: src/skit/tui_add.py:1359 msgid "Fill {{name}} placeholders from a form before each run" msgstr "" -#: src/skit/tui_add.py:1349 src/skit/tui_settings.py:544 +#: src/skit/tui_add.py:1365 src/skit/tui_settings.py:539 msgid "Runner (the agent this prompt runs with)" msgstr "" -#: src/skit/tui_add.py:1351 src/skit/tui_add.py:1503 -#: src/skit/tui_settings.py:554 src/skit/tui_settings.py:576 +#: src/skit/tui_add.py:1367 src/skit/tui_add.py:1519 +#: src/skit/tui_settings.py:549 src/skit/tui_settings.py:571 msgid "ask on the run form" msgstr "" -#: src/skit/tui_add.py:1382 +#: src/skit/tui_add.py:1398 msgid "Link the original — edits take effect immediately; skit never writes to the file" msgstr "" -#: src/skit/tui_add.py:1398 +#: src/skit/tui_add.py:1414 msgid "No {{name}} placeholders detected — the body travels to the agent as written." msgstr "" -#: src/skit/tui_add.py:1406 +#: src/skit/tui_add.py:1422 #, python-format msgid "Detected %(count)s placeholders — probably not written for insertion. Tick only the ones you need, or untick the switch above." msgstr "" @@ -2968,7 +2986,7 @@ msgstr "" msgid "skit couldn't read this script's argument declarations — type everything into the extra-arguments field." msgstr "" -#: src/skit/tui_form.py:195 src/skit/tui_settings.py:212 +#: src/skit/tui_form.py:195 src/skit/tui_settings.py:211 msgid "required" msgstr "" @@ -3014,9 +3032,9 @@ msgstr "" msgid "Save as preset" msgstr "" -#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:303 -#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:268 -#: src/skit/tui_settings.py:408 +#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:304 +#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:267 +#: src/skit/tui_settings.py:403 msgid "Save" msgstr "" @@ -3108,10 +3126,10 @@ msgid "Preset \"%(preset)s\" saved." msgstr "" #: src/skit/tui_health.py:31 src/skit/tui_health.py:148 -#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:304 +#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:305 #: src/skit/tui_runner.py:211 src/skit/tui_runner.py:263 #: src/skit/tui_runner.py:354 src/skit/tui_runner.py:422 -#: src/skit/tui_settings.py:267 src/skit/tui_settings.py:418 +#: src/skit/tui_settings.py:266 src/skit/tui_settings.py:413 msgid "Back" msgstr "" @@ -3178,124 +3196,124 @@ msgstr "" msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Install by hand with: skit agent install --to DIR" msgstr "" -#: src/skit/tui_prefs.py:126 +#: src/skit/tui_prefs.py:127 msgid "Manage agents" msgstr "" -#: src/skit/tui_prefs.py:127 +#: src/skit/tui_prefs.py:128 msgid "Teach an AI agent" msgstr "" -#: src/skit/tui_prefs.py:173 +#: src/skit/tui_prefs.py:174 #, python-format msgid "%(count)s agent configured: %(names)s" msgid_plural "%(count)s agents configured: %(names)s" msgstr[0] "" msgstr[1] "" -#: src/skit/tui_prefs.py:179 +#: src/skit/tui_prefs.py:180 msgid "No agents configured." msgstr "" -#: src/skit/tui_prefs.py:202 +#: src/skit/tui_prefs.py:203 msgid "Interface language" msgstr "" -#: src/skit/tui_prefs.py:204 +#: src/skit/tui_prefs.py:205 msgid "Automatic (follow the system)" msgstr "" -#: src/skit/tui_prefs.py:210 +#: src/skit/tui_prefs.py:211 #, python-format msgid "Currently in effect: %(locale)s" msgstr "" -#: src/skit/tui_prefs.py:214 +#: src/skit/tui_prefs.py:215 msgid "Editor" msgstr "" -#: src/skit/tui_prefs.py:217 +#: src/skit/tui_prefs.py:218 msgid "e.g. code --wait (empty = use $VISUAL / $EDITOR)" msgstr "" -#: src/skit/tui_prefs.py:223 +#: src/skit/tui_prefs.py:224 #, python-format msgid "Empty means: %(cmd)s (from $VISUAL / $EDITOR)" msgstr "" -#: src/skit/tui_prefs.py:228 +#: src/skit/tui_prefs.py:229 msgid "Interactive form" msgstr "" -#: src/skit/tui_prefs.py:231 +#: src/skit/tui_prefs.py:232 msgid "Mini form — opens in place, fully clickable" msgstr "" -#: src/skit/tui_prefs.py:235 +#: src/skit/tui_prefs.py:236 msgid "Line-by-line prompts — plainest, best over slow terminals" msgstr "" -#: src/skit/tui_prefs.py:240 +#: src/skit/tui_prefs.py:241 msgid "Used by terminal runs: `skit run` parameter prompts and the `skit add` review panel." msgstr "" -#: src/skit/tui_prefs.py:246 +#: src/skit/tui_prefs.py:247 msgid "After a run (from this menu)" msgstr "" -#: src/skit/tui_prefs.py:249 +#: src/skit/tui_prefs.py:250 msgid "Quit skit — leave the run's output in the terminal" msgstr "" -#: src/skit/tui_prefs.py:253 +#: src/skit/tui_prefs.py:254 msgid "Return to the Library immediately" msgstr "" -#: src/skit/tui_prefs.py:257 +#: src/skit/tui_prefs.py:258 msgid "JavaScript runtime" msgstr "" -#: src/skit/tui_prefs.py:261 +#: src/skit/tui_prefs.py:262 msgid "Automatic — the first of deno / bun / node found" msgstr "" -#: src/skit/tui_prefs.py:267 +#: src/skit/tui_prefs.py:268 msgid "Runs js/ts entries that don't pin their own runtime." msgstr "" -#: src/skit/tui_prefs.py:274 +#: src/skit/tui_prefs.py:275 msgid "Shell on Windows" msgstr "" -#: src/skit/tui_prefs.py:277 +#: src/skit/tui_prefs.py:278 msgid "Path to bash.exe (empty = Git Bash / WSL detection)" msgstr "" -#: src/skit/tui_prefs.py:281 +#: src/skit/tui_prefs.py:282 msgid "Shell scripts need an explicit bash here." msgstr "" -#: src/skit/tui_prefs.py:286 src/skit/tui_runner.py:381 +#: src/skit/tui_prefs.py:287 src/skit/tui_runner.py:381 msgid "Agents (prompt runners)" msgstr "" -#: src/skit/tui_prefs.py:290 +#: src/skit/tui_prefs.py:291 msgid "Manage agents…" msgstr "" -#: src/skit/tui_prefs.py:292 +#: src/skit/tui_prefs.py:293 msgid "Teach an AI agent skit…" msgstr "" -#: src/skit/tui_prefs.py:316 +#: src/skit/tui_prefs.py:317 msgid "Download mirrors (mainland-China acceleration)" msgstr "" -#: src/skit/tui_prefs.py:318 +#: src/skit/tui_prefs.py:319 msgid "Each ecosystem is its own choice — mirror vendors differ per axis." msgstr "" -#: src/skit/tui_prefs.py:323 +#: src/skit/tui_prefs.py:324 msgid "Master switch — \"off\" pauses mirrors but keeps the saved URLs." msgstr "" @@ -3369,221 +3387,221 @@ msgstr "" msgid "New agent…" msgstr "" -#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 +#: src/skit/tui_settings.py:43 src/skit/tui_settings.py:59 msgid "Discard" msgstr "" -#: src/skit/tui_settings.py:45 src/skit/tui_settings.py:61 +#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 msgid "Keep editing" msgstr "" -#: src/skit/tui_settings.py:57 +#: src/skit/tui_settings.py:56 msgid "Discard unsaved changes?" msgstr "" -#: src/skit/tui_settings.py:103 src/skit/tui_settings.py:214 +#: src/skit/tui_settings.py:102 src/skit/tui_settings.py:213 msgid "Form label:" msgstr "" -#: src/skit/tui_settings.py:107 src/skit/tui_settings.py:218 +#: src/skit/tui_settings.py:106 src/skit/tui_settings.py:217 msgid "secret (never saved to disk)" msgstr "" -#: src/skit/tui_settings.py:111 src/skit/tui_settings.py:222 +#: src/skit/tui_settings.py:110 src/skit/tui_settings.py:221 msgid "env variable to read it from (optional)" msgstr "" -#: src/skit/tui_settings.py:137 src/skit/tui_settings.py:257 +#: src/skit/tui_settings.py:136 src/skit/tui_settings.py:256 msgid "Anything skit previously remembered for this value will be deleted too." msgstr "" -#: src/skit/tui_settings.py:177 +#: src/skit/tui_settings.py:176 msgid "Type:" msgstr "" -#: src/skit/tui_settings.py:180 +#: src/skit/tui_settings.py:179 msgid "type: str / int / float / bool / choice / path" msgstr "" -#: src/skit/tui_settings.py:184 +#: src/skit/tui_settings.py:183 msgid "Default:" msgstr "" -#: src/skit/tui_settings.py:187 +#: src/skit/tui_settings.py:186 msgid "default value (optional)" msgstr "" -#: src/skit/tui_settings.py:191 +#: src/skit/tui_settings.py:190 msgid "Choices:" msgstr "" -#: src/skit/tui_settings.py:194 +#: src/skit/tui_settings.py:193 msgid "comma separated (for type: choice)" msgstr "" -#: src/skit/tui_settings.py:198 +#: src/skit/tui_settings.py:197 msgid "Help:" msgstr "" -#: src/skit/tui_settings.py:201 +#: src/skit/tui_settings.py:200 msgid "one-line help shown under the field (optional)" msgstr "" -#: src/skit/tui_settings.py:206 +#: src/skit/tui_settings.py:205 msgid "Flag:" msgstr "" -#: src/skit/tui_settings.py:209 +#: src/skit/tui_settings.py:208 msgid "--flag (empty = positional)" msgstr "" -#: src/skit/tui_settings.py:269 src/skit/tui_settings.py:416 +#: src/skit/tui_settings.py:268 src/skit/tui_settings.py:411 msgid "Resync" msgstr "" -#: src/skit/tui_settings.py:390 +#: src/skit/tui_settings.py:385 msgid "Basics" msgstr "" -#: src/skit/tui_settings.py:393 +#: src/skit/tui_settings.py:388 msgid "Renaming keeps everything — remembered values, presets, the stored copy." msgstr "" -#: src/skit/tui_settings.py:398 +#: src/skit/tui_settings.py:393 msgid "Description (shown in the Library)" msgstr "" -#: src/skit/tui_settings.py:430 +#: src/skit/tui_settings.py:425 #, python-format msgid "Keep a copy — your original file is never modified. Source: %(path)s" msgstr "" -#: src/skit/tui_settings.py:447 +#: src/skit/tui_settings.py:442 msgid "Run in (working directory)" msgstr "" -#: src/skit/tui_settings.py:453 +#: src/skit/tui_settings.py:448 msgid "The source file's folder" msgstr "" -#: src/skit/tui_settings.py:455 +#: src/skit/tui_settings.py:450 msgid "skit's stored-copy folder" msgstr "" -#: src/skit/tui_settings.py:456 +#: src/skit/tui_settings.py:451 msgid "Wherever skit is run from" msgstr "" -#: src/skit/tui_settings.py:457 +#: src/skit/tui_settings.py:452 msgid "A fixed folder (type it below)" msgstr "" -#: src/skit/tui_settings.py:468 +#: src/skit/tui_settings.py:463 msgid "/absolute/path" msgstr "" -#: src/skit/tui_settings.py:472 +#: src/skit/tui_settings.py:467 msgid "Interpreter / runtime" msgstr "" -#: src/skit/tui_settings.py:475 +#: src/skit/tui_settings.py:470 msgid "empty = automatic (shebang, then detection order)" msgstr "" -#: src/skit/tui_settings.py:589 +#: src/skit/tui_settings.py:584 msgid "Parameters (the run form's fields)" msgstr "" -#: src/skit/tui_settings.py:595 +#: src/skit/tui_settings.py:590 msgid "(programs have no managed parameters)" msgstr "" -#: src/skit/tui_settings.py:600 +#: src/skit/tui_settings.py:595 msgid "skit doesn't write to this file — maintain the [tool.skit] definitions in the source directly." msgstr "" -#: src/skit/tui_settings.py:619 +#: src/skit/tui_settings.py:614 msgid "This script's run form comes from its own command-line arguments. Managing a hardcoded constant here would replace that form — leave it as is." msgstr "" -#: src/skit/tui_settings.py:625 src/skit/tui_settings.py:694 +#: src/skit/tui_settings.py:620 src/skit/tui_settings.py:689 msgid "Detected but not yet managed — tick to manage:" msgstr "" -#: src/skit/tui_settings.py:634 +#: src/skit/tui_settings.py:629 msgid "Every input() is managed — this script can now run with --no-input." msgstr "" -#: src/skit/tui_settings.py:650 +#: src/skit/tui_settings.py:645 #, python-brace-format msgid "Saving re-reads the {placeholders} from the template." msgstr "" -#: src/skit/tui_settings.py:663 +#: src/skit/tui_settings.py:658 msgid "Variable insertion ({{name}} placeholders become form fields)" msgstr "" -#: src/skit/tui_settings.py:674 +#: src/skit/tui_settings.py:669 msgid "Off — the body travels to the agent exactly as written." msgstr "" -#: src/skit/tui_settings.py:718 +#: src/skit/tui_settings.py:713 msgid "Add a parameter — type a name, then Save:" msgstr "" -#: src/skit/tui_settings.py:719 +#: src/skit/tui_settings.py:714 msgid "new parameter name" msgstr "" -#: src/skit/tui_settings.py:759 +#: src/skit/tui_settings.py:754 #, python-format msgid "%(name)s has an unknown type — use str, int, float, bool, choice, or path." msgstr "" -#: src/skit/tui_settings.py:771 +#: src/skit/tui_settings.py:766 #, python-format msgid "%(name)s: the default doesn't match its type." msgstr "" -#: src/skit/tui_settings.py:779 +#: src/skit/tui_settings.py:774 #, python-format msgid "%(name)s is a choice parameter but has no choices — fill its Choices field." msgstr "" -#: src/skit/tui_settings.py:805 +#: src/skit/tui_settings.py:800 msgid "None yet — press Ctrl+S inside the run form to save one." msgstr "" -#: src/skit/tui_settings.py:809 +#: src/skit/tui_settings.py:804 msgid "Untick a preset to delete it on save:" msgstr "" -#: src/skit/tui_settings.py:840 +#: src/skit/tui_settings.py:835 msgid "comma separated, e.g. chalk@^5, zod" msgstr "" -#: src/skit/tui_settings.py:846 +#: src/skit/tui_settings.py:841 msgid "Python constraint, e.g. \">=3.11\" (empty = automatic)" msgstr "" -#: src/skit/tui_settings.py:862 +#: src/skit/tui_settings.py:857 msgid "Needs (external commands)" msgstr "" -#: src/skit/tui_settings.py:865 +#: src/skit/tui_settings.py:860 msgid "comma separated, e.g. ffmpeg, jq" msgstr "" -#: src/skit/tui_settings.py:870 +#: src/skit/tui_settings.py:865 #, python-format msgid "Entry settings · %(name)s" msgstr "" -#: src/skit/tui_settings.py:925 +#: src/skit/tui_settings.py:920 msgid "Everything still matches the script." msgstr "" -#: src/skit/tui_settings.py:1080 src/skit/tui_settings.py:1099 +#: src/skit/tui_settings.py:1069 src/skit/tui_settings.py:1088 #, python-format msgid "Deleted previously remembered value(s): %(names)s" msgstr "" diff --git a/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo b/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo index 0021eada195e102c5aff94cf0c9f7f02fef425cd..be67fcc881bc910cf14e0e64b9bd445236d83a43 100644 GIT binary patch delta 13361 zcmYM)cX(9Q*2nQPKoUwIF_6$<2mwL}BtanbB=iy>bVCP|&_eHZ=uK(T6e(f>R78+o zq^XFCfC9=@!v&-XC^I zux>-giNbN1jB79nbFdATCRmJvu_8Xj3K*H}IN3fLEof+DyKoNP!ihK{#Zr6+-y>ev z$Z@{GCiLDNLz_5GIA&v6oPkyGZET8XkR&+~1j=X{_Qok#26NEYnZ`dfR$+%`j+246 za5=VX?l`0IDn?;Cd5g!XSQT%2XiPclKgAe2U6M=}znjY>Rr% zEUbikQ3JSyjMXVhnl-U3jKl31f|r&5FdBbi0nF3IajIb`>I+F&0o!2$j=@^E300&Q zu?QCJ>Nxo^7Nao{M`9nOe4MkWq78qEI>U6#L|+Jdhio`wup}NwrSb}D#G&163A*DX z;k+Y3IX8WFkIiI*n3%u-7g)=dlWL za4)-F8xx5;V?$hsn)yjAg8`IuVT?izyf&&>vrrRy1&iS(RA!IhAiRtI{ok#R-4kTk%8riw_s@> zjZ-v~x;vN$%f4(KMqwav0ye_hs2NYfQ@Gmwd|-y-c!`H&Bre5zxD$i$9_GczSOPsP zyfRe^eHv*i8cJb5)Qrbs0B%7&;1g_)`!N)AWm!jsQ61Gn4QwQ8&6lErQq4l8?keg5Ap@=YTVe&`X{e>yh-GmHvXPv# z*c2kA8 zhHLN$mL6&|yoi;ELxx!<6H%F%h$rv}vJ;*8!>#6g*WE_(5tf=%)OP8Fd2k5M!;z@% z^Ax9G45v#W+>9^a9@LV2g__V^Y=Tjv>^TEb86Jw7zzoypEOIZbMitja)C2aQmgWvt z#RsUODmU6r%yiTMvQbq%35(!LR8fD3dfsQK44gwPZHY1V{UjW(_kTAUukpcIR1s#4 zwI39RaTam$arXH-tWBIS-p=qr7*4zzV{sd*;~ezjLo9|76D&h<$cg8qU=ud?=HTGohIAc za~}Hl{|Ooz`6Ud-$JiMISm^rL9h2~NR0kI@8gHPgJ#?ydSPzx*u^5JP(VrnyCbpr9 z`zy?kcczkmeen?&3SxfJp&1p!CaB}F8TLaB?0xt1-KZn<0O~ z?QmRU16+VwsuQSl;NdiG(EfhLg*dD<-A=TwIGcDGcEIA~KMk`n0Y61A-o-K)I@8_( zaoC7B6?5ZajK^iz8$U;7ENYhfTKCb=%*UWoHxo6&<*2ngimLvnsI{xis7hl`td5gh zx1uI+8I$paId)$MY(+c`wN1Z4WikgfaNjK&?8j@wF zJF+94i#P~7FR&s#f*L@?D^_grs5Nhf<j3+n^37fggx=Gd%eqRwjGCHDSaRF;TxDA-$8Bbtr&^Nu_6A3>Mwd3 z*%?oxGmU6Gg(|W~7>ZSvTP@T_ZJ!j(Mjxt!JE)mGLJc_g3VZof!xF?@P@j*$Ae`bl z8^eefu5kDNTU^i$J5beq3Jc*KY=cix4`{j4-U;oo8SzUPhwHIDo<@DY+$vkjVVFSt zBQhr^-|O}#-da?~etVt#tE0d-?7`s}Ph1LXU>nqcW}r^Ow@@?9LDfp$)t0&8s9p00 z>b`a8!Lx3B9>);pf7AXRn1+eOr+qXE(g<2(RT_@%h#RA-bQ!Ap&*B8^vetgbpTT;> zMb=Tcn1+G)2P(A>u`cFaZzp48R3>_(GPe|qqVEC??b~};3B%vAZPEZ0Ps38U8LQ$K zSRWr?M~vTK2g@u>Azq0^@B+5N-?0JKe%pR2jl}?BA4Y&Pn}&*Q4Qd7-VL9B174Qel zjsKuh?7U-f00tB0!$uf^8gM$6#_3oB*JE+qk9zJoR6l=Vr1t+a8pXKaeb+Kk2elh| zq1J3JrrPGt;ppuEad_?_8pxwYi2W z#-f{S?JJ@Nl7%6-7;EBs)XdIfAl`F*gj)0GsFc^)>^QIDIMny@yl(?2fLaq`fus^qZU~S_2n2c3Ev?|WVVZ;Y;nyzoLwcUbR(ob+deukBC>PNOc zH>0ZmuaC%oFpX+kt%&Mj8{#&o2fT?I`4$Yq9rywsK&Ad$RO)Y_ItcjKc1av+Dc;8< z4BW!_Lu+)nk+EjV4^EjXiM+>OntaUo5-Z-g48iBk_;89INc% zhbJDv*4TZob+8Hh5MRdj*l3@1xExi)dvO##!4~Kn$^yMeV-ISix3LmNAFy4KhJ%O) zqB3(1TVk$*_9s^=>UuV6DOO_{+>3qiN9^EXf}gUJh+jEm6D@q0tZ4s_pb^6dxsKS| zsVb@_vQcY40rTKA)Ieur0Pe&5co@s!*VqZ~U=2(;YR?;srHFT7E&LvbVCnA6~~Gymy-X>%ot?P!jW>u?JVg>cmN?5s$W}bn{;7km}br_28q3+x5qfwW}VT{8kSPf&ow*w>{ixN*m zW#V-V!!1|^4`U+U#>QCwtZl>osE%f%I$nvnaV@GiH=r`%d)NKKR#eBkP$RpF74dh> zgGJ6+%8H{tFNbj_UgK!P@!JzZ@kJbaQFL8|@tVmbl0`32kG*o_LH-ZMTGCCf_TID$?1g z=YE75*f;nw{*0RFD_6*WV;YxfEXE4I@;eWAA#3CGy2_!1zhMJhcg=nm{DNBJsO$D` z!i(`E;sQ5p;QKI*_*<-nWp3KcJ7Xg8VAMe0xk>(Cq>=A8+XW3!BO8xh@f5bjinr`2 z9)q2T-^X(J96O-*wpITqREK*o3je^SSon@jARW_*SK+Jp=Nm9U-?hlp5sshT!8Vo1#4gq*2S>L}Lz(g^%s!Qsi$NSq8S``ZBDIIk*KQp4c}03@a1oe`*7(fvWa>SQ8(k zKCkwV`4Uzmo`D^4JEm*@2R!3k=0a~&s!Be$8KvS-;yG9yAL7dx{jdF(jaN}?`!ANk zrjEyde+H`RSD-rFfnL1g#<@Kn|J$-Uw$T2cN<(Y&CBA_FxE9Lg@t<^FRFPIjb<`8J zRFhG~whrU)Jl4m6-1eMCn4h>4s+M}=Ok9BaUZFf5d;hnlv7ZY|aWbX`c>KTJzCv|W zGSK5+>jv1Cco4S2eYg_y1bO@i)|)tqxOiTV|3AsDKxOm_Hp9aCJpS6~hJA=bS+iD;xzPfeHMn}d#H2c04f82qcRv3 z?C~Gbje|Wt|AEkx3))^Qumk>sf!MB~$A5BlK|LS~8{$$7#xt%t*opWShG9}6kN+oH z2lNt8!{WFBTjK%LE(r?pdHlr{7h*H+hdQfgpl0|cY9Oak9sc0@E2=2(VOcC6>Tz~s zW7PMbpa$%D!Hh-KSXa!4uVQ{&>7${PZa@{&XQ6(^0#_x0^-+jce`)MT=NRso0I{9Z}V~6_w)K zSP_%MJ^uaOA0vsEqK@31r~&;4HGu2b3!MmUH+BJP;G40k_Wvy!N=dPzc4H)Jrg2yj zYoTV;0hN*YsMNoKI%rO!)-qQy`+f{+0L@U(>4+LwH}`WNrV_u2A=>{pX@qj&G3thV z#qEYr)Ok=7Rkh7gYn|mf2Q{FLs0r-G1$YFvW9t%DL(fnHNGNHk?}FMDuV5$ocfO+0 z9?O)nnGSc|gc{IAR3@&uv8S}h|1QXjdhkLVjh~V>l|VhI)QI)b?zR zs-<3D_P?rs2NxpnJPyM%*gbRz!2SpcEZM&G06w=4Tpo;D!>H)V=C*MQVQq+jH2M$0L z@B3ID_oHTh5o0lTWgA$$YXj7hwsQ6Lp%Kl6v8WSiJ!&mKK&{+fSJ;@zn8;VkN$`3rRrMOX1Jna@e5q5V4xHPbDq8SX~a#x2wh!Ljy^ zXn?BXcQFm`p_VG8s%36G>i(su`@g^@cms1|Y&Bc5cpRYp--m{7_{#MeYUT-Xw$=?% z189q?dLL@dccKoUtEiclh_{p{x@Nd8MeX-P$cg9tgQ*yk!1<#6KaPghWIk%d>rvHu z6zky6s1%1+w^Wuw6=4$UzIIp`GqEyGK-JDWs1A>zYU&}zhT}Nm&6t7Snzl=3qbBkz>fj2fWwlcd_0k!J^>AG+ z_PNz)1yCtEn$C--5 zQ4_k0D)yjyHnI46?0=;=nG0QTB2K~YP}kelcTYOhTksHSX`Z5LCcJ?iOf69<-hj&3 ze$>prMlIcS*WXb~^*>aG^7LR6|ElI(5Q6jhYtF%Qm071aXljVDms zH#*tA_adq|lU>K6X1>M!{5)!)zANs9`=}KCixF5f#Zp@hHM5qeweN>*F&j0r1Mc-> zsHHiHdOb%svJSy|T)JyIfswfj1d-M;OJ|~lgX5uullr=<+tQ)F>L8w%ZKyAPG zP}Thts)+t}Em4W-D3 zdOd!DDvk^K0zSbse6h91|7&>`>a0JA+Lo74DKDC8yQCHBIjgV*et;$M0rtd@Hun7? z=+g+_q9He-4wS9v-}k7ZJLg`%j2ihP)KMDI){3zN>i)8*`zoT2=vt_weFm!dK0v*G z&!TGTkGAZ8eGt^niYp#B5uJ=9h^L^QcNZICTyI;tp{TWA;+Oq*f<{>`IDM=Ly_i54 zgE|*_U>iJweX&qqOZ9M6l^?(b7}U?>|G(+JhRuj;zib`(up{vv)N=}C*g(>;KK(nB zXsD|9qGnVv(+-+}s2W&~s_HLYuj5eSfGpciqfs3$Mt$!G)Is&ojlKQtdJ<|vy^u4= zIflL!TnHX$Z?U&=8SxX;He58w{yktfYK{Lyt^IS?fWcPX!KimbBh)raMGd4Y>cAO` zO8Gg|2^gGhwbCM+{jU=)gA1{^7PYot<4t^m$MDx7R{a}?+U~fEjkz8_%+`1`Rwdqv z%ET4al2#sW+p{HVfKyOQJJ)sJaGzDLkF*&lp?)un zK`q%y?2jR%^rMn(imHuEs7%I=wjU;)@B;BV)Y6Xgjj@B|1JqBbBd8fv7;7oYMs++7 zb&#w;?c;AyFPlrKZTZ4DvmEM(O+bB~idu>(*bUd94zNe4UF9o2-ioCmHs``XREOJ9 zYkdgyyZ#F|zKz=7_ffwS3QVwGN+Uq<~Rnt(c}HltGbUsQ_2CRxVnqMp|U^I%VGi2YDCu>o~Z9mPPs z|8r<4rC0qI_(6f{z?p0vCZcALg-YeCs0@6F%GBqmCHM}j;APan3Qn;#PeygP05!3V zr~&T5+}i(#)WBn?Dm{)tcm`wf0_uir11?G~1smaR%`zRO;JLw|^HLhAoMwqB=f-FX3s_ODTGWWo8715idb)vv*M6 zKaX0%UoZ>rV*yN`$^O^Tm^stFxX<+nDkEQF8h(Q-vD7Tf&<<3Ue~Q{>7cdf|X4}v6 z=BVQAi%R);eCqcTTVTedx`MquX9(IY2R+p#?>GqawXoBZm#~R)`+jcSvUC zQAs;m52`gfsFpWAp>Fl~9rG^ct`UB1-dn%D@$R+7Gp?`LcH_XTYfI++I&H?P!C{_C zdDm{Z@#a>dJw%`9DtUeRzN>rpU!VWVwFR56y|eB5>x-`LopyEa2X_yyUKY2nXov<= h-yU>r$-;)Ks)c*Dt?Jy+6PNq$!S#D`!aa#2{tu6`<+K0* delta 13111 zcmYM)33yG{`p5Bgh&hB9A~7b^RDu{n5=3GQK_cd%F$OW$Oda!7H7l)BMYUIzss{|!XRFRwmV3pb-2O|!0V^H< zRLSXFY3v!Q=>PwDE6TYpgljP$KEyzLjyo{0j&p7C07hehy3Q5CMBIj{n1a4~&c$O# zoPjHFyK{aQQs22{TsV%qusctwj=>F_i^C?^5vSllJdKMmx}kGHcnrJYd5p(eG0qjm zQ5cJ}aXcPI#^M@su@w%-w)F3I(MaP$0KK)rZb%>QT};JGSOIG{axNV^VIqEpeB}Pd ziWuG42GA3m5U<5#{0SH1i(IUP`|t(4fy_s|6Fac{|Uu=#`kfgZtsEiio;!D^F%i>z}ccQVM#wrYM;aq>*f-5kn zrE_C&6IR4f@>UBQV;LNQ5%?Nbz>kny-8p;*OSg9J3?9H;xHQSR53x0A=92rf4f)ra zG^K)6B;$~KTo&%ZqU4XkyHlu+gWEZ`5)+Z^x$jZm4{dKVZ|6A$DJQoD2jS1iBV1Ak z=Z@fNq<&qG6!K5ux<&Nf6OW)CSdN8XfoZ5>yNP7qO`sCPaUW_xKOu$e3Z>a|nqg(# zhZ?|oWQ^_tYGRd0cTF6HLHLgHA53Eh7QlaFB%bzec#ba+2X&?%upa8c8K@#%hsE#$ z=EvW$3jT?sv3wV@i_1|(`z?Nop8kMNjp2R2&BaO;bGHM`8kmR{NsEGvjvhyJd>udivrBRv>CfEhH5?>@f>0N(} z(Zt1jQ+Sw+n)yO3i$}32{)!s-V^pzLqSQ5^HdqofP??>ELvTB0@BgrL=W^->48%;- z0488P+=RM*5p@DyM-}T`)EXxCu?ciU?S_#Ug3D2v`2efoU#Q(tv9Hz8P%Pu;!Xg?< z-Bt|1|Drnl6?5ZVOu)ye8OQc>?knt$`n+m?)){MI1g2mtj=?0*Ynfej^ z8fktiK`D$t&A2}1!l9@Kj7E)o5*EfIsE*E}I(mc}SkypU^AyyQO~M%b7~}93Y9Q4z zoTKbqoeb7rGh4_7mcxCGN>$B4c5-z`io~tJ7w`lsGk38ZK0`K$D?8XuyfLV4xfk=` z|1dA!$K3c2YN9zQfQlG0g#2p_Vp&<8QGVUO{D`V5Xg9wXg*7 zB-D0XiUn{p*2RxepI=9P?-_<;xPPcM60tZJ24Df4jMH!~9>ToCY-V3#W#Ye4nTr^1 zGarCi#2+9#&5aphHT8{Wz(~tTG-@|A!2tAkr7@32FO0-%_zD&uW&3nF79rk%T7pkd zGrEY)Fle+rC=Hd`Zm0(~o?z@5o@gZtLIj36f zTw#R1FgR-y`o<()=2Uf*k($@gvun~?$E!9rc{=ba6?*`Vy z;AwVXCE#r0snhr%l}5mH`xWehwTa)y@^}%;;u9>5VXRXEMx%Z+CSfg{iZ9`IRK|j4 z+HQ$K%{(2Ix#6e*`cX@_RSi}DHPqS_n`N(7tuTsspywN?8Jxs;{0H^DMoc3KGf_qO z87h-msDXcr74QKjV97a_;U1_Z_NUV*Mq@tei|bG${Qxz>{TPamup$4$Oy_P&IG^b>9=z#PV|Ju(W;`OM|Sr7RU~CU*ZsqpKn$A0crrg1y*dO zP-|Wr!*K|zK_TLi<@Bjh*S=9ilOa4?hmnVo>~ScP~Gs>ZHhVJxwn{0Gpe zOhenJIu6Als17cmW_ATN;@emmLs!@l+Z6SAPt1dZJV#K0tD$=Qb8mj&SI1!t^W$)?xFqZf^uEkjXPc!%)mDYj=d@M!Cb^cump}o7290Y1Xg1>zKt(v{~xB2lMB~T#rQujzJr0p4^aEsx4}l- z49gG?#V}leq4+L_<3UtMKVt;mz>-*Cqh+EZYDrt;SndDOG@9y0?10raSyfL)J$Npv z!*{SJ9z+#u>33}@Dxzv48kL!$*czu}0G>b<>1k9k=Gbg&Uj(brze}MJgp;ryEiD{uu)?^h2wmN|;RC0QLPj zACiBKd=(e+;5saVn^CFXjY|C)R0nrZyCiIzEyW6KLVOqdV(snTbxhE89Ef#4VoTy0 ztbuuU@NZMF-VX9#hsFvn=s{Un3G;qzM{-TS73?HSTFnl+hB`b*1#;t5j*<8PYh#7O98%Z`Nt%m1V!PuS*3tg2{e`7&0BQ~2 zLuKM;)QE%rZ3js^e1Uir=D<~03*W?=cmj3*6D*4{N9_+L-B302HfjP_Fc_ncv5o29 zCDG`Q^RPYM!dckpIImN91hZ=(%g%>o*oEs^_#>A8(gyMrOA$x@$ErRFI}q={P|R_{ zGFlpysmkaNqtTm&QaTYevTaxd52I#s7US_Amcu$H_3A{yqCVe(mGMhdM($z~Mx3&f zbQtC(o`}_O1{T5(Pm%wkH1=~r9iB$TS5VdY2yl^O>k6ScNa*7L3O8n23eW*fvbX z!o(v{9Z$!cI1g2v3sIR^?0x(U}W7u?6P8U~8L# zMTp0uMm`rc(9cj0+=u$!w-|`k+2o2FVlG^Qy8b#UGaIoJzK5^iQ_QFRKlyv_PqEmP z57wa8^e5CBKlVNkxoAJrHE^hp?TL-~e9I43R5!6WaSryRQeP4^fC$vdSrhr6OXmNI zVzr;>NBh4qjV$bj^LcQ@CHv9X`d@p{T`a`sPjCR{zidT167}HK7=)kUc07if>A0V* zW=`T_;=)(>gDtK{ElulR$p0ra&e3Ru^MAD$gX5?*4!Uap&UX@iNc;#j@=e$5VA+lJ ziSu2zna5)^ac9&(7hxTIfH7F*h7GJAb|Kz#gZ#InQRJrGn2tIkS712a#8fQso6V#* zs>2Of5x>Xg_zX3HX8*H)w=)A5690&maq#bUbT36^W*1h&g17wkbJ^gQ9hqD413oBv z+kS=*;z8oHKdkzT-LdW27d61KSPNI7PS7mWOrK&THn?j=nSr|h0BQi2aV6IG-?KND zlQ@eDHShBSgP&k^tn|QslsaMs;@40!+Tr;a9JK|f| z4e#JSO!~(%mHVkpC>n=r|Bs>(#SNFS9~OOPe{(S&wYI-uS*-Qk9?%}k5KlvOxDLzX zDKEZ_ZHUY82eei=80+H4SOl+o{)Kt9{|oqh*;QH$)ln3@YHuKB6O0=RqqRh|@3? zuVZeE3H151Pp+nc_J9;@%m-625chdzVH)wb7>v~l`m$fq8ew_jObo?^sI}jW%EUcX zZG{!G87E^&;$f%>&MD-#k?iGyIy~(873zyWpi)&R$mc%8NYu!$qGtTNXGmcyvINY> z_3@Y=r=v2u5LHXtP?@`mLHNW^qX~@yMeMerzLd=|UoMO1OtD{2|_kET(H3&&CW`8Gyi$zs0jBQ_Q_fOJ%c!>~8b zMQyKZSOaSox0&}s-JgN#cnp@p$*6&^Le2auZ~8? zeZCai5`Tz#(BD`X^M}~|rBL@rptfmCR8e+AE#(By)flAxzng~EEDIOl8Qg)rN?MTw zh1vrXQ7IjSs`|C4qWl3K2gK4BK^C!X^g>tqh{2w zv{iX))ROeX7jX`%x<5ue_!)*^t}^z$GN{be@QgtfTT4{6_d-o{DQY5X(67d68pSYI zS$jYjs@N)faXe~Z$*Aqu3)65Us=AM23A~OY@EOj=;pKeUf1r7QU5J~Nw}b30)aPO0 z?EjiH)`#16I*oeJ6Vw_8N7xLaQN@>xI?*z`>obrda;s4p`U*9}zfr{&P{Eh|(i)4s zi07jwavc-#&kF2+4Is9n%{&v8+6kBk{ip}d#dutb+OB6%KUTpn*tQ#oWr*KHW$q}d z=q{nY{}gqw<*8&#(E{U%C;4fpdiP-kJc*k59juPUE8D;lJlmtzw5R82)c&7`I&gNN zmU2I;7QR7E=n^L5RnIzA>~sHg8oFT-zJM#e5B6bc;w;p8a20j16C5R1trM9kl;*yl89H5tX_Hs0VCBJ>UX1!@n>mHm+_< zmVkqZN2Bil!853a%`6eM)*Vm-NJka>Qq+U5RYiwB0UPNZ%FyjAUZ){NC?puXDh=0XQjE`pj zYd>#|w&FUDdeC2}?ULBQ=ceKe)Qo%$t=dCTGfP0FI2AkNLVN{(@virY@lHC_uik0Y z(iDufnyDPi{@2OWg9}RWPSk@>qGtXhYV96-K1VH8?l{X(7;0C#Qdkc56+?1<`U}7CnnyG$YH3r*d3@}xj#`w*}ADO`(IFw zN6jQS!7|nXHL#(m4yK^C=S~|HPnIiC62}iI8pmQqlFdG&!{yl)zasdVq4Sz zE}@F=D(XQ`Q8Q@R%9s5or>>|BEk*r0UO?@h-@MNQTW6nit~o|?{dLq){}tBP{=Y{< zDX*Gj+oUJzLI1*5xF1Vlt~Rzk!%_G9Q3L$ga}VkuI)qw+^QfY`=3T#!8hF08c9K@X z?7#o3PD2l>jrw8()EV6hb+)fS72keTQC&q9l}q-vBdWL(Q1`b*buLUHNh*M{+l#ZBzI6n_7t_Yp+C=<8*_1x-Y@2E)2(HJcE6({7aVU8L0pM)SaTSfDb~`ecAt+?IvtN zoYcoUT8bTszeGK#OkW#FChAA!HB?pq2bqy;(9aH<$*3CGjM@#~dp^eD#3B9JP4w^P z&`^i(pl-Z@I;rvuusF&y1vR6Q$dTdB;Y#AN8TKRg2`(oN9Aw*YJ;o4cp_Vx3U|ajb zo+0R0b(f`~{hNl`X1!4Z8H~Mf9xCP6P$yv7AyzBhu>tWotd84JYx^VKz`#sj_J3~s z5LNxVhuZG&4YS{tgkkJ|t??W#)WBn?OguoXY3y*@o;^?lT#Q=VH#|?Eit#Cy$Al4f zaP`AP;_cW0@1ru%Y@}`H6x92|+>z{mt=T0mWMKFxd#Rj{IsxyYPPE3O?Zspuen`#0KHG$|cmZ7Prj^9LO`aRSE_7m#2=ANI1_GN{!W*rP8PDI_%8?_XR zu{&-YHBeMhl8-b8hre}dg#54A*bSQuNNp4SD7<5<)i(W|I~YA=#Ozq?CADXuut zQq~qVfI%35Bd{@!LDj@g)IoI?b>APTls@v}0+XzRU{r^#Q4^Se%H%p!20q6??f>&M zv;@DPUZ?M)*0$VaTk}*@hi{=~wj0&)mzWbzd!9pO;5*ENS5Uj+H`M(hQ*5GTQE>}N z|1R0PkmlI~HNw7_j+5~v{1J85$Gzgq{+rB2*oU~qRNJ2K;|$`i7$ z9bd$*_%r&oFJq=zYGz_E@dng3`xNzs>!>xnjRP@Yx-a{``^iL|jN?)Fo$x$^%FOrJ z0e`~RF>;1w=qRelzn;PV*Eai&3lUgO&qN(y6H#kD3tz#FsON;wvF}CUVdB&|J1<7X*WI}|<9d$eTZb&$IVdx7OrARB iqoQK!)!n)Fzd7sf3Ml5=lw;LzC4Ad=oec5SAN7BMG?8=w diff --git a/src/skit/locales/zh_CN/LC_MESSAGES/skit.po b/src/skit/locales/zh_CN/LC_MESSAGES/skit.po index 612c2d9..1bf23d7 100644 --- a/src/skit/locales/zh_CN/LC_MESSAGES/skit.po +++ b/src/skit/locales/zh_CN/LC_MESSAGES/skit.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: skit 0.0.1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-07-23 22:54-0700\n" +"POT-Creation-Date: 2026-07-26 01:20-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh_CN\n" @@ -18,61 +18,61 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/skit/analysis.py:147 +#: src/skit/analysis.py:162 #, python-format msgid "The parameter definitions for %(name)s have drifted from the script:" msgstr "%(name)s 的参数定义与脚本内容已漂移:" -#: src/skit/analysis.py:156 +#: src/skit/analysis.py:171 #, python-brace-format, python-format msgid "%(name)s is no longer read from the environment (its ${...:-default} was removed or overridden by a plain assignment) — your value would be silently ignored. Re-add or resync." msgstr "%(name)s 不再从环境变量读取(其 ${...:-default} 已被删除,或被普通赋值覆盖)——你设置的值会被悄悄忽略。请重新添加或执行 resync。" -#: src/skit/analysis.py:166 +#: src/skit/analysis.py:181 #, python-format msgid "%(name)s: injection target no longer exists (dropped from this run's form)" msgstr "%(name)s:找不到注入目标(已从本次表单剔除)" -#: src/skit/analysis.py:174 +#: src/skit/analysis.py:189 #, python-format msgid "%(name)s: type changed from %(old)s to %(new)s in the source (still injected — double-check the value)" msgstr "%(name)s:源码中的类型已从 %(old)s 变为 %(new)s(仍会注入,请确认值)" -#: src/skit/analysis.py:182 +#: src/skit/analysis.py:197 #, python-format msgid "%(name)s: its prompt no longer matches a unique input/read call; falling back to position (still injected — double-check this lands on the right question, especially if it's a secret)" msgstr "%(name)s:其提示文字已无法对应到唯一的 input/read 调用;改以位置对应(仍会注入——请再次确认其对应到正确的问题,尤其是机密参数)" -#: src/skit/analysis.py:190 +#: src/skit/analysis.py:205 #, python-format msgid "To refresh the definitions, run: skit params %(name)s --resync" msgstr "若要更新定义,请运行:skit params %(name)s --resync" -#: src/skit/analysis.py:204 +#: src/skit/analysis.py:219 #, python-format msgid "%(name)s isn't a managed parameter; skipped." msgstr "%(name)s 不是受管理的参数;已跳过。" -#: src/skit/analysis.py:205 +#: src/skit/analysis.py:220 #, python-format msgid "Dropped %(name)s: it no longer exists in the script." msgstr "已剔除 %(name)s:脚本中已找不到。" -#: src/skit/analysis.py:206 +#: src/skit/analysis.py:221 #, python-format msgid "%(name)s is already managed; skipped." msgstr "%(name)s 已在管理中;已跳过。" -#: src/skit/analysis.py:208 +#: src/skit/analysis.py:223 #, python-format msgid "%(name)s isn't a detectable parameter in the current script; skipped." msgstr "%(name)s 在当前脚本中检测不到;已跳过。" -#: src/skit/analysis.py:211 +#: src/skit/analysis.py:226 msgid "Could not parse the script (syntax error); resync skipped. Parameter definitions are unchanged." msgstr "无法解析脚本(语法错误);已跳过重新同步。参数定义保持不变。" -#: src/skit/analysis.py:215 +#: src/skit/analysis.py:230 #, python-format msgid "%(name)s: re-anchored to its current position after its prompt stopped matching uniquely; double-check the prompt/secret assignment is still correct." msgstr "%(name)s:因提示文字无法唯一对应,已重新锚定到其当前位置;请再次确认提示文字/机密设置仍然正确。" @@ -129,19 +129,19 @@ msgstr "%(kind)s 条目不接受依赖包——去掉 --dep。" msgid "A Python constraint doesn't apply to %(kind)s scripts." msgstr "Python 约束不适用于 %(kind)s 脚本。" -#: src/skit/cli.py:390 src/skit/cli.py:775 src/skit/cli.py:1213 -#: src/skit/cli.py:2012 +#: src/skit/cli.py:390 src/skit/cli.py:763 src/skit/cli.py:1201 +#: src/skit/cli.py:2000 msgid "Name in skit" msgstr "在 skit 里的名称" -#: src/skit/cli.py:396 src/skit/cli.py:1483 src/skit/cli.py:2018 -#: src/skit/tui_add.py:245 +#: src/skit/cli.py:396 src/skit/cli.py:1471 src/skit/cli.py:2006 +#: src/skit/tui_add.py:246 msgid "Description (optional)" msgstr "描述(可留空)" #: src/skit/cli.py:420 src/skit/cli.py:428 src/skit/store.py:224 #: src/skit/store.py:355 src/skit/store.py:427 src/skit/store.py:639 -#: src/skit/tui_add.py:356 +#: src/skit/tui_add.py:357 #, python-format msgid "File not found: %(path)s" msgstr "找不到文件:%(path)s" @@ -151,8 +151,8 @@ msgstr "找不到文件:%(path)s" msgid "Not a file: %(path)s" msgstr "不是文件:%(path)s" -#: src/skit/cli.py:457 src/skit/cli.py:1131 src/skit/cli.py:2646 -#: src/skit/tui_add.py:1415 +#: src/skit/cli.py:457 src/skit/cli.py:1119 src/skit/cli.py:2640 +#: src/skit/tui_add.py:1431 msgid " (secret)" msgstr " (机密)" @@ -170,7 +170,7 @@ msgstr "%(num)s. input() 第 %(ordinal)s 个:%(prompt)s%(secret)s" msgid "⚠ looks like a loop accumulator — probably not a parameter" msgstr "⚠ 看起来是循环的累加变量,多半不是参数" -#: src/skit/cli.py:491 src/skit/tui_add.py:1039 +#: src/skit/cli.py:491 src/skit/tui_add.py:1049 msgid "This script reads command-line arguments; the run form has an extra-arguments field for them." msgstr "这个脚本会吃命令行参数;运行表单里有「额外参数」栏可以填。" @@ -179,13 +179,13 @@ msgstr "这个脚本会吃命令行参数;运行表单里有「额外参数」 msgid "💡 %(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (skit edit %(script)s)." msgstr "💡 %(names)s 直接写在代码里,skit 没法把它们变成表单字段。想管理其中一个,先在脚本开头给它取个名字,例如 OUTPUT = '…'(skit edit %(script)s)。" -#: src/skit/cli.py:521 src/skit/tui_add.py:981 +#: src/skit/cli.py:521 src/skit/tui_add.py:991 #, python-format msgid "✓ skit read this script's own arguments (%(count)s field). Running it opens a form — nothing to memorize." msgid_plural "✓ skit read this script's own arguments (%(count)s fields). Running it opens a form — nothing to memorize." msgstr[0] "✓ skit 看懂了这个脚本自己的参数(共 %(count)s 项)。运行时会打开表单——不用背任何命令。" -#: src/skit/cli.py:531 src/skit/tui_add.py:996 +#: src/skit/cli.py:531 src/skit/tui_add.py:1006 #, python-format msgid "This script parses its own arguments (%(names)s); skit couldn't model them statically, so the run form offers an extra-arguments field." msgstr "这个脚本自己解析参数(%(names)s);skit 无法静态读懂它们,运行表单会提供「额外参数(原样传给脚本)」栏。" @@ -204,1720 +204,1738 @@ msgstr "哪些交给 skit 管理?(编号如 1,3 / all / none)" msgid "Reference mode never touches the original file, so parameter setup was skipped." msgstr "reference 模式绝不改动原文件;跳过参数设置。" -#: src/skit/cli.py:719 +#: src/skit/cli.py:707 #, python-format msgid "The draft's #! names no interpreter skit knows — add it with: skit add %(path)s --kind " msgstr "草稿的 #! 指定了 skit 不认识的解释器——请用以下命令添加:skit add %(path)s --kind <语言>" -#: src/skit/cli.py:732 +#: src/skit/cli.py:720 #, python-format msgid "--dep/--python are python flags, but the draft's shebang names %(kind)s — drop them, or keep the python shebang." msgstr "--dep/--python 是 python 专用标志,但草稿的 shebang 指定的是 %(kind)s——去掉它们,或改回 python shebang。" -#: src/skit/cli.py:760 +#: src/skit/cli.py:748 msgid "--edit opens your editor, which --no-input forbids — pipe the script in instead: skit add - -n NAME" msgstr "--edit 会打开你的编辑器,而 --no-input 禁止这么做——请改用管道把脚本传进来:skit add - -n NAME" -#: src/skit/cli.py:768 +#: src/skit/cli.py:756 msgid "Writing a new script in an editor needs an interactive terminal." msgstr "用编辑器新建脚本需要交互式终端。" -#: src/skit/cli.py:777 src/skit/cli.py:1215 src/skit/cli.py:3385 -#: src/skit/cli.py:3429 src/skit/config.py:620 src/skit/store.py:1135 -#: src/skit/tui_add.py:446 src/skit/tui_runner.py:160 +#: src/skit/cli.py:765 src/skit/cli.py:1203 src/skit/cli.py:3388 +#: src/skit/cli.py:3432 src/skit/config.py:620 src/skit/store.py:1133 +#: src/skit/tui_add.py:447 src/skit/tui_runner.py:160 msgid "A name is required." msgstr "必须提供名称。" -#: src/skit/cli.py:788 src/skit/cli.py:1224 src/skit/store.py:707 +#: src/skit/cli.py:776 src/skit/cli.py:1212 src/skit/store.py:707 #, python-format msgid "The name %(name)s is already taken — pick another name." msgstr "名称 %(name)s 已被占用——请换一个名称。" -#: src/skit/cli.py:800 src/skit/cli.py:1239 +#: src/skit/cli.py:788 src/skit/cli.py:1227 msgid "Opening your editor…" msgstr "正在打开编辑器…" -#: src/skit/cli.py:808 src/skit/cli.py:1109 src/skit/cli.py:1245 -#: src/skit/cli.py:2152 src/skit/editor.py:105 src/skit/langs/launch.py:712 -#: src/skit/store.py:445 src/skit/tui_add.py:531 src/skit/tui_add.py:747 -#: src/skit/tui_add.py:1079 src/skit/tui_add.py:1265 src/skit/tui_add.py:1543 -#: src/skit/tui_settings.py:364 +#: src/skit/cli.py:796 src/skit/cli.py:1097 src/skit/cli.py:1233 +#: src/skit/cli.py:2140 src/skit/editor.py:105 src/skit/langs/launch.py:712 +#: src/skit/store.py:445 src/skit/tui_add.py:532 src/skit/tui_add.py:749 +#: src/skit/tui_add.py:1089 src/skit/tui_add.py:1281 src/skit/tui_add.py:1559 +#: src/skit/tui_settings.py:359 #, python-format msgid "Can't read %(path)s: %(error)s" msgstr "无法读取 %(path)s:%(error)s" -#: src/skit/cli.py:813 +#: src/skit/cli.py:801 msgid "Nothing was written, so no script was added." msgstr "没有写入任何内容,因此没有加入脚本。" -#: src/skit/cli.py:883 src/skit/cli.py:936 src/skit/cli.py:1285 +#: src/skit/cli.py:871 src/skit/cli.py:924 src/skit/cli.py:1273 msgid "Reading the script from stdin needs an explicit --name." msgstr "从 stdin 读脚本必须用 --name 指定名称。" -#: src/skit/cli.py:893 src/skit/cli.py:943 src/skit/cli.py:1305 +#: src/skit/cli.py:881 src/skit/cli.py:931 src/skit/cli.py:1293 msgid "Nothing arrived on stdin, so there is nothing to add." msgstr "stdin 没有收到任何内容,没有东西可添加。" -#: src/skit/cli.py:991 +#: src/skit/cli.py:979 msgid "# New prompt" msgstr "# 新提示词" -#: src/skit/cli.py:1007 +#: src/skit/cli.py:995 msgid "Custom agents: skit runner add mycli -- mycli run {{prompt}} (see skit runner --help)" msgstr "自定义 agent:skit runner add mycli -- mycli run {{prompt}}(详见 skit runner --help)" -#: src/skit/cli.py:1031 src/skit/cli.py:3482 +#: src/skit/cli.py:1019 src/skit/cli.py:3485 #, python-format msgid "Unknown runner: %(runner)s. Configured runners: %(names)s" msgstr "未知的执行器:%(runner)s。已配置的执行器:%(names)s" -#: src/skit/cli.py:1046 +#: src/skit/cli.py:1034 #, python-format msgid "The draft is no longer at %(path)s, so nothing was added." msgstr "草稿已不在 %(path)s,因此没有添加任何内容。" -#: src/skit/cli.py:1050 +#: src/skit/cli.py:1038 #, python-format msgid "Your draft was kept at %(path)s — fix the problem and add it with: skit add %(path)s" msgstr "你的草稿保留在 %(path)s——修正问题后用以下命令加入:skit add %(path)s" -#: src/skit/cli.py:1054 src/skit/tui_add.py:545 src/skit/tui_add.py:571 +#: src/skit/cli.py:1042 src/skit/tui_add.py:546 src/skit/tui_add.py:572 #, python-format msgid "Your draft was kept at %(path)s" msgstr "你的草稿保留在 %(path)s" -#: src/skit/cli.py:1071 +#: src/skit/cli.py:1059 msgid "- = no pin (the run form asks each time)" msgstr "- = 不固定(每次在运行表单选择)" -#: src/skit/cli.py:1074 src/skit/cli.py:2887 +#: src/skit/cli.py:1062 src/skit/cli.py:2881 msgid "Run this prompt with which agent?" msgstr "用哪个执行器运行这个提示词?" -#: src/skit/cli.py:1129 src/skit/cli.py:2644 +#: src/skit/cli.py:1117 src/skit/cli.py:2638 msgid "Detected placeholders (each becomes a form field):" msgstr "检测到的占位符(每个都会成为表单字段):" -#: src/skit/cli.py:1136 src/skit/cli.py:2651 src/skit/tui_add.py:295 -#: src/skit/tui_add.py:1423 src/skit/tui_settings.py:704 +#: src/skit/cli.py:1124 src/skit/cli.py:2645 src/skit/tui_add.py:296 +#: src/skit/tui_add.py:1439 src/skit/tui_settings.py:699 #, python-format msgid "…and %(count)s more" msgstr "……还有 %(count)s 个" -#: src/skit/cli.py:1140 +#: src/skit/cli.py:1128 msgid "Manage which? (all / none / off / numbers like 1,3)" msgstr "要管理哪些?(all / none / off / 或编号如 1,3)" -#: src/skit/cli.py:1169 +#: src/skit/cli.py:1157 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written (turn it on with: skit params %(name)s --interpolate)" msgstr "变量插入已关闭——正文会原封不动送达 agent(开启:skit params %(name)s --interpolate)" -#: src/skit/cli.py:1174 +#: src/skit/cli.py:1162 #, python-format msgid "Detected %(count)s placeholders — too many to manage automatically, so none were. Manage the ones you need with: skit params %(name)s --add NAME, or turn insertion off with --no-interpolate." msgstr "检测到 %(count)s 个占位符——数量太多,未自动管理任何一个。需要的请用 skit params %(name)s --add 名称 管理,或用 --no-interpolate 关闭插值。" -#: src/skit/cli.py:1178 +#: src/skit/cli.py:1166 #, python-format msgid "Runs with %(runner)s (change it with: skit params %(name)s --runner NAME)" msgstr "以 %(runner)s 运行(更改:skit params %(name)s --runner NAME)" -#: src/skit/cli.py:1206 +#: src/skit/cli.py:1194 msgid "--prompt with no path opens your editor, which --no-input forbids — pipe the body in instead: skit add - --prompt -n NAME" msgstr "--prompt 未带路径时会打开你的编辑器,而 --no-input 禁止这么做——请改用管道把正文传进来:skit add - --prompt -n NAME" -#: src/skit/cli.py:1250 +#: src/skit/cli.py:1238 msgid "Nothing was written, so no prompt was added." msgstr "没有写入任何内容,因此没有添加提示词。" -#: src/skit/cli.py:1356 +#: src/skit/cli.py:1344 #, python-format msgid "Unknown kind: %(kind)s. Choose from: %(kinds)s" msgstr "未知类型:%(kind)s。可选:%(kinds)s" -#: src/skit/cli.py:1376 src/skit/tui_add.py:120 +#: src/skit/cli.py:1364 src/skit/tui_add.py:121 #, python-format msgid "The #! in %(file)s names no interpreter skit knows. What is it?" msgstr "%(file)s 的 #! 指定了 skit 不认识的解释器。这是什么?" -#: src/skit/cli.py:1378 src/skit/tui_add.py:122 +#: src/skit/cli.py:1366 src/skit/tui_add.py:123 #, python-format msgid "What is %(file)s? skit can't tell from the name." msgstr "%(file)s 是什么?skit 无法从名称判断。" -#: src/skit/cli.py:1383 +#: src/skit/cli.py:1371 msgid "- = cancel" msgstr "- = 取消" -#: src/skit/cli.py:1385 src/skit/cli.py:1465 +#: src/skit/cli.py:1373 src/skit/cli.py:1453 msgid "Which one?" msgstr "选哪个?" -#: src/skit/cli.py:1399 +#: src/skit/cli.py:1387 #, python-format msgid "Detected parameters: %(names)s (the run form asks for them; your last values are remembered)" msgstr "检测到参数:%(names)s(运行表单会询问;会记住你上次的值)" -#: src/skit/cli.py:1459 +#: src/skit/cli.py:1447 msgid "What would you like to add?" msgstr "你想添加什么?" -#: src/skit/cli.py:1460 +#: src/skit/cli.py:1448 msgid "A file you already have — a script, program, or prompt" msgstr "你已有的文件——脚本、程序或提示词" -#: src/skit/cli.py:1461 +#: src/skit/cli.py:1449 msgid "A new script, written in your editor" msgstr "一支新脚本,在你的编辑器里写" -#: src/skit/cli.py:1462 +#: src/skit/cli.py:1450 msgid "A new AI-agent prompt, written in your editor" msgstr "一条给 AI agent 的新提示词,在你的编辑器里写" -#: src/skit/cli.py:1463 +#: src/skit/cli.py:1451 #, python-brace-format msgid "A command template (e.g. ffmpeg -i {input})" msgstr "一条命令模板(例如 ffmpeg -i {input})" -#: src/skit/cli.py:1476 +#: src/skit/cli.py:1464 msgid "Command template" msgstr "命令模板" -#: src/skit/cli.py:1479 src/skit/tui_add.py:242 +#: src/skit/cli.py:1467 src/skit/tui_add.py:243 msgid "Name for the command" msgstr "命令的名称" -#: src/skit/cli.py:1489 +#: src/skit/cli.py:1477 msgid "Path to the file" msgstr "文件路径" -#: src/skit/cli.py:1496 +#: src/skit/cli.py:1484 msgid "Add a script, executable, prompt, or command to skit." msgstr "把脚本、可执行文件、提示词或命令加入 skit。" -#: src/skit/cli.py:1498 +#: src/skit/cli.py:1486 #, python-brace-format msgid "Examples: skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" msgstr "例:skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" -#: src/skit/cli.py:1505 +#: src/skit/cli.py:1493 msgid "Path to a script, executable, or prompt, or '-' to read one from stdin (omit it in a terminal and skit asks what to add)" msgstr "脚本、可执行文件或提示词的路径;“-”表示从 stdin 读取(在终端里省略它,skit 会问你要添加什么)" -#: src/skit/cli.py:1510 +#: src/skit/cli.py:1498 msgid "Name / alias (defaults to the file name)" msgstr "名称 / alias(默认用文件名)" -#: src/skit/cli.py:1516 +#: src/skit/cli.py:1504 msgid "Description (inferred from the source when possible)" msgstr "说明(可行时从来源推断)" -#: src/skit/cli.py:1519 +#: src/skit/cli.py:1507 msgid "Write a brand-new script in your editor, then add it" msgstr "在编辑器里新写一支脚本,再加入 skit" -#: src/skit/cli.py:1524 +#: src/skit/cli.py:1512 msgid "Reference mode: link to the original file instead of copying it" msgstr "reference 模式:不复制,链接原文件" -#: src/skit/cli.py:1529 +#: src/skit/cli.py:1517 msgid "Force the executable kind (normally inferred from the file itself)" msgstr "强制当作可执行文件(一般会自动判断)" -#: src/skit/cli.py:1534 +#: src/skit/cli.py:1522 msgid "Force the language kind (e.g. shell, js) for an extensionless file" msgstr "强制指定语言类型(如 shell、js),用于没有扩展名的文件" -#: src/skit/cli.py:1537 +#: src/skit/cli.py:1525 #, python-brace-format msgid "Register a command template, e.g. --cmd 'ffmpeg -i {input}'" msgstr "登记命令模板,如 --cmd 'ffmpeg -i {input}'" -#: src/skit/cli.py:1543 +#: src/skit/cli.py:1531 msgid "Add the file as a prompt for an AI agent (with no path: draft one in your editor)" msgstr "将文件添加为 AI agent 的提示词(不给路径则在编辑器中撰写新的)" -#: src/skit/cli.py:1549 +#: src/skit/cli.py:1537 msgid "Pin the agent a prompt entry runs with (see skit runner list)" msgstr "固定提示词条目使用的执行器(见 skit runner list)" -#: src/skit/cli.py:1555 +#: src/skit/cli.py:1543 msgid "Prompt only: no variable insertion at all — the body travels exactly as written" msgstr "仅提示词:完全不做变量插入——正文原封不动送达" -#: src/skit/cli.py:1561 +#: src/skit/cli.py:1549 msgid "A dependency (repeat for more; skips the interactive question)" msgstr "一项依赖(可重复;会跳过交互询问)" -#: src/skit/cli.py:1564 src/skit/cli.py:4884 +#: src/skit/cli.py:1552 src/skit/cli.py:4912 msgid "Python version constraint, e.g. \">=3.11\"" msgstr "Python 版本约束,如 \">=3.11\"" -#: src/skit/cli.py:1567 +#: src/skit/cli.py:1555 msgid "Never prompt; accept the detected suggestions" msgstr "从不询问;直接采用检测到的建议" -#: src/skit/cli.py:1576 +#: src/skit/cli.py:1564 msgid "--prompt names the kind outright — drop --edit/--exe/--kind/--cmd." msgstr "--prompt 已直接指定类型——请去掉 --edit/--exe/--kind/--cmd。" -#: src/skit/cli.py:1589 +#: src/skit/cli.py:1577 msgid "stdin ('-')" msgstr "stdin('-')" -#: src/skit/cli.py:1590 +#: src/skit/cli.py:1578 msgid "a file path" msgstr "文件路径" -#: src/skit/cli.py:1598 +#: src/skit/cli.py:1586 #, python-format msgid "%(flags)s each pick a different way to add — use exactly one (nothing was added)." msgstr "%(flags)s 各自代表一种不同的添加方式——请只用其中一种(未添加任何内容)。" -#: src/skit/cli.py:1639 +#: src/skit/cli.py:1627 msgid "a --cmd template takes only --name/--description" msgstr "--cmd 模板只接受 --name/--description" -#: src/skit/cli.py:1642 +#: src/skit/cli.py:1630 msgid "stdin authors a brand-new copy, and --ref/--exe need an existing file" msgstr "stdin 会撰写一份全新副本,而 --ref/--exe 需要现成的文件" -#: src/skit/cli.py:1644 +#: src/skit/cli.py:1632 msgid "--edit drafts a fresh script: its kind comes from the shebang you write (e.g. #!/usr/bin/env bash), --ref/--exe need an existing file, and a prompt is drafted with skit add --prompt" msgstr "--edit 会起草一个全新脚本:它的类型取自你写的 shebang(例如 #!/usr/bin/env bash),--ref/--exe 需要现成的文件,而提示词要用 skit add --prompt 起草" -#: src/skit/cli.py:1649 +#: src/skit/cli.py:1637 msgid "a drafted prompt takes only --name/--description/--runner/--no-interpolate" msgstr "草稿提示词只接受 --name/--description/--runner/--no-interpolate" -#: src/skit/cli.py:1657 +#: src/skit/cli.py:1645 #, python-format msgid "%(flags)s can't apply here — %(hint)s (nothing was added)." msgstr "%(flags)s 在这里无法应用——%(hint)s(未添加任何内容)。" -#: src/skit/cli.py:1670 src/skit/cli.py:1988 +#: src/skit/cli.py:1658 src/skit/cli.py:1976 msgid "--no-interpolate only applies to prompt entries — add one with --prompt." msgstr "--no-interpolate 只适用于提示词条目——用 --prompt 添加一个。" -#: src/skit/cli.py:1675 src/skit/cli.py:1983 +#: src/skit/cli.py:1663 src/skit/cli.py:1971 msgid "--runner only applies to prompt entries — add one with --prompt." msgstr "--runner 只适用于提示词条目——用 --prompt 添加一个。" -#: src/skit/cli.py:1690 +#: src/skit/cli.py:1678 msgid "--exe needs an existing program on disk — stdin and the editor author scripts." msgstr "--exe 需要磁盘上既有的程序——stdin 与编辑器用来编写脚本。" -#: src/skit/cli.py:1699 +#: src/skit/cli.py:1687 msgid "--dep/--python are script flags — a prompt has no dependencies." msgstr "--dep/--python 是脚本标志——提示词没有依赖。" -#: src/skit/cli.py:1708 +#: src/skit/cli.py:1696 msgid "--runner/--no-interpolate only apply to prompt entries — pipe one with --prompt." msgstr "--runner/--no-interpolate 只适用于提示词条目——用 --prompt 以管道传入一个。" -#: src/skit/cli.py:1726 +#: src/skit/cli.py:1714 msgid "The piped text's #! names no interpreter skit knows — pass --kind to choose one." msgstr "管道文本的 #! 指定了 skit 不认识的解释器——请用 --kind <语言> 指定一个。" -#: src/skit/cli.py:1739 +#: src/skit/cli.py:1727 #, python-format msgid "--dep/--python don't apply to a %(kind)s entry (piped text's kind)." msgstr "--dep/--python 不适用于 %(kind)s 条目(管道文本的种类)。" -#: src/skit/cli.py:1759 +#: src/skit/cli.py:1747 msgid "A --cmd entry needs a --name" msgstr "--cmd 条目必须指定 --name" -#: src/skit/cli.py:1779 +#: src/skit/cli.py:1767 msgid "Provide a source path — or pipe the text in (skit add - -n NAME; add --prompt for an AI-agent prompt), or register a command template with --cmd." msgstr "请提供来源路径——或把文本管道进来(skit add - -n NAME;AI-agent 提示词再加 --prompt),或用 --cmd 登记一条命令模板。" -#: src/skit/cli.py:1811 +#: src/skit/cli.py:1799 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …), or pick a lane outright with %(lanes)s (nothing was added)." msgstr "%(flags)s 需要一个来源——在同一条命令里给出路径(skit add PATH …),或直接用 %(lanes)s 选定一条添加方式(没有添加任何东西)。" -#: src/skit/cli.py:1818 +#: src/skit/cli.py:1806 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …) (nothing was added)." msgstr "%(flags)s 需要一个来源——在同一条命令里给出路径(skit add PATH …)(没有添加任何东西)。" -#: src/skit/cli.py:1840 +#: src/skit/cli.py:1828 msgid "Use --kind or --exe, not both." msgstr "--kind 与 --exe 只能择一。" -#: src/skit/cli.py:1868 +#: src/skit/cli.py:1856 #, python-format msgid "%(file)s is a directory — pass --exe to add it as a program that runs directly." msgstr "%(file)s 是一个目录——加 --exe 可把它作为直接运行的程序加入。" -#: src/skit/cli.py:1876 +#: src/skit/cli.py:1864 #, python-format msgid "%(file)s is a directory. Add it as a program that runs directly?" msgstr "%(file)s 是一个目录。要作为直接运行的程序添加吗?" -#: src/skit/cli.py:1910 +#: src/skit/cli.py:1898 #, python-format msgid "%(file)s is one of skit's own kept drafts — a resumed draft is always added as a copy (and consumed on success), which a reference or program entry can't be. Drop %(flags)s." msgstr "%(file)s 是 skit 自己保留的草稿——恢复草稿一律以副本加入(成功后即消耗),而 reference 或程序项目做不到这点。请去掉 %(flags)s。" -#: src/skit/cli.py:1917 +#: src/skit/cli.py:1905 #, python-format msgid "%(file)s is one of skit's own kept drafts, and a draft is always added as a script or prompt copy — pass --kind to name its language." msgstr "%(file)s 是 skit 自己保留的草稿,而草稿一律以脚本或提示词副本加入——请用 --kind <语言> 指定它的语言。" -#: src/skit/cli.py:1941 +#: src/skit/cli.py:1929 #, python-format msgid "%(file)s looks like a prompt. Add it as one?" msgstr "%(file)s 看起来是提示词。要作为提示词添加吗?" -#: src/skit/cli.py:2034 +#: src/skit/cli.py:2022 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one." msgstr "%(file)s 的 #! 指定了 skit 不认识的解释器——请用 --kind <语言> 指定一个。" -#: src/skit/cli.py:2039 +#: src/skit/cli.py:2027 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one, or --exe to run it directly." msgstr "%(file)s 的 #! 指定了 skit 不认识的解释器——请用 --kind <语言> 指定一个,或用 --exe 直接运行。" -#: src/skit/cli.py:2051 +#: src/skit/cli.py:2039 #, python-format msgid "%(file)s is a kept draft skit can't classify — pass --kind to add it as a script, or --prompt for an AI-agent prompt." msgstr "%(file)s 是 skit 无法分类的保留草稿——请用 --kind <语言> 将它加入为脚本,或用 --prompt 加入为 AI agent 提示词。" -#: src/skit/cli.py:2060 +#: src/skit/cli.py:2048 #, python-format msgid "%(file)s isn't a script or an executable — pass --kind for an extensionless script, --prompt for an AI-agent prompt, --exe for a program, or --cmd for a command template." msgstr "%(file)s 不是脚本也不是可执行文件——没有扩展名的脚本请加 --kind ,AI agent 提示词请加 --prompt,程序请加 --exe,命令模板请用 --cmd。" -#: src/skit/cli.py:2210 +#: src/skit/cli.py:2198 #, python-format msgid "(%(mode)s mode)" msgstr "(%(mode)s 模式)" -#: src/skit/cli.py:2215 +#: src/skit/cli.py:2203 #, python-format msgid "Added: %(name)s" msgstr "已加入:%(name)s" -#: src/skit/cli.py:2219 +#: src/skit/cli.py:2207 #, python-format msgid "Description: %(desc)s" msgstr "描述:%(desc)s" -#: src/skit/cli.py:2223 src/skit/cli.py:2381 +#: src/skit/cli.py:2211 src/skit/cli.py:2369 #, python-format msgid "Dependencies: %(deps)s" msgstr "依赖:%(deps)s" -#: src/skit/cli.py:2227 +#: src/skit/cli.py:2215 #, python-format msgid "Managed parameters: %(names)s" msgstr "管理中的参数:%(names)s" -#: src/skit/cli.py:2229 src/skit/cli.py:2422 +#: src/skit/cli.py:2217 src/skit/cli.py:2410 #, python-format msgid "Run it: skit run %(name)s" msgstr "运行:skit run %(name)s" -#: src/skit/cli.py:2232 +#: src/skit/cli.py:2220 #, python-format msgid "Secret parameter values are never saved by skit: %(names)s" msgstr "skit 永远不会保存机密参数的值:%(names)s" -#: src/skit/cli.py:2238 +#: src/skit/cli.py:2226 msgid "When this prompt runs, the selected agent receives those values as plaintext and may log or sync them." msgstr "运行此提示词时,所选 agent 会以明文收到这些值,并可能记录或同步它们。" -#: src/skit/cli.py:2250 +#: src/skit/cli.py:2238 msgid "List every registered entry." msgstr "列出所有已登记的条目。" -#: src/skit/cli.py:2252 src/skit/cli.py:2463 src/skit/cli.py:3247 -#: src/skit/cli.py:3624 src/skit/cli.py:4896 src/skit/cli.py:5123 -#: src/skit/cli.py:5467 +#: src/skit/cli.py:2240 src/skit/cli.py:2451 src/skit/cli.py:3250 +#: src/skit/cli.py:3627 src/skit/cli.py:4924 src/skit/cli.py:5151 +#: src/skit/cli.py:5495 msgid "Output as JSON" msgstr "以 JSON 输出" -#: src/skit/cli.py:2275 +#: src/skit/cli.py:2263 msgid "No entries yet. Add one with: skit add " msgstr "还没有任何条目。用 skit add 添加一个。" -#: src/skit/cli.py:2280 src/skit/tui.py:370 src/skit/tui.py:1059 -#: src/skit/tui_add.py:638 src/skit/tui_add.py:855 src/skit/tui_add.py:1328 +#: src/skit/cli.py:2268 src/skit/tui.py:377 src/skit/tui.py:1088 +#: src/skit/tui_add.py:639 src/skit/tui_add.py:865 src/skit/tui_add.py:1344 msgid "Name" msgstr "名称" -#: src/skit/cli.py:2281 src/skit/cli.py:3942 src/skit/tui.py:370 -#: src/skit/tui.py:1059 +#: src/skit/cli.py:2269 src/skit/cli.py:3974 src/skit/tui.py:377 +#: src/skit/tui.py:1088 msgid "Kind" msgstr "类型" -#: src/skit/cli.py:2282 src/skit/tui_add.py:640 src/skit/tui_add.py:857 -#: src/skit/tui_add.py:1333 +#: src/skit/cli.py:2270 src/skit/tui_add.py:641 src/skit/tui_add.py:867 +#: src/skit/tui_add.py:1349 msgid "Description" msgstr "说明" -#: src/skit/cli.py:2307 src/skit/cli.py:2308 src/skit/cli.py:2448 -#: src/skit/cli.py:3689 src/skit/cli.py:3690 +#: src/skit/cli.py:2295 src/skit/cli.py:2296 src/skit/cli.py:2436 +#: src/skit/cli.py:3716 src/skit/cli.py:3717 msgid "yes" msgstr "是" -#: src/skit/cli.py:2363 +#: src/skit/cli.py:2351 #, python-format msgid "Source: %(path)s" msgstr "来源:%(path)s" -#: src/skit/cli.py:2366 +#: src/skit/cli.py:2354 #, python-format msgid "Working directory: %(dir)s" msgstr "工作目录:%(dir)s" -#: src/skit/cli.py:2370 +#: src/skit/cli.py:2358 #, python-format msgid "Interpreter: %(program)s" msgstr "解释器:%(program)s" -#: src/skit/cli.py:2385 src/skit/cli.py:4862 +#: src/skit/cli.py:2373 src/skit/cli.py:4890 #, python-format msgid "Python constraint: %(python)s" msgstr "Python 版本约束:%(python)s" -#: src/skit/cli.py:2389 +#: src/skit/cli.py:2377 #, python-format msgid "Needs: %(needs)s" msgstr "所需命令:%(needs)s" -#: src/skit/cli.py:2393 +#: src/skit/cli.py:2381 #, python-format msgid "Command template: %(template)s" msgstr "命令模板:%(template)s" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 #, python-format msgid "Runner: %(runner)s" msgstr "执行器:%(runner)s" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 msgid "(asks at run time)" msgstr "(运行时询问)" -#: src/skit/cli.py:2400 +#: src/skit/cli.py:2388 msgid "Variable insertion: off (the body travels as written)" msgstr "变量插入:关闭(正文原样送达)" -#: src/skit/cli.py:2404 src/skit/cli.py:3067 +#: src/skit/cli.py:2392 src/skit/cli.py:3061 msgid "skit could not model this script's own arguments; pass them after -- instead." msgstr "skit 无法读懂这个脚本自己的参数声明;请把参数放在 -- 之后直接传入。" -#: src/skit/cli.py:2410 +#: src/skit/cli.py:2398 msgid "No form fields — arguments after -- go to the selected agent." msgstr "没有表单字段——接在 -- 之后的参数会传给所选 agent。" -#: src/skit/cli.py:2412 +#: src/skit/cli.py:2400 msgid "No form fields — arguments after -- are appended to the command." msgstr "没有表单字段——接在 -- 之后的参数会附加到命令末尾。" -#: src/skit/cli.py:2415 +#: src/skit/cli.py:2403 msgid "No form fields — arguments after -- pass straight through to the script." msgstr "没有表单字段——接在 -- 之后的参数会透传给脚本。" -#: src/skit/cli.py:2420 +#: src/skit/cli.py:2408 #, python-format msgid "Presets: %(names)s" msgstr "参数组合:%(names)s" -#: src/skit/cli.py:2429 src/skit/cli.py:3728 src/skit/cli.py:3941 +#: src/skit/cli.py:2417 src/skit/cli.py:3755 src/skit/cli.py:3973 msgid "Parameter" msgstr "参数" -#: src/skit/cli.py:2430 src/skit/cli.py:3730 src/skit/cli.py:3943 +#: src/skit/cli.py:2418 src/skit/cli.py:3757 src/skit/cli.py:3975 msgid "Type" msgstr "类型" -#: src/skit/cli.py:2431 +#: src/skit/cli.py:2419 msgid "Required" msgstr "必填" -#: src/skit/cli.py:2432 src/skit/cli.py:3731 src/skit/cli.py:3944 +#: src/skit/cli.py:2420 src/skit/cli.py:3758 src/skit/cli.py:3976 msgid "Default" msgstr "默认值" -#: src/skit/cli.py:2433 +#: src/skit/cli.py:2421 msgid "Choices" msgstr "可选值" -#: src/skit/cli.py:2434 src/skit/cli.py:3732 src/skit/cli.py:3945 +#: src/skit/cli.py:2422 src/skit/cli.py:3759 src/skit/cli.py:3977 msgid "Secret" msgstr "机密" -#: src/skit/cli.py:2435 src/skit/tui.py:340 src/skit/tui.py:486 +#: src/skit/cli.py:2423 src/skit/tui.py:343 src/skit/tui.py:493 msgid "Help" msgstr "帮助" -#: src/skit/cli.py:2440 src/skit/cli.py:3696 src/skit/cli.py:3704 -#: src/skit/cli.py:3714 src/skit/cli.py:3949 src/skit/cli.py:3954 +#: src/skit/cli.py:2428 src/skit/cli.py:3723 src/skit/cli.py:3731 +#: src/skit/cli.py:3741 src/skit/cli.py:3981 src/skit/cli.py:3986 msgid "•••" msgstr "•••" -#: src/skit/cli.py:2458 +#: src/skit/cli.py:2446 msgid "Show everything about one entry: metadata, dependencies, parameters, presets." msgstr "显示一个条目的全部信息:元数据、依赖、参数和参数组合。" -#: src/skit/cli.py:2459 +#: src/skit/cli.py:2447 msgid "Examples: skit show resize · skit show resize --json" msgstr "例:skit show resize · skit show resize --json" -#: src/skit/cli.py:2520 +#: src/skit/cli.py:2508 msgid "Remove a registered entry (an original source file is left untouched)." msgstr "移除一个已登记的条目(原始来源文件不受影响)。" -#: src/skit/cli.py:2523 src/skit/cli.py:3421 +#: src/skit/cli.py:2511 src/skit/cli.py:3424 src/skit/cli.py:3655 msgid "Skip confirmation" msgstr "跳过确认" -#: src/skit/cli.py:2533 src/skit/tui.py:149 +#: src/skit/cli.py:2512 src/skit/cli.py:3425 src/skit/cli.py:3656 +msgid "Never prompt" +msgstr "绝不提示" + +#: src/skit/cli.py:2523 +msgid "Confirmation is required; pass --yes to remove the entry." +msgstr "需要确认;请传 --yes 以移除条目。" + +#: src/skit/cli.py:2527 src/skit/tui.py:149 #, python-format msgid "Remove \"%(name)s\"?" msgstr "移除「%(name)s」?" -#: src/skit/cli.py:2535 +#: src/skit/cli.py:2529 #, python-format msgid "Remove \"%(name)s\"? Your original file will not be deleted." msgstr "移除「%(name)s」?你的原始文件不会被删除。" -#: src/skit/cli.py:2546 +#: src/skit/cli.py:2540 #, python-format msgid "Removed: %(name)s" msgstr "已移除:%(name)s" -#: src/skit/cli.py:2549 +#: src/skit/cli.py:2543 msgid "Rename an entry (presets, remembered values and history survive)." msgstr "重命名条目(参数组合、记住的值与历史都会保留)。" -#: src/skit/cli.py:2552 +#: src/skit/cli.py:2546 msgid "The new name" msgstr "新名称" -#: src/skit/cli.py:2561 +#: src/skit/cli.py:2555 #, python-format msgid "Renamed to %(name)s." msgstr "已重命名为 %(name)s。" -#: src/skit/cli.py:2565 +#: src/skit/cli.py:2559 msgid "Set an entry's description (shown in the Library and skit list)." msgstr "设置条目的说明(显示在工具库和 skit list 中)。" -#: src/skit/cli.py:2568 +#: src/skit/cli.py:2562 msgid "The description (empty text clears it)" msgstr "说明(留空即清除)" -#: src/skit/cli.py:2578 +#: src/skit/cli.py:2572 #, python-format msgid "Description updated for %(name)s." msgstr "已更新 %(name)s 的说明。" -#: src/skit/cli.py:2582 +#: src/skit/cli.py:2576 #, python-format msgid "Description cleared for %(name)s." msgstr "已清除 %(name)s 的说明。" -#: src/skit/cli.py:2590 +#: src/skit/cli.py:2584 #, python-format msgid "No editable entry named %(name)s." msgstr "找不到名为 %(name)s 的可编辑条目。" -#: src/skit/cli.py:2594 +#: src/skit/cli.py:2588 #, python-format msgid "No editable entry named \"%(name)s\". Create a script now?" msgstr "找不到名为“%(name)s”的可编辑条目。要现在创建脚本吗?" -#: src/skit/cli.py:2622 src/skit/cli.py:3812 +#: src/skit/cli.py:2616 src/skit/cli.py:3839 #, python-format msgid "Detected but not yet managed: %(names)s … and %(count)d more candidate (use --add to manage them)" msgid_plural "Detected but not yet managed: %(names)s … and %(count)d more candidates (use --add to manage them)" msgstr[0] "检测到但尚未管理:%(names)s……另有 %(count)d 个(用 --add 管理)" -#: src/skit/cli.py:2630 src/skit/cli.py:3820 +#: src/skit/cli.py:2624 src/skit/cli.py:3847 #, python-format msgid "Detected but not yet managed: %(names)s (use --add to manage them)" msgstr "检测到但尚未管理:%(names)s(用 --add 管理)" -#: src/skit/cli.py:2655 +#: src/skit/cli.py:2649 msgid "Manage which? (all / none / numbers like 1,3)" msgstr "要管理哪些?(all / none / 或编号如 1,3)" -#: src/skit/cli.py:2671 src/skit/tui.py:930 +#: src/skit/cli.py:2665 src/skit/tui.py:959 #, python-format msgid "Now managed: %(names)s" msgstr "已加入管理:%(names)s" -#: src/skit/cli.py:2677 +#: src/skit/cli.py:2671 msgid "Open a script or prompt source in your editor (offers to create a script if the name is new)." msgstr "在编辑器中打开脚本或提示词来源(名称不存在时可创建脚本)。" -#: src/skit/cli.py:2679 +#: src/skit/cli.py:2673 msgid "Example: skit edit resize" msgstr "例:skit edit resize" -#: src/skit/cli.py:2691 src/skit/tui.py:884 +#: src/skit/cli.py:2685 src/skit/tui.py:913 #, python-format msgid "%(name)s has no editable source (programs and command templates run as-is)." msgstr "%(name)s 没有可编辑的源码(程序和命令模板按原样运行)。" -#: src/skit/cli.py:2699 +#: src/skit/cli.py:2693 #, python-format msgid "%(name)s: the referenced source file is gone: %(path)s" msgstr "%(name)s:reference 原文件已消失:%(path)s" -#: src/skit/cli.py:2704 +#: src/skit/cli.py:2698 #, python-format msgid "Editing the original file (reference mode): %(path)s" msgstr "正在编辑原始文件(reference 模式):%(path)s" -#: src/skit/cli.py:2711 src/skit/cli.py:4484 src/skit/cli.py:4626 +#: src/skit/cli.py:2705 src/skit/cli.py:4520 src/skit/cli.py:4652 #, python-format msgid "%(name)s has no stored copy to edit." msgstr "%(name)s 没有可编辑的副本。" -#: src/skit/cli.py:2718 +#: src/skit/cli.py:2712 #, python-format msgid "Saved %(name)s." msgstr "已保存 %(name)s。" -#: src/skit/cli.py:2726 +#: src/skit/cli.py:2720 #, python-format msgid "skit reconciles parameter drift at run time; review managed parameters with: skit params %(name)s" msgstr "skit 会在运行时自动校对参数差异;可用 skit params %(name)s 查看管理中的参数" -#: src/skit/cli.py:2742 src/skit/cli.py:3666 +#: src/skit/cli.py:2736 src/skit/cli.py:3693 #, python-format msgid "Unknown preset \"%(preset)s\". Available: %(presets)s" msgstr "没有名为“%(preset)s”的参数组合。现有:%(presets)s" -#: src/skit/cli.py:2772 +#: src/skit/cli.py:2766 #, python-format msgid "Malformed --set (expected NAME=VALUE): %(items)s" msgstr "--set 格式错误(应为 NAME=VALUE):%(items)s" -#: src/skit/cli.py:2782 +#: src/skit/cli.py:2776 #, python-format msgid "Unknown parameter for --set: %(names)s. This entry's parameters: %(valid)s" msgstr "--set 指定了未知参数:%(names)s。此条目的参数:%(valid)s" -#: src/skit/cli.py:2833 +#: src/skit/cli.py:2827 #, python-format msgid "Parameters for %(name)s (press Enter to keep the value shown):" msgstr "%(name)s 的参数(按 Enter 沿用显示的值):" -#: src/skit/cli.py:2876 src/skit/cli.py:4378 +#: src/skit/cli.py:2870 src/skit/cli.py:4410 msgid "--runner only applies to prompt entries." msgstr "--runner 只适用于提示词条目。" -#: src/skit/cli.py:2896 src/skit/cli.py:3280 src/skit/cli.py:3312 +#: src/skit/cli.py:2890 src/skit/cli.py:3283 src/skit/cli.py:3315 #: src/skit/langs/launch.py:663 msgid "No agents are configured. Add one with: skit runner add mycli -- mycli run {{prompt}}" msgstr "尚未配置任何执行器。请添加一个:skit runner add mycli -- mycli run {{prompt}}" -#: src/skit/cli.py:2904 src/skit/langs/launch.py:669 +#: src/skit/cli.py:2898 src/skit/langs/launch.py:669 #, python-format msgid "No runner selected for %(name)s. Pass --runner NAME, or pin one with: skit params %(name)s --runner NAME" msgstr "%(name)s 尚未选择执行器。请带上 --runner NAME,或固定一个:skit params %(name)s --runner NAME" -#: src/skit/cli.py:2914 src/skit/cli.py:3106 src/skit/cli.py:4385 +#: src/skit/cli.py:2908 src/skit/cli.py:3100 src/skit/cli.py:4417 #: src/skit/langs/launch.py:678 #, python-format msgid "The runner %(runner)s isn't configured (known: %(names)s). Manage runners with: skit runner list" msgstr "执行器 %(runner)s 未配置(已知:%(names)s)。管理执行器:skit runner list" -#: src/skit/cli.py:2926 +#: src/skit/cli.py:2920 msgid "Run a registered entry in the terminal." msgstr "在终端中运行已登记的条目。" -#: src/skit/cli.py:2928 +#: src/skit/cli.py:2922 msgid "Examples: skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" msgstr "例:skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" -#: src/skit/cli.py:2933 +#: src/skit/cli.py:2927 msgid "Arguments passed through after --" msgstr "接在 -- 之后传入的参数" -#: src/skit/cli.py:2935 +#: src/skit/cli.py:2929 msgid "Never prompt; reuse last values and defaults" msgstr "从不询问;沿用上次的值 / 默认值" -#: src/skit/cli.py:2941 +#: src/skit/cli.py:2935 msgid "Named preset of parameter values to prefill the form with" msgstr "用哪个参数组合(preset)预填表单" -#: src/skit/cli.py:2948 +#: src/skit/cli.py:2942 #, python-brace-format msgid "Set a parameter value by name, as NAME=VALUE (repeatable; values may use tokens like {cwd} or {env:VAR}; the form no longer asks for a field you set)" msgstr "直接以 NAME=VALUE 指定参数值(可重复;值可用 {cwd}、{env:VAR} 等占位符;已指定的字段表单不再询问)" -#: src/skit/cli.py:2952 +#: src/skit/cli.py:2946 msgid "Save this run's values as a named preset" msgstr "把这次的值存成参数组合" -#: src/skit/cli.py:2955 +#: src/skit/cli.py:2949 msgid "Line-by-line prompts instead of the inline form" msgstr "用逐行问答取代迷你表单" -#: src/skit/cli.py:2961 +#: src/skit/cli.py:2955 msgid "Skip the parameter form and injection and run the script as-is (escape hatch)" msgstr "跳过参数表单与注入,原样运行脚本(逃生门)" -#: src/skit/cli.py:2968 +#: src/skit/cli.py:2962 msgid "Print the exact command that would run (tokens and globs expanded), then exit" msgstr "打印实际会运行的完整命令(token 与通配符已展开)后退出" -#: src/skit/cli.py:2974 +#: src/skit/cli.py:2968 msgid "Run a prompt entry with this agent (overrides its pin for one run)" msgstr "以此执行器运行提示词条目(仅本次运行,覆盖其固定设置)" -#: src/skit/cli.py:2981 +#: src/skit/cli.py:2975 msgid "Forget the remembered extra arguments before this run (they are otherwise reused when you pass none)" msgstr "在本次运行前忘掉记住的额外参数(未传入任何参数时,原本会沿用它们)" -#: src/skit/cli.py:2998 +#: src/skit/cli.py:2992 msgid "--raw runs the script as-is; --set, --preset, and --save-preset do not apply." msgstr "--raw 会原样运行脚本;--set、--preset、--save-preset 都不适用。" -#: src/skit/cli.py:3013 +#: src/skit/cli.py:3007 msgid "--raw doesn't apply to a prompt entry: its {{placeholders}} are the artifact itself — there is no as-is without them." msgstr "--raw 不适用于 prompt 条目:它的 {{placeholders}} 本身就是产物——没有它们就没有“原样”可言。" -#: src/skit/cli.py:3018 +#: src/skit/cli.py:3012 #, python-brace-format, python-format msgid "--raw doesn't apply to a %(kind)s entry: its {placeholders} are the artifact itself — there is no as-is without them." msgstr "--raw 不适用于 %(kind)s 条目:它的 {placeholders} 本身就是产物——没有它们就没有“原样”可言。" -#: src/skit/cli.py:3045 +#: src/skit/cli.py:3039 msgid "Raw mode: skipping the parameter form and injection." msgstr "raw 模式:已跳过参数表单与注入。" -#: src/skit/cli.py:3059 src/skit/cli.py:3562 src/skit/tui_form.py:935 +#: src/skit/cli.py:3053 src/skit/cli.py:3565 src/skit/tui_form.py:935 #, python-format msgid "%(name)s has no form fields, so there's nothing to save." msgstr "%(name)s 没有表单字段,没有东西可存。" -#: src/skit/cli.py:3139 +#: src/skit/cli.py:3140 #, python-format msgid "Reusing your last arguments: %(args)s" msgstr "沿用上次的参数:%(args)s" -#: src/skit/cli.py:3166 src/skit/cli.py:3617 +#: src/skit/cli.py:3167 src/skit/cli.py:3620 #, python-format msgid "Preset \"%(preset)s\" saved for %(name)s." msgstr "已为 %(name)s 保存参数组合“%(preset)s”。" -#: src/skit/cli.py:3227 +#: src/skit/cli.py:3230 #, python-format msgid "Run exited with code %(code)s" msgstr "本次运行以退出码 %(code)s 结束" -#: src/skit/cli.py:3237 +#: src/skit/cli.py:3240 msgid "Manage the agents (runners) that prompt entries run with." msgstr "管理提示词条目使用的执行器(AI agent)。" -#: src/skit/cli.py:3244 +#: src/skit/cli.py:3247 msgid "List the configured runners (seeds them into config on first use)." msgstr "列出已配置的执行器(首次使用时会写入配置文件)。" -#: src/skit/cli.py:3251 +#: src/skit/cli.py:3254 msgid "Include malformed raw rows and their repair indexes" msgstr "包含格式错误的原始行及其修复索引" -#: src/skit/cli.py:3288 +#: src/skit/cli.py:3291 msgid "Row" msgstr "行" -#: src/skit/cli.py:3289 src/skit/cli.py:3320 +#: src/skit/cli.py:3292 src/skit/cli.py:3323 msgid "Runner" msgstr "执行器" -#: src/skit/cli.py:3290 src/skit/cli.py:3321 src/skit/kindnames.py:29 +#: src/skit/cli.py:3293 src/skit/cli.py:3324 src/skit/kindnames.py:29 msgid "Command" msgstr "命令" -#: src/skit/cli.py:3291 +#: src/skit/cli.py:3294 msgid "Status" msgstr "状态" -#: src/skit/cli.py:3294 +#: src/skit/cli.py:3297 msgid "container" msgstr "容器" -#: src/skit/cli.py:3328 +#: src/skit/cli.py:3331 msgid "The built-in amp preset uses amp -x and runs the prompt once; it does not open an interactive session." msgstr "内置 amp 预设使用 amp -x,只运行提示词一次;它不会打开交互式会话。" -#: src/skit/cli.py:3345 +#: src/skit/cli.py:3348 msgid "A runner needs a command — e.g. skit runner add mycli mycli run {{prompt}}" msgstr "执行器需要一条命令——例如 skit runner add mycli mycli run {{prompt}}" -#: src/skit/cli.py:3348 +#: src/skit/cli.py:3351 msgid "A runner command must contain the {{prompt}} slot exactly once — that's where the rendered prompt lands." msgstr "执行器命令必须恰好包含一个 {{prompt}} 槽位——渲染后的提示词就放在那里。" -#: src/skit/cli.py:3352 src/skit/config.py:614 src/skit/tui_runner.py:42 +#: src/skit/cli.py:3355 src/skit/config.py:614 src/skit/tui_runner.py:42 msgid "{{prompt}} can't be the command itself — the first word must be the program to run." msgstr "{{prompt}} 不能是命令本身——第一个词必须是要运行的程序。" -#: src/skit/cli.py:3355 src/skit/config.py:617 src/skit/tui_runner.py:45 +#: src/skit/cli.py:3358 src/skit/config.py:617 src/skit/tui_runner.py:45 msgid "Runner commands take only the {{prompt}} slot — single-brace text is literal, and other {{holes}} aren't supported." msgstr "执行器命令只接受 {{prompt}} 槽位——单大括号是字面文字,其他 {{占位符}} 不支持。" -#: src/skit/cli.py:3364 +#: src/skit/cli.py:3367 msgid "Register a runner: skit runner add NAME COMMAND… ({{prompt}} marks where the rendered prompt goes; each shell word becomes one argument, no shell involved)" msgstr "注册执行器:skit runner add 名称 命令…({{prompt}} 标记渲染后提示词的位置;每个 shell 词就是一个参数,不经过 shell)" -#: src/skit/cli.py:3373 +#: src/skit/cli.py:3376 msgid "Runner name (e.g. claude)" msgstr "执行器名称(例如 claude)" -#: src/skit/cli.py:3375 +#: src/skit/cli.py:3378 msgid "The command, word by word, with one {{prompt}} slot" msgstr "命令,逐词给出,含一个 {{prompt}} 槽位" -#: src/skit/cli.py:3380 +#: src/skit/cli.py:3383 msgid "Replace the runner if the name already exists (the edit path)" msgstr "名称已存在时替换该执行器(编辑路径)" -#: src/skit/cli.py:3395 +#: src/skit/cli.py:3398 #, python-format msgid "The runner %(name)s already exists — pass --force to replace its command." msgstr "执行器 %(name)s 已存在——加上 --force 以替换其命令。" -#: src/skit/cli.py:3403 +#: src/skit/cli.py:3406 #, python-format msgid "Runner %(name)s updated: %(command)s" msgstr "已更新执行器 %(name)s:%(command)s" -#: src/skit/cli.py:3407 +#: src/skit/cli.py:3410 #, python-format msgid "Runner %(name)s added: %(command)s" msgstr "已添加执行器 %(name)s:%(command)s" -#: src/skit/cli.py:3411 +#: src/skit/cli.py:3414 msgid "Remove a configured runner." msgstr "移除已配置的执行器。" -#: src/skit/cli.py:3414 +#: src/skit/cli.py:3417 msgid "Runner name" msgstr "执行器名称" -#: src/skit/cli.py:3419 +#: src/skit/cli.py:3422 msgid "Remove one raw row index from 'runner list --all' (or 'container')" msgstr "移除“runner list --all”中的一个原始行索引(或“container”)" -#: src/skit/cli.py:3422 -msgid "Never prompt" -msgstr "绝不提示" - -#: src/skit/cli.py:3425 +#: src/skit/cli.py:3428 msgid "Pass exactly one runner name or --row INDEX." msgstr "请只传一个执行器名称或 --row INDEX。" -#: src/skit/cli.py:3445 src/skit/cli.py:3449 +#: src/skit/cli.py:3448 src/skit/cli.py:3452 msgid "--row must be a non-negative index or 'container'." msgstr "--row 必须是非负索引或“container”。" -#: src/skit/cli.py:3454 +#: src/skit/cli.py:3457 #, python-format msgid "Unknown runner row: %(row)s. Inspect with: skit runner list --all" msgstr "未知执行器行:%(row)s。请用 skit runner list --all 检查" -#: src/skit/cli.py:3462 +#: src/skit/cli.py:3465 #, python-format msgid "Runner row %(row)s is valid. Remove the agent by name instead: skit runner remove \"%(name)s\"" msgstr "执行器第 %(row)s 行有效。请改用名称移除代理:skit runner remove \"%(name)s\"" -#: src/skit/cli.py:3471 src/skit/tui_runner.py:315 +#: src/skit/cli.py:3474 src/skit/tui_runner.py:315 msgid "Remove the malformed prompt runner container?" msgstr "要移除格式错误的提示词执行器容器吗?" -#: src/skit/cli.py:3473 +#: src/skit/cli.py:3476 #, python-format msgid "Remove runner row %(row)d (\"%(name)s\")?" msgstr "要移除执行器行 %(row)d(“%(name)s”)吗?" -#: src/skit/cli.py:3486 src/skit/tui_runner.py:321 +#: src/skit/cli.py:3489 src/skit/tui_runner.py:321 #, python-format msgid "Remove the agent \"%(name)s\"?" msgstr "要移除 agent「%(name)s」吗?" -#: src/skit/cli.py:3496 src/skit/tui_runner.py:326 +#: src/skit/cli.py:3499 src/skit/tui_runner.py:326 #, python-format msgid "%(count)d prompt pins this runner and will need another runner before it can run again." msgid_plural "%(count)d prompts pin this runner and will need another runner before they can run again." msgstr[0] "有 %(count)d 个提示词固定使用此执行器;再次运行前需要选择另一个执行器。" -#: src/skit/cli.py:3505 +#: src/skit/cli.py:3508 msgid "Confirmation is required; pass --yes to remove the runner." msgstr "需要确认;请传 --yes 以移除执行器。" -#: src/skit/cli.py:3520 +#: src/skit/cli.py:3523 msgid "The runner row changed before it could be removed; inspect again." msgstr "执行器行在移除前已发生变化;请重新检查。" -#: src/skit/cli.py:3523 +#: src/skit/cli.py:3526 #, python-format msgid "Runner %(name)s removed." msgstr "已移除执行器 %(name)s。" -#: src/skit/cli.py:3526 +#: src/skit/cli.py:3529 msgid "Malformed prompt runner container removed." msgstr "已移除格式错误的提示词执行器容器。" -#: src/skit/cli.py:3529 +#: src/skit/cli.py:3532 #, python-format msgid "Malformed runner row %(row)d removed." msgstr "已移除格式错误的执行器第 %(row)d 行。" -#: src/skit/cli.py:3538 +#: src/skit/cli.py:3541 msgid "Manage named parameter presets for an entry." msgstr "管理条目的具名参数组合(preset)。" -#: src/skit/cli.py:3543 +#: src/skit/cli.py:3546 msgid "Save a set of parameter values as a named preset." msgstr "把一组参数值保存成参数组合(preset)。" -#: src/skit/cli.py:3546 src/skit/cli.py:3651 src/skit/tui_form.py:343 +#: src/skit/cli.py:3549 src/skit/cli.py:3654 src/skit/tui_form.py:343 msgid "Preset name" msgstr "组合名称" -#: src/skit/cli.py:3550 +#: src/skit/cli.py:3553 msgid "Save the last run's values without asking (automation-friendly)" msgstr "直接存上次运行的值,不再询问(适合自动化)" -#: src/skit/cli.py:3570 src/skit/cli.py:3586 +#: src/skit/cli.py:3573 src/skit/cli.py:3589 #, python-format msgid "%(name)s has no remembered values yet — run it once first." msgstr "%(name)s 还没有记住的值——先运行一次。" -#: src/skit/cli.py:3604 +#: src/skit/cli.py:3607 #, python-format msgid "Secret values are never stored in presets; skipped: %(names)s" msgstr "机密值不会存入参数组合;已跳过:%(names)s" -#: src/skit/cli.py:3621 +#: src/skit/cli.py:3624 msgid "List an entry's saved presets." msgstr "列出条目已保存的参数组合(preset)。" -#: src/skit/cli.py:3638 +#: src/skit/cli.py:3641 #, python-format msgid "No presets for %(name)s yet. Create one with: skit run %(name)s --save-preset " msgstr "%(name)s 还没有参数组合。创建一个:skit run %(name)s --save-preset <组合名>" -#: src/skit/cli.py:3648 +#: src/skit/cli.py:3651 msgid "Delete a named preset from an entry." msgstr "从条目删除一个具名参数组合(preset)。" -#: src/skit/cli.py:3660 +#: src/skit/cli.py:3673 +msgid "Confirmation is required; pass --yes to delete the preset." +msgstr "需要确认;请传 --yes 以删除参数组合。" + +#: src/skit/cli.py:3677 +#, python-format +msgid "Delete preset \"%(preset)s\" from %(name)s?" +msgstr "从 %(name)s 删除参数组合“%(preset)s”?" + +#: src/skit/cli.py:3683 #, python-format msgid "Preset \"%(preset)s\" deleted from %(name)s." msgstr "已从 %(name)s 删除参数组合“%(preset)s”。" -#: src/skit/cli.py:3715 +#: src/skit/cli.py:3742 #, python-format msgid "default %(value)s" msgstr "默认 %(value)s" -#: src/skit/cli.py:3717 +#: src/skit/cli.py:3744 msgid "optional" msgstr "选填" -#: src/skit/cli.py:3719 +#: src/skit/cli.py:3746 msgid "secret" msgstr "机密" -#: src/skit/cli.py:3729 +#: src/skit/cli.py:3756 msgid "Delivery" msgstr "传递方式" -#: src/skit/cli.py:3733 src/skit/cli.py:3946 +#: src/skit/cli.py:3760 src/skit/cli.py:3978 msgid "Last value" msgstr "上次的值" -#: src/skit/cli.py:3777 +#: src/skit/cli.py:3804 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written. Turn it on with: skit params %(name)s --interpolate" msgstr "变量插入已关闭——正文会原封不动送达 agent。开启:skit params %(name)s --interpolate" -#: src/skit/cli.py:3788 src/skit/cli.py:3917 src/skit/cli.py:3926 +#: src/skit/cli.py:3815 src/skit/cli.py:3949 src/skit/cli.py:3958 #, python-format msgid "%(name)s has no managed parameters." msgstr "%(name)s 没有管理的参数。" -#: src/skit/cli.py:3793 +#: src/skit/cli.py:3820 msgid "Prompt placeholders (the run form asks for them):" msgstr "提示词的占位符(运行表单会询问):" -#: src/skit/cli.py:3795 +#: src/skit/cli.py:3822 msgid "Command template placeholders (the run form asks for them):" msgstr "命令模板的占位符(运行表单会询问):" -#: src/skit/cli.py:3802 +#: src/skit/cli.py:3829 msgid "Declared environment variables (set on the run):" msgstr "声明的环境变量(运行时设置):" -#: src/skit/cli.py:3825 +#: src/skit/cli.py:3852 #, python-format msgid "No longer in the prompt (the value would be ignored): %(names)s — remove with --rm, or edit the body." msgstr "提示词中已不存在(其值会被忽略):%(names)s——用 --rm 移除,或编辑正文。" -#: src/skit/cli.py:3932 +#: src/skit/cli.py:3964 #, python-format msgid "%(name)s has no managed parameters. Use --manage to bring a detected candidate under management." msgstr "%(name)s 没有管理的参数。用 --manage 把检测到的候选纳入管理。" -#: src/skit/cli.py:3970 +#: src/skit/cli.py:4002 #, python-format msgid "Detected but not yet managed: %(names)s" msgstr "检测到但尚未管理:%(names)s" -#: src/skit/cli.py:3977 +#: src/skit/cli.py:4009 msgid "Reference mode: skit never writes the original file — manage parameters by editing its [tool.skit] block in the source directly." msgstr "参照模式:skit 绝不写入原始文件——请直接编辑源码中的 [tool.skit] 区块来管理参数。" -#: src/skit/cli.py:3985 +#: src/skit/cli.py:4017 #, python-format msgid "Detected but not yet managed: %(names)s (use --manage to manage them)" msgstr "检测到但尚未管理:%(names)s(用 --manage 管理)" -#: src/skit/cli.py:3990 +#: src/skit/cli.py:4022 #, python-brace-format, python-format msgid "This script locates itself ($0 / BASH_SOURCE). Injecting a constant runs it from a temporary copy, so it would see that copy path instead. Rewriting the constant as NAME=\"${NAME:-value}\" delivers the value through the environment with no copy at all — `skit params %(name)s --normalize NAME` does the rewrite for you on the stored copy." msgstr "这个脚本会读取自己的位置($0 / BASH_SOURCE)。注入常量时它会从临时副本运行,因此看到的是那个副本的路径。把常量改写成 NAME=\"${NAME:-value}\",值就会改由环境变量传递,完全不产生副本——`skit params %(name)s --normalize NAME` 会在存储的副本上帮你完成改写。" -#: src/skit/cli.py:4009 +#: src/skit/cli.py:4041 msgid "Show or edit an entry's managed or declared parameters." msgstr "查看或编辑条目的管理参数或声明参数。" -#: src/skit/cli.py:4011 +#: src/skit/cli.py:4043 msgid "Examples: skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" msgstr "例:skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" -#: src/skit/cli.py:4019 +#: src/skit/cli.py:4051 msgid "Prune definitions that no longer match the script and refresh changed types" msgstr "修剪已对不上脚本的定义,并更新类型已变的项" -#: src/skit/cli.py:4024 +#: src/skit/cli.py:4056 msgid "Bring a currently detected candidate under management (repeatable)" msgstr "把当前检测到的候选纳入管理(可重复)" -#: src/skit/cli.py:4027 +#: src/skit/cli.py:4059 msgid "Drop a managed parameter (repeatable)" msgstr "移除一个受管理的参数(可重复)" -#: src/skit/cli.py:4032 +#: src/skit/cli.py:4064 msgid "Declare a new parameter on an exe/command entry, by name (repeatable)" msgstr "为 exe/command 条目声明一个新参数,按名称指定(可重复)" -#: src/skit/cli.py:4035 +#: src/skit/cli.py:4067 msgid "Remove a declared parameter, by name (repeatable)" msgstr "移除一个声明的参数,按名称指定(可重复)" -#: src/skit/cli.py:4040 +#: src/skit/cli.py:4072 msgid "Set a declared parameter's type, as NAME=str|int|float|bool|choice|path" msgstr "设置声明参数的类型,格式 NAME=str|int|float|bool|choice|path" -#: src/skit/cli.py:4043 +#: src/skit/cli.py:4075 msgid "Set a declared parameter's default, as NAME=VALUE" msgstr "设置声明参数的默认值,格式 NAME=VALUE" -#: src/skit/cli.py:4048 +#: src/skit/cli.py:4080 msgid "Set a declared parameter's choices, as NAME=a,b,c (comma separated)" msgstr "设置声明参数的可选值,格式 NAME=a,b,c(逗号分隔)" -#: src/skit/cli.py:4054 +#: src/skit/cli.py:4086 msgid "Set how a declared parameter reaches the program, as NAME=env|flag|placeholder" msgstr "设置声明参数如何传递给程序,格式 NAME=env|flag|placeholder" -#: src/skit/cli.py:4060 +#: src/skit/cli.py:4092 msgid "Set a declared flag parameter's option, as NAME=--out (empty = positional)" msgstr "设置声明的 flag 参数选项,格式 NAME=--out(留空为位置参数)" -#: src/skit/cli.py:4063 +#: src/skit/cli.py:4095 msgid "Mark a declared parameter as required (repeatable)" msgstr "把某个声明参数标为必填(可重复)" -#: src/skit/cli.py:4066 +#: src/skit/cli.py:4098 msgid "Mark a declared parameter as optional (repeatable)" msgstr "把某个声明参数标为选填(可重复)" -#: src/skit/cli.py:4069 +#: src/skit/cli.py:4101 msgid "Set a declared parameter's help text, as NAME=text" msgstr "设置声明参数的帮助文字,格式 NAME=text" -#: src/skit/cli.py:4072 +#: src/skit/cli.py:4104 msgid "Mark a managed parameter as secret (repeatable)" msgstr "把某个受管参数标为机密(可重复)" -#: src/skit/cli.py:4077 +#: src/skit/cli.py:4109 msgid "Remove the secret mark from a managed parameter (repeatable)" msgstr "取消某个受管参数的机密标记(可重复)" -#: src/skit/cli.py:4080 +#: src/skit/cli.py:4112 msgid "Set a parameter's form prompt, as NAME=text (repeatable)" msgstr "以 NAME=text 设定参数的表单提示(可重复)" -#: src/skit/cli.py:4086 +#: src/skit/cli.py:4118 msgid "Read a secret parameter from an environment variable at run time, as NAME=ENVVAR (empty ENVVAR clears it; repeatable)" msgstr "运行时从环境变量读取机密参数,格式 NAME=ENVVAR(ENVVAR 留空表示清除;可重复)" -#: src/skit/cli.py:4093 +#: src/skit/cli.py:4125 #, python-brace-format msgid "Shell only: rewrite a constant into the ${NAME:-default} idiom in the stored copy, so its value is delivered as an environment variable instead of a rewritten temporary copy (repeatable)" msgstr "仅限 Shell:把常量改写成 ${NAME:-default} 写法(改的是仓库副本),之后它的值就用环境变量传递,不必再改写临时副本(可重复)" -#: src/skit/cli.py:4100 +#: src/skit/cli.py:4132 msgid "Prompt only: pin the agent this prompt runs with (empty value clears the pin)" msgstr "仅提示词:固定此提示词使用的执行器(空值清除固定)" -#: src/skit/cli.py:4108 +#: src/skit/cli.py:4140 msgid "Prompt only: turn variable insertion on/off for this prompt (off = the body travels exactly as written)" msgstr "仅提示词:开关此提示词的变量插入(关闭 = 正文原封不动送达)" -#: src/skit/cli.py:4116 +#: src/skit/cli.py:4148 msgid "Set where the entry runs: origin (its own folder), store, invoke (where you run skit from), or an absolute path" msgstr "设置条目的运行位置:origin(它自己的文件夹)、store、invoke(你运行 skit 的位置),或一个绝对路径" -#: src/skit/cli.py:4123 +#: src/skit/cli.py:4155 #, python-brace-format msgid "Command only: rewrite the template ({placeholders} are re-read from it)" msgstr "仅限 command:改写模板({placeholders} 会从中重新读取)" -#: src/skit/cli.py:4129 +#: src/skit/cli.py:4161 msgid "Pin the interpreter/runtime an interpreted entry runs with (e.g. zsh, bun; empty value returns to automatic)" msgstr "钉选解释型条目使用的解释器/运行时(例如 zsh、bun;留空即回到自动检测)" -#: src/skit/cli.py:4133 +#: src/skit/cli.py:4165 msgid "Output the read view as JSON" msgstr "读取视图以 JSON 输出" -#: src/skit/cli.py:4195 +#: src/skit/cli.py:4227 #, python-format msgid "%(op)s is its own operation — run it in a separate skit params call (nothing was changed)." msgstr "%(op)s 是独立的操作——请在另一次 skit params 调用中执行(本次未更改任何内容)。" -#: src/skit/cli.py:4265 +#: src/skit/cli.py:4297 #, python-format msgid "%(name)s manages its parameters from the script itself — use --manage / --unmanage, or edit the [tool.skit] block." msgstr "%(name)s 从脚本自身管理参数——请使用 --manage / --unmanage,或直接编辑 [tool.skit] 区块。" -#: src/skit/cli.py:4353 +#: src/skit/cli.py:4385 #, python-format msgid "Template updated. Placeholders: %(names)s" msgstr "模板已更新。占位符:%(names)s" -#: src/skit/cli.py:4357 +#: src/skit/cli.py:4389 #, python-format msgid "%(name)s now runs in: %(dir)s" msgstr "%(name)s 现在于此运行:%(dir)s" -#: src/skit/cli.py:4362 +#: src/skit/cli.py:4394 #, python-format msgid "%(name)s now runs with: %(program)s" msgstr "%(name)s 现在以此运行:%(program)s" -#: src/skit/cli.py:4366 +#: src/skit/cli.py:4398 #, python-format msgid "%(name)s is back to automatic interpreter detection." msgstr "%(name)s 已回到自动检测解释器。" -#: src/skit/cli.py:4397 +#: src/skit/cli.py:4429 #, python-format msgid "%(name)s now runs with %(runner)s." msgstr "%(name)s 现在以 %(runner)s 运行。" -#: src/skit/cli.py:4401 +#: src/skit/cli.py:4433 #, python-format msgid "Cleared the runner pin — %(name)s asks at run time." msgstr "已清除执行器固定——%(name)s 会在运行时询问。" -#: src/skit/cli.py:4410 +#: src/skit/cli.py:4442 msgid "--interpolate only applies to prompt entries." msgstr "--interpolate 只适用于提示词条目。" -#: src/skit/cli.py:4417 +#: src/skit/cli.py:4449 #, python-format msgid "Variable insertion is on — %(name)s fills its managed placeholders again." msgstr "变量插入已开启——%(name)s 会再次填入已管理的占位符。" -#: src/skit/cli.py:4421 +#: src/skit/cli.py:4453 #, python-format msgid "Variable insertion is off — %(name)s travels to the agent exactly as written." msgstr "变量插入已关闭——%(name)s 会原封不动送达 agent。" -#: src/skit/cli.py:4433 +#: src/skit/cli.py:4465 #, python-format msgid "%(name)s isn't a managed parameter; --env-source skipped." msgstr "%(name)s 不是管理的参数;--env-source 已略过。" -#: src/skit/cli.py:4440 src/skit/cli.py:4695 +#: src/skit/cli.py:4472 src/skit/cli.py:4723 #, python-format msgid "%(name)s isn't secret; --env-source only applies to secret parameters (mark it with --secret first)." msgstr "%(name)s 不是机密参数;--env-source 只适用于机密参数(先用 --secret 标记)。" -#: src/skit/cli.py:4468 +#: src/skit/cli.py:4499 #, python-format msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from." msgstr "%(name)s 没有管理的参数——它的种类没有分析器可以读取参数。" -#: src/skit/cli.py:4476 +#: src/skit/cli.py:4506 +#, python-format +msgid "Declare one instead: skit params %(name)s --add PARAM" +msgstr "改为手动声明一个:skit params %(name)s --add <参数名>" + +#: src/skit/cli.py:4512 #, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Edit the [tool.skit] block in the source directly." msgstr "%(name)s 是 reference 模式;skit 绝不写原文件。请直接改原文件的 [tool.skit] 区块。" -#: src/skit/cli.py:4497 +#: src/skit/cli.py:4533 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=text)." msgstr "已忽略格式错误的值:%(item)s(应为 NAME=text)。" -#: src/skit/cli.py:4536 src/skit/cli.py:4820 +#: src/skit/cli.py:4562 src/skit/cli.py:4848 #, python-format msgid "Removed previously stored plaintext value(s) for now-secret parameter(s): %(names)s" msgstr "已移除下列刚设为机密的参数先前以明文存储的值:%(names)s" -#: src/skit/cli.py:4543 +#: src/skit/cli.py:4569 #, python-format msgid "Updated %(name)s. Managed parameters: %(names)s" msgstr "已更新 %(name)s。受管理的参数:%(names)s" -#: src/skit/cli.py:4549 +#: src/skit/cli.py:4575 #, python-format msgid "The run form now asks for the managed parameters — the script's own command-line form (%(frameworks)s) is set aside until they are removed (--unmanage)." msgstr "运行表单现在会询问这些管理的参数——脚本自己的命令行表单(%(frameworks)s)会先搁置,直到它们被移除(--unmanage)为止。" -#: src/skit/cli.py:4565 +#: src/skit/cli.py:4591 #, python-format msgid "%(name)s isn't a plain constant with a literal value, so there's nothing to normalize; skipped." msgstr "%(name)s 不是带字面值的普通常量,没有可以规范化的东西;已跳过。" -#: src/skit/cli.py:4568 +#: src/skit/cli.py:4594 #, python-format msgid "%(name)s is assigned more than once at the top level; normalizing it would change which value wins. Skipped." msgstr "%(name)s 在顶层被赋值多次;规范化会改变最终生效的值。已跳过。" -#: src/skit/cli.py:4571 +#: src/skit/cli.py:4597 #, python-format msgid "%(name)s is readonly, so the script could never take a value from the environment; skipped." msgstr "%(name)s 是 readonly,脚本永远不可能从环境变量取值;已跳过。" -#: src/skit/cli.py:4573 +#: src/skit/cli.py:4599 #, python-format msgid "%(name)s already reads from the environment; nothing to do." msgstr "%(name)s 已经从环境变量读取了;无需处理。" -#: src/skit/cli.py:4575 +#: src/skit/cli.py:4601 #, python-format msgid "%(name)s's value contains a character that can't be moved into ${...:-...} safely (one of } \" ` $ \\ or a newline); skipped — it keeps being injected into a temporary copy." msgstr "%(name)s 的值里有无法安全放进 ${...:-...} 的字符(} \" ` $ \\ 或换行);已跳过——它会继续用临时副本注入。" -#: src/skit/cli.py:4579 +#: src/skit/cli.py:4605 msgid "Could not parse the script (syntax error); nothing was normalized." msgstr "无法解析脚本(语法错误);没有做任何规范化。" -#: src/skit/cli.py:4610 +#: src/skit/cli.py:4636 #, python-brace-format, python-format msgid "%(name)s has no --normalize: it is a shell idiom (VAR=value -> VAR=\"${VAR:-value}\")." msgstr "%(name)s 没有 --normalize:那是 shell 的写法(VAR=value -> VAR=\"${VAR:-value}\")。" -#: src/skit/cli.py:4618 +#: src/skit/cli.py:4644 #, python-brace-format, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Change the line to VAR=\"${VAR:-value}\" in the source directly." msgstr "%(name)s 是 reference 模式;skit 绝不写原文件。请直接把原文件里那行改成 VAR=\"${VAR:-value}\"。" -#: src/skit/cli.py:4637 +#: src/skit/cli.py:4663 #, python-format msgid "%(name)s isn't valid UTF-8, so --normalize can't rewrite it safely; nothing was changed — its constants keep being injected into a temporary copy." msgstr "%(name)s 不是有效的 UTF-8,--normalize 无法安全改写;什么都没改——它的常量会继续用临时副本注入。" -#: src/skit/cli.py:4674 +#: src/skit/cli.py:4702 #, python-format msgid "Normalized %(names)s in %(name)s: delivered as environment variables from now on (no temporary copy, and $0 stays your real file)." msgstr "已规范化 %(name)s 中的 %(names)s:今后用环境变量传值(不再写临时副本,$0 也仍指向你的真实文件)。" -#: src/skit/cli.py:4684 +#: src/skit/cli.py:4712 #, python-format msgid "%(name)s isn't a declared parameter; skipped." msgstr "%(name)s 不是已声明的参数,已跳过。" -#: src/skit/cli.py:4685 +#: src/skit/cli.py:4713 #, python-format msgid "%(name)s is already declared; skipped." msgstr "%(name)s 已经声明过,已跳过。" -#: src/skit/cli.py:4686 +#: src/skit/cli.py:4714 #, python-format msgid "%(name)s: that delivery isn't available for this kind; skipped." msgstr "%(name)s:该传递方式不适用于此类型,已跳过。" -#: src/skit/cli.py:4688 +#: src/skit/cli.py:4716 #, python-format msgid "%(name)s isn't a template placeholder, so it can't use placeholder delivery; skipped." msgstr "%(name)s 不是模板占位符,无法使用 placeholder 传递方式,已跳过。" -#: src/skit/cli.py:4691 +#: src/skit/cli.py:4719 #, python-format msgid "%(name)s: unknown type; skipped (use str, int, float, bool, choice, or path)." msgstr "%(name)s:未知类型,已跳过(可用 str、int、float、bool、choice 或 path)。" -#: src/skit/cli.py:4693 +#: src/skit/cli.py:4721 #, python-format msgid "%(name)s: the default doesn't fit its type; skipped." msgstr "%(name)s:默认值与其类型不符,已跳过。" -#: src/skit/cli.py:4698 +#: src/skit/cli.py:4726 #, python-format msgid "%(name)s: a choice parameter needs choices; set --choices %(name)s=a,b,c." msgstr "%(name)s:choice 参数需要可选值,请设置 --choices %(name)s=a,b,c。" -#: src/skit/cli.py:4701 +#: src/skit/cli.py:4729 #, python-format msgid "%(name)s is on by default, so its flag could only ever turn it on again. Declare the flag that turns it OFF instead (--no-%(name)s and the like), with default false." msgstr "%(name)s 默认就是开的,它的标志只会再开一次。请改成声明用来关掉它的那个标志(--no-%(name)s 之类),默认 false。" -#: src/skit/cli.py:4757 +#: src/skit/cli.py:4785 #, python-format msgid "Variable insertion is off for %(name)s — turn it on first with: skit params %(name)s --interpolate" msgstr "%(name)s 的变量插入已关闭——请先开启:skit params %(name)s --interpolate" -#: src/skit/cli.py:4771 +#: src/skit/cli.py:4799 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=VALUE)." msgstr "已忽略格式错误的值:%(item)s(应为 NAME=VALUE)。" -#: src/skit/cli.py:4827 +#: src/skit/cli.py:4855 #, python-format msgid "Updated %(name)s. Declared parameters: %(names)s" msgstr "已更新 %(name)s。已声明的参数:%(names)s" -#: src/skit/cli.py:4855 +#: src/skit/cli.py:4883 #, python-format msgid "Dependencies of %(name)s: %(deps)s" msgstr "%(name)s 的依赖:%(deps)s" -#: src/skit/cli.py:4864 +#: src/skit/cli.py:4892 #, python-format msgid "External commands needed by %(name)s: %(needs)s" msgstr "%(name)s 所需的外部命令:%(needs)s" -#: src/skit/cli.py:4871 +#: src/skit/cli.py:4899 msgid "View or update an entry's package dependencies, Python constraint, and needed commands." msgstr "查看或更新条目的包依赖、Python 版本约束和所需命令。" -#: src/skit/cli.py:4874 +#: src/skit/cli.py:4902 msgid "Examples: skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" msgstr "示例:skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" -#: src/skit/cli.py:4880 +#: src/skit/cli.py:4908 msgid "A dependency (repeat for more; replaces the whole list)" msgstr "一项依赖(可重复;整组取代原列表)" -#: src/skit/cli.py:4882 +#: src/skit/cli.py:4910 msgid "Remove every dependency" msgstr "清空所有依赖" -#: src/skit/cli.py:4890 +#: src/skit/cli.py:4918 msgid "An external command the entry needs on PATH (repeat; replaces the whole list)" msgstr "条目在 PATH 上所需的外部命令(可重复;替换整个列表)" -#: src/skit/cli.py:4894 +#: src/skit/cli.py:4922 msgid "Remove every needed external command" msgstr "清空所有所需的外部命令" -#: src/skit/cli.py:4914 +#: src/skit/cli.py:4942 #, python-format msgid "%(name)s doesn't take package dependencies — only --need applies here." msgstr "%(name)s 不接受包依赖——这里只能用 --need。" -#: src/skit/cli.py:4920 +#: src/skit/cli.py:4948 msgid "Use --dep to set the list or --clear to empty it — not both." msgstr "--dep 用来设置列表、--clear 用来清空——两者择一。" -#: src/skit/cli.py:4925 +#: src/skit/cli.py:4953 msgid "Use --need to set the list or --clear-needs to empty it — not both." msgstr "--need 用来设置列表、--clear-needs 用来清空——两者择一。" -#: src/skit/cli.py:4960 +#: src/skit/cli.py:4988 #, python-format msgid "Dependencies of %(name)s updated: %(deps)s" msgstr "%(name)s 的依赖已更新:%(deps)s" -#: src/skit/cli.py:4964 +#: src/skit/cli.py:4992 #, python-format msgid "Python constraint of %(name)s updated: %(value)s" msgstr "%(name)s 的 Python 版本约束已更新:%(value)s" -#: src/skit/cli.py:4974 +#: src/skit/cli.py:5002 #, python-format msgid "Needs of %(name)s updated: %(needs)s" msgstr "%(name)s 所需的命令已更新:%(needs)s" -#: src/skit/cli.py:4987 +#: src/skit/cli.py:5015 msgid "Connect skit to AI agents: install the official Agent Skill." msgstr "把 skit 接上 AI agent:安装官方 Agent Skill。" -#: src/skit/cli.py:5002 +#: src/skit/cli.py:5030 #, python-format msgid "Could not write the skill there: %(error)s" msgstr "无法把 skill 写到那里:%(error)s" -#: src/skit/cli.py:5005 src/skit/tui_prefs.py:195 +#: src/skit/cli.py:5033 src/skit/tui_prefs.py:196 #, python-format msgid "Installed the skit Agent Skill: %(path)s" msgstr "已安装 skit Agent Skill:%(path)s" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "user" msgstr "用户" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "project" msgstr "项目" -#: src/skit/cli.py:5012 +#: src/skit/cli.py:5040 msgid "Agent directories on this machine:" msgstr "这台机器上的 agent 目录:" -#: src/skit/cli.py:5016 +#: src/skit/cli.py:5044 msgid "Install where?" msgstr "要装到哪里?" -#: src/skit/cli.py:5023 +#: src/skit/cli.py:5051 #, python-format msgid "Write the skill into %(path)s?" msgstr "把 skill 写入 %(path)s?" -#: src/skit/cli.py:5033 +#: src/skit/cli.py:5061 msgid "Install skit's Agent Skill into an AI agent's skills directory." msgstr "把 skit 的 Agent Skill 安装到 AI agent 的 skills 目录。" -#: src/skit/cli.py:5035 +#: src/skit/cli.py:5063 msgid "Examples: skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" msgstr "例:skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" -#: src/skit/cli.py:5042 +#: src/skit/cli.py:5070 msgid "Where to install: claude, codex, or agents (the cross-agent ./.agents directory)" msgstr "安装目标:claude、codex,或 agents(跨 agent 的 ./.agents 目录)" -#: src/skit/cli.py:5048 +#: src/skit/cli.py:5076 msgid "Install into this skills directory instead of a named target" msgstr "安装到这个 skills 目录,而不是具名目标" -#: src/skit/cli.py:5054 +#: src/skit/cli.py:5082 msgid "Install into the current project (./.claude, ./.codex) instead of your home directory" msgstr "安装到当前项目(./.claude、./.codex),而不是主目录" -#: src/skit/cli.py:5063 +#: src/skit/cli.py:5091 msgid "Use a named target (with optional --project) or --to — not both." msgstr "具名目标(可搭配 --project)和 --to 二选一,不能同时。" -#: src/skit/cli.py:5075 +#: src/skit/cli.py:5103 #, python-format msgid "Unknown target %(name)s. Valid targets: claude, codex, agents." msgstr "没有 %(name)s 这个目标。可用目标:claude、codex、agents。" -#: src/skit/cli.py:5084 +#: src/skit/cli.py:5112 msgid "Nothing installed: name a target (claude, codex, agents) or pass --to DIR." msgstr "没有安装任何东西:请指定目标(claude、codex、agents)或改用 --to DIR。" -#: src/skit/cli.py:5091 +#: src/skit/cli.py:5119 msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Pass --to DIR to choose one yourself." msgstr "没有检测到 agent 目录(~/.claude、~/.codex、./.agents 等)。可用 --to DIR 自行指定。" -#: src/skit/cli.py:5097 +#: src/skit/cli.py:5125 msgid "Cancelled — nothing was written." msgstr "已取消——没有写入任何东西。" -#: src/skit/cli.py:5118 +#: src/skit/cli.py:5146 msgid "Check that uv is available and the entry library is intact." msgstr "检查 uv 是否可用以及工具库是否完好。" -#: src/skit/cli.py:5121 +#: src/skit/cli.py:5149 msgid "Rebuild the index from each entry's meta.toml" msgstr "从每个条目的 meta.toml 重建索引" -#: src/skit/cli.py:5137 src/skit/tui_health.py:172 +#: src/skit/cli.py:5165 src/skit/tui_health.py:172 #, python-format msgid "Index rebuilt: %(count)s entry" msgid_plural "Index rebuilt: %(count)s entries" msgstr[0] "索引已重建:%(count)s 条" -#: src/skit/cli.py:5183 src/skit/tui_health.py:62 +#: src/skit/cli.py:5211 src/skit/tui_health.py:62 #, python-format msgid "uv: %(path)s" msgstr "uv:%(path)s" -#: src/skit/cli.py:5186 src/skit/tui_health.py:65 +#: src/skit/cli.py:5214 src/skit/tui_health.py:65 msgid "uv: not found. Install it from https://docs.astral.sh/uv/getting-started/installation/" msgstr "uv:未找到。安装方式:https://docs.astral.sh/uv/getting-started/installation/" -#: src/skit/cli.py:5190 src/skit/tui_health.py:72 +#: src/skit/cli.py:5218 src/skit/tui_health.py:72 #, python-format msgid "%(count)s entry registered" msgid_plural "%(count)s entries registered" msgstr[0] "已登记 %(count)s 个条目" -#: src/skit/cli.py:5195 +#: src/skit/cli.py:5223 #, python-format msgid "%(name)s: the launch target is gone from disk" msgstr "%(name)s:运行目标已从磁盘消失" -#: src/skit/cli.py:5199 +#: src/skit/cli.py:5227 #, python-format msgid "%(name)s: form definitions are out of sync — run: skit params %(name)s --resync" msgstr "%(name)s:表单定义过期——运行:skit params %(name)s --resync" -#: src/skit/cli.py:5203 +#: src/skit/cli.py:5231 #, python-format msgid "%(name)s: missing external command(s): %(tools)s" msgstr "%(name)s:缺少外部命令:%(tools)s" -#: src/skit/cli.py:5209 +#: src/skit/cli.py:5237 #, python-format msgid "%(name)s: a run would refuse to start — %(reason)s" msgstr "%(name)s:运行会拒绝启动——%(reason)s" -#: src/skit/cli.py:5213 +#: src/skit/cli.py:5241 #, python-format msgid "Ignored malformed runner row(s) in config: %(rows)s. Inspect and repair with: skit runner list --all" msgstr "已忽略配置中格式错误的执行器行:%(rows)s。检查并修复:skit runner list --all" -#: src/skit/cli.py:5217 src/skit/tui_health.py:134 +#: src/skit/cli.py:5245 src/skit/tui_health.py:134 #, python-format msgid "Library: %(path)s (%(count)s · %(size)s)" msgstr "工具库:%(path)s(%(count)s 个 · %(size)s)" -#: src/skit/cli.py:5235 +#: src/skit/cli.py:5263 #, python-format msgid "auto (%(locale)s)" msgstr "自动(%(locale)s)" -#: src/skit/cli.py:5272 +#: src/skit/cli.py:5300 msgid "default ($VISUAL / $EDITOR)" msgstr "默认($VISUAL / $EDITOR)" -#: src/skit/cli.py:5288 +#: src/skit/cli.py:5316 msgid "auto (bash on PATH)" msgstr "自动(使用 PATH 上的 bash)" -#: src/skit/cli.py:5292 +#: src/skit/cli.py:5320 msgid "auto (deno > bun > node)" msgstr "自动(deno > bun > node)" -#: src/skit/cli.py:5327 +#: src/skit/cli.py:5355 msgid "Mirrors are switched off — run `skit config mirror on` to activate them." msgstr "镜像当前处于关闭状态——运行 `skit config mirror on` 启用。" -#: src/skit/cli.py:5340 +#: src/skit/cli.py:5368 msgid "Nothing to enable: no mirror URLs are saved. Set an axis first: mirror.pypi / mirror.github / mirror.npm." msgstr "没有可启用的镜像:尚未保存任何镜像 URL。请先设置某个轴:mirror.pypi / mirror.github / mirror.npm。" -#: src/skit/cli.py:5345 +#: src/skit/cli.py:5373 #, python-format msgid "Unknown mirror value: %(name)s. \"mirror\" is the master switch (on / off); mirrors are picked per ecosystem: mirror.pypi (%(pypi)s), mirror.github (%(github)s), mirror.npm (%(npm)s) — each also takes a URL or \"off\"." msgstr "未知的 mirror 值:%(name)s。“mirror”只是总开关(on / off);镜像按生态各自挑选:mirror.pypi(%(pypi)s)、mirror.github(%(github)s)、mirror.npm(%(npm)s)——每项也接受 URL 或“off”。" -#: src/skit/cli.py:5361 +#: src/skit/cli.py:5389 #, python-format msgid "Unknown %(key)s value: %(name)s. Choose from: %(names)s, off — or give a full URL." msgstr "未知的 %(key)s 值:%(name)s。可选:%(names)s、off——或直接给出完整 URL。" -#: src/skit/cli.py:5385 +#: src/skit/cli.py:5413 #, python-format msgid "Unknown mirror.github value: %(name)s. Choose from: %(names)s, off — or give an https:// github-release base URL (the uv binary is downloaded and executed, so https:// is required)." msgstr "未知的 mirror.github 值:%(name)s。可选:%(names)s、off——或给出一个 https:// 的 github-release 基底 URL(uv 主程序会被下载并执行,因此必须是 https://)。" -#: src/skit/cli.py:5402 +#: src/skit/cli.py:5430 #, python-format msgid "Unknown language: %(tag)s. Available: %(locales)s" msgstr "未知语言:%(tag)s。可用语言:%(locales)s" -#: src/skit/cli.py:5419 +#: src/skit/cli.py:5447 #, python-format msgid "Unknown form style: %(value)s. Choose from: tui, plain" msgstr "未知的表单形态:%(value)s。可选:tui、plain" -#: src/skit/cli.py:5429 src/skit/tui_prefs.py:482 +#: src/skit/cli.py:5457 src/skit/tui_prefs.py:483 #, python-format msgid "No such file: %(path)s" msgstr "找不到文件:%(path)s" -#: src/skit/cli.py:5436 +#: src/skit/cli.py:5464 #, python-format msgid "Unknown JS runner: %(value)s. Choose from: %(names)s" msgstr "未知的 JS 运行时:%(value)s。可选:%(names)s" -#: src/skit/cli.py:5443 +#: src/skit/cli.py:5471 #, python-format msgid "Unknown after-run behavior: %(value)s. Choose from: exit, stay" msgstr "未知的运行后行为:%(value)s。可选:exit、stay" -#: src/skit/cli.py:5451 +#: src/skit/cli.py:5479 msgid "Read or set skit's settings (language, editor, mirror, form style, after-run)." msgstr "读取或设置 skit 的设置(语言、编辑器、镜像、表单形态、运行后行为)。" -#: src/skit/cli.py:5453 +#: src/skit/cli.py:5481 msgid "Examples: skit config · skit config lang zh-TW · skit config mirror.pypi tsinghua" msgstr "例:skit config · skit config lang zh-CN · skit config mirror.pypi tsinghua" -#: src/skit/cli.py:5460 +#: src/skit/cli.py:5488 msgid "Setting name: lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" msgstr "设置项:lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" -#: src/skit/cli.py:5465 +#: src/skit/cli.py:5493 msgid "New value (omit to read; lang also accepts \"auto\")" msgstr "新值(省略表示读取;lang 也接受 \"auto\")" -#: src/skit/cli.py:5482 +#: src/skit/cli.py:5510 #, python-format msgid "Unknown setting: %(key)s. Available: %(keys)s" msgstr "未知的设置:%(key)s。可用:%(keys)s" -#: src/skit/cli.py:5514 src/skit/tui_prefs.py:412 src/skit/tui_prefs.py:452 +#: src/skit/cli.py:5542 src/skit/tui_prefs.py:413 src/skit/tui_prefs.py:453 msgid "A custom choice needs a URL." msgstr "选了 custom 就需要填 URL。" -#: src/skit/cli.py:5521 src/skit/tui_prefs.py:347 +#: src/skit/cli.py:5549 src/skit/tui_prefs.py:348 msgid "github-release mirror base URL" msgstr "github-release 镜像基底 URL" -#: src/skit/cli.py:5527 src/skit/tui_prefs.py:457 +#: src/skit/cli.py:5555 src/skit/tui_prefs.py:458 #, python-format msgid "The uv binary is downloaded and executed, so the github-release base URL must use https:// (got: %(url)s)." msgstr "uv 主程序会被下载并执行,因此 github-release 基底 URL 必须使用 https://(当前为:%(url)s)。" -#: src/skit/cli.py:5546 src/skit/tui_prefs.py:336 +#: src/skit/cli.py:5574 src/skit/tui_prefs.py:337 msgid "PyPI index (Python packages)" msgstr "PyPI 索引(Python 包)" -#: src/skit/cli.py:5548 src/skit/tui_prefs.py:340 +#: src/skit/cli.py:5576 src/skit/tui_prefs.py:341 msgid "PyPI index URL" msgstr "PyPI 索引 URL" -#: src/skit/cli.py:5552 src/skit/tui_prefs.py:341 +#: src/skit/cli.py:5580 src/skit/tui_prefs.py:342 msgid "GitHub releases (Python builds, the uv binary)" msgstr "GitHub 发布(Python 本体、uv 主程序)" -#: src/skit/cli.py:5561 src/skit/tui_prefs.py:350 +#: src/skit/cli.py:5589 src/skit/tui_prefs.py:351 msgid "npm registry (JS/TS packages)" msgstr "npm registry(JS/TS 包)" -#: src/skit/cli.py:5563 src/skit/tui_prefs.py:354 +#: src/skit/cli.py:5591 src/skit/tui_prefs.py:355 msgid "npm registry URL" msgstr "npm registry URL" -#: src/skit/cli.py:5579 +#: src/skit/cli.py:5607 msgid "Network to PyPI / GitHub looks slow or blocked." msgstr "检测到访问 PyPI / GitHub 缓慢或受阻。" -#: src/skit/cli.py:5581 +#: src/skit/cli.py:5609 msgid "Configure mirrors for faster installs (mainland China)?" msgstr "是否配置镜像以加速下载(中国大陆)?" @@ -2018,59 +2036,59 @@ msgstr "%(name)s 需要%(type)s——你输入的是 %(value)r。" msgid "%(name)s must be one of: %(choices)s" msgstr "%(name)s 必须是其中之一:%(choices)s" -#: src/skit/flows.py:709 +#: src/skit/flows.py:712 #, python-format msgid "%(name)s reads from the environment variable %(env)s, but it isn't set." msgstr "%(name)s 要从环境变量 %(env)s 读取,但它尚未设置。" -#: src/skit/flows.py:906 +#: src/skit/flows.py:915 #, python-format msgid "→ inject: %(pairs)s" msgstr "→ 注入:%(pairs)s" -#: src/skit/flows.py:909 +#: src/skit/flows.py:918 msgid " (written to a temporary copy, deleted after the run; your original file is untouched)" msgstr " (写进临时副本,跑完即删;你的原始文件不变)" -#: src/skit/flows.py:969 +#: src/skit/flows.py:978 msgid "Secret-marked values are never saved by skit, but this prompt sends them to the selected agent as plaintext; the agent may log or sync them." msgstr "标记为机密的值不会由 skit 保存,但此提示词会以明文将它们发送给所选 agent;agent 可能记录或同步这些值。" -#: src/skit/flows.py:980 +#: src/skit/flows.py:989 #, python-format msgid "%(name)s can't contain a line break: a shell `read` takes ONE line, so everything after the break would be thrown away." msgstr "%(name)s 不能包含换行:shell 的 `read` 只读取一行,换行之后的内容会被丢弃。" -#: src/skit/flows.py:984 +#: src/skit/flows.py:993 #, python-format msgid "%(name)s is read on the same line as other values, so its value can't contain spaces or tabs — the shell would split it across the other fields. Only the LAST value on a `read` line may contain spaces." msgstr "%(name)s 与其他值读在同一行,所以它的值不能包含空格或制表符——shell 会把它拆散到其他字段里。一行 `read` 中只有最后一个值可以包含空格。" -#: src/skit/flows.py:989 +#: src/skit/flows.py:998 #, python-format msgid "%(name)s starts or ends with a space or tab, which a shell `read` strips off the line — the script would receive it trimmed. Remove the surrounding whitespace." msgstr "%(name)s 以空格或制表符开头或结尾,而 shell 的 `read` 会把行首尾的空白剥掉——脚本收到的会是被修剪过的值。请去掉两端的空白。" -#: src/skit/flows.py:1051 +#: src/skit/flows.py:1060 msgid "The built-in amp runner is one-shot: amp -x runs this prompt once and does not open an interactive session." msgstr "内置 amp 执行器为单次运行模式:amp -x 只运行此提示词一次,不会打开交互式会话。" -#: src/skit/flows.py:1098 +#: src/skit/flows.py:1107 #, python-format msgid "%(value)s isn't a valid %(type)s for %(param)s." msgstr "%(value)s 不是 %(param)s 的有效 %(type)s 值。" -#: src/skit/flows.py:1110 +#: src/skit/flows.py:1119 #, python-format msgid "%(empty)s is empty, but %(filled)s is filled and they are read on the same line — a shell `read` would hand your value to %(empty)s. Fill %(empty)s in, or clear %(filled)s." msgstr "%(empty)s 是空的,但 %(filled)s 有值,而它们由同一行 read 读取——shell 的 `read` 会把你的值给到 %(empty)s。请填上 %(empty)s,或清空 %(filled)s。" -#: src/skit/flows.py:1124 +#: src/skit/flows.py:1133 #, python-format msgid "skit refused to run its own injected copy: %(detail)s" msgstr "skit 拒绝运行自己注入出来的副本:%(detail)s" -#: src/skit/flows.py:1132 +#: src/skit/flows.py:1141 #, python-format msgid "The script and its form definitions don't match anymore: %(detail)s. Run `skit params %(name)s --resync` to fix it." msgstr "脚本内容和表单定义对不上了:%(detail)s。运行 `skit params %(name)s --resync` 即可修复。" @@ -2350,7 +2368,7 @@ msgstr "%(name)s 没有原始文件——origin 不适用于它的种类。" msgid "%(name)s has no stored copy — store doesn't apply to its kind." msgstr "%(name)s 没有存储的副本——store 不适用于它的种类。" -#: src/skit/store.py:551 src/skit/tui_settings.py:519 +#: src/skit/store.py:551 src/skit/tui_settings.py:514 msgid "The working directory must be origin, store, invoke, or an absolute path." msgstr "工作目录必须是 origin、store、invoke,或一个绝对路径。" @@ -2364,7 +2382,7 @@ msgstr "%(name)s 不是通过可钉选的解释器运行。" msgid "%(name)s isn't a command entry." msgstr "%(name)s 不是 command 条目。" -#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:947 +#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:942 msgid "Command template must not be empty" msgstr "命令模板不可为空" @@ -2398,22 +2416,22 @@ msgstr "%(name)s 是 reference 模式条目:它从自己的项目运行,包 msgid "%(name)s's stored copy isn't valid UTF-8, so skit can't rewrite the script's own dependency block — and that block is what uv reads. Edit it in the script itself: skit edit %(name)s" msgstr "%(name)s 存储的副本不是合法的 UTF-8,skit 没办法改写脚本自己的依赖区块——而 uv 读的正是那个区块。请直接在脚本里改:skit edit %(name)s" -#: src/skit/store.py:1150 src/skit/tui_settings.py:1013 +#: src/skit/store.py:1148 src/skit/tui_settings.py:1008 #, python-format msgid "The name %(name)s is already taken." msgstr "名称 %(name)s 已被使用。" -#: src/skit/store.py:1192 +#: src/skit/store.py:1190 #, python-format msgid "%(slug)s: meta.toml is missing; skipped" msgstr "%(slug)s:缺 meta.toml,已跳过" -#: src/skit/store.py:1198 +#: src/skit/store.py:1196 #, python-format msgid "%(slug)s: meta.toml is corrupt (%(error)s); skipped" msgstr "%(slug)s:meta.toml 损坏(%(error)s),已跳过" -#: src/skit/store.py:1204 +#: src/skit/store.py:1202 #, python-format msgid "%(slug)s: the referenced source file is gone: %(path)s" msgstr "%(slug)s:reference 原文件已消失:%(path)s" @@ -2427,7 +2445,7 @@ msgstr "环境变量 %(name)s 尚未设置(%(token)s 需要它)。" msgid "Runner picked on the run form" msgstr "执行器在运行表单上选择" -#: src/skit/tui.py:81 src/skit/tui_settings.py:556 src/skit/tui_settings.py:579 +#: src/skit/tui.py:81 src/skit/tui_settings.py:551 src/skit/tui_settings.py:574 #, python-format msgid "%(runner)s (no longer configured)" msgstr "%(runner)s(已不在配置中)" @@ -2457,14 +2475,14 @@ msgid "%(days)s d ago" msgstr "%(days)s 天前" #: src/skit/tui.py:125 src/skit/tui.py:162 src/skit/tui.py:204 -#: src/skit/tui.py:330 src/skit/tui.py:476 src/skit/tui_runner.py:210 +#: src/skit/tui.py:333 src/skit/tui.py:483 src/skit/tui_runner.py:210 #: src/skit/tui_runner.py:262 src/skit/tui_runner.py:287 #: src/skit/tui_runner.py:334 msgid "Remove" msgstr "移除" -#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:157 -#: src/skit/tui_add.py:181 src/skit/tui_runner.py:288 +#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:158 +#: src/skit/tui_add.py:182 src/skit/tui_runner.py:288 #: src/skit/tui_runner.py:335 msgid "Keep" msgstr "保留" @@ -2477,8 +2495,8 @@ msgstr "你的原始文件不会被删除。" msgid "Close" msgstr "关闭" -#: src/skit/tui.py:199 src/skit/tui.py:333 src/skit/tui.py:462 -#: src/skit/tui.py:471 src/skit/tui_form.py:554 src/skit/tui_form.py:555 +#: src/skit/tui.py:199 src/skit/tui.py:336 src/skit/tui.py:469 +#: src/skit/tui.py:478 src/skit/tui_form.py:554 src/skit/tui_form.py:555 #: src/skit/tui_form.py:723 msgid "Run" msgstr "运行" @@ -2487,202 +2505,202 @@ msgstr "运行" msgid "Rerun with last values" msgstr "用上次的值重跑" -#: src/skit/tui.py:201 src/skit/tui.py:335 src/skit/tui.py:474 +#: src/skit/tui.py:201 src/skit/tui.py:338 src/skit/tui.py:481 msgid "Entry settings" msgstr "条目设置" -#: src/skit/tui.py:202 src/skit/tui.py:336 src/skit/tui.py:479 -#: src/skit/tui_settings.py:801 +#: src/skit/tui.py:202 src/skit/tui.py:339 src/skit/tui.py:486 +#: src/skit/tui_settings.py:796 msgid "Presets" msgstr "参数组合" -#: src/skit/tui.py:203 src/skit/tui.py:331 src/skit/tui.py:332 -#: src/skit/tui.py:475 +#: src/skit/tui.py:203 src/skit/tui.py:334 src/skit/tui.py:335 +#: src/skit/tui.py:482 msgid "Edit source" msgstr "编辑来源" -#: src/skit/tui.py:205 src/skit/tui.py:337 src/skit/tui.py:478 +#: src/skit/tui.py:205 src/skit/tui.py:340 src/skit/tui.py:485 msgid "Add entry" msgstr "添加条目" -#: src/skit/tui.py:206 src/skit/tui.py:341 src/skit/tui.py:480 +#: src/skit/tui.py:206 src/skit/tui.py:344 src/skit/tui.py:487 msgid "Search" msgstr "搜索" -#: src/skit/tui.py:207 src/skit/tui.py:345 src/skit/tui.py:372 -#: src/skit/tui.py:483 src/skit/tui.py:1063 +#: src/skit/tui.py:207 src/skit/tui.py:348 src/skit/tui.py:379 +#: src/skit/tui.py:490 src/skit/tui.py:1092 msgid "Detail pane" msgstr "详情栏" -#: src/skit/tui.py:208 src/skit/tui.py:338 src/skit/tui.py:484 -#: src/skit/tui_prefs.py:155 +#: src/skit/tui.py:208 src/skit/tui.py:341 src/skit/tui.py:491 +#: src/skit/tui_prefs.py:156 msgid "Preferences" msgstr "偏好设置" -#: src/skit/tui.py:209 src/skit/tui.py:339 src/skit/tui.py:485 +#: src/skit/tui.py:209 src/skit/tui.py:342 src/skit/tui.py:492 #: src/skit/tui_health.py:55 msgid "Health check" msgstr "健康检查" -#: src/skit/tui.py:210 src/skit/tui.py:322 src/skit/tui.py:323 +#: src/skit/tui.py:210 src/skit/tui.py:325 src/skit/tui.py:326 msgid "Quit" msgstr "退出" -#: src/skit/tui.py:256 src/skit/tui.py:371 src/skit/tui.py:1056 -#: src/skit/tui.py:1062 +#: src/skit/tui.py:259 src/skit/tui.py:378 src/skit/tui.py:1085 +#: src/skit/tui.py:1091 msgid "Library" msgstr "工具库" -#: src/skit/tui.py:334 src/skit/tui.py:473 +#: src/skit/tui.py:337 src/skit/tui.py:480 msgid "Rerun" msgstr "重跑" -#: src/skit/tui.py:381 src/skit/tui.py:1058 +#: src/skit/tui.py:388 src/skit/tui.py:1087 msgid "/ to search names and descriptions…" msgstr "/ 搜索名称或说明…" -#: src/skit/tui.py:436 src/skit/tui.py:519 +#: src/skit/tui.py:443 src/skit/tui.py:534 msgid "Your entries will appear here." msgstr "你的条目会显示在这里。" -#: src/skit/tui.py:439 +#: src/skit/tui.py:446 #, python-format msgid "%(shown)s/%(total)s entry" msgid_plural "%(shown)s/%(total)s entries" msgstr[0] "%(shown)s/%(total)s 个条目" -#: src/skit/tui.py:463 +#: src/skit/tui.py:470 msgid "Back to list" msgstr "返回列表" -#: src/skit/tui.py:521 +#: src/skit/tui.py:536 msgid "Press a to add the first one," msgstr "按 a 添加第一个," -#: src/skit/tui.py:522 +#: src/skit/tui.py:537 msgid "or run: skit add in a terminal." msgstr "或在终端运行 skit add <路径>。" -#: src/skit/tui.py:538 +#: src/skit/tui.py:553 msgid "The copy is kept by skit; your original file is never modified." msgstr "副本由 skit 保管;你的原始文件永远不会被改动。" -#: src/skit/tui.py:542 src/skit/tui_settings.py:436 +#: src/skit/tui.py:557 src/skit/tui_settings.py:431 #, python-format msgid "Linked to the original: %(path)s" msgstr "链接原文件:%(path)s" -#: src/skit/tui.py:552 +#: src/skit/tui.py:567 msgid "(no description — add one in Entry settings)" msgstr "(没有说明——可在条目设置中添加)" -#: src/skit/tui.py:571 +#: src/skit/tui.py:586 #, python-format msgid "Parameters %(summary)s" msgstr "参数 %(summary)s" -#: src/skit/tui.py:574 +#: src/skit/tui.py:589 #, python-format msgid "Presets %(names)s" msgstr "参数组合 %(names)s" -#: src/skit/tui.py:583 +#: src/skit/tui.py:598 #, python-format msgid "Depends on %(deps)s" msgstr "依赖 %(deps)s" -#: src/skit/tui.py:589 +#: src/skit/tui.py:604 msgid "finished" msgstr "完成" -#: src/skit/tui.py:591 +#: src/skit/tui.py:606 #, python-format msgid "failed (code %(code)s)" msgstr "失败(代码 %(code)s)" -#: src/skit/tui.py:594 +#: src/skit/tui.py:609 #, python-format msgid "Last run %(when)s · %(outcome)s" msgstr "上次运行 %(when)s · %(outcome)s" -#: src/skit/tui.py:598 +#: src/skit/tui.py:613 msgid "Not run yet" msgstr "还没运行过" -#: src/skit/tui.py:604 +#: src/skit/tui.py:619 msgid "The script changed — skit checks the form against it before every run." msgstr "脚本改过了——每次运行前 skit 会自动核对表单。" -#: src/skit/tui.py:692 +#: src/skit/tui.py:707 msgid "Press Ctrl+C again to quit" msgstr "再按一次 Ctrl+C 退出" -#: src/skit/tui.py:712 src/skit/tui.py:743 src/skit/tui.py:755 -#: src/skit/tui.py:793 src/skit/tui.py:820 src/skit/tui.py:844 -#: src/skit/tui.py:898 src/skit/tui.py:963 src/skit/tui.py:1087 +#: src/skit/tui.py:727 src/skit/tui.py:758 src/skit/tui.py:770 +#: src/skit/tui.py:808 src/skit/tui.py:843 src/skit/tui.py:871 +#: src/skit/tui.py:927 src/skit/tui.py:992 src/skit/tui.py:1116 #, python-format msgid "Error: %(error)s" msgstr "错误:%(error)s" -#: src/skit/tui.py:723 +#: src/skit/tui.py:738 msgid "A prompt needs a configured agent to run with." msgstr "提示词需要一个已配置的 agent 才能运行。" -#: src/skit/tui.py:744 +#: src/skit/tui.py:759 msgid "The runner is no longer configured." msgstr "该执行器已不在配置中。" -#: src/skit/tui.py:781 +#: src/skit/tui.py:796 #, python-format msgid "%(name)s hasn't run yet — press Enter to fill the form first." msgstr "%(name)s 还没运行过——先按 Enter 填一次表单。" -#: src/skit/tui.py:829 src/skit/tui.py:1070 src/skit/tui_form.py:653 +#: src/skit/tui.py:856 src/skit/tui.py:1099 src/skit/tui_form.py:653 #, python-format msgid "Run %(name)s" msgstr "运行 %(name)s" -#: src/skit/tui.py:852 +#: src/skit/tui.py:879 #, python-format msgid "Last: %(name)s ✗ couldn't launch" msgstr "上次:%(name)s ✗ 无法启动" -#: src/skit/tui.py:858 +#: src/skit/tui.py:887 #, python-format msgid "Last: %(name)s ✓ finished" msgstr "上次:%(name)s ✓ 完成" -#: src/skit/tui.py:860 +#: src/skit/tui.py:889 #, python-format msgid "Last: %(name)s ✗ failed (code %(code)s)" msgstr "上次:%(name)s ✗ 失败(代码 %(code)s)" -#: src/skit/tui.py:867 +#: src/skit/tui.py:896 msgid "✓ finished" msgstr "✓ 完成" -#: src/skit/tui.py:869 +#: src/skit/tui.py:898 #, python-format msgid "✗ failed (code %(code)s)" msgstr "✗ 失败(代码 %(code)s)" -#: src/skit/tui.py:870 +#: src/skit/tui.py:899 msgid "✗ couldn't launch" msgstr "✗ 无法启动" -#: src/skit/tui.py:902 src/skit/tui.py:922 +#: src/skit/tui.py:931 src/skit/tui.py:951 #, python-format msgid "Edited %(name)s." msgstr "已编辑 %(name)s。" -#: src/skit/tui.py:974 +#: src/skit/tui.py:1003 msgid "✓ added" msgstr "✓ 已添加" -#: src/skit/tui_add.py:70 src/skit/tui_add.py:139 src/skit/tui_add.py:197 -#: src/skit/tui_add.py:320 src/skit/tui_add.py:601 src/skit/tui_add.py:654 -#: src/skit/tui_add.py:680 src/skit/tui_add.py:900 src/skit/tui_add.py:1207 -#: src/skit/tui_add.py:1364 src/skit/tui_form.py:326 src/skit/tui_form.py:348 +#: src/skit/tui_add.py:71 src/skit/tui_add.py:140 src/skit/tui_add.py:198 +#: src/skit/tui_add.py:321 src/skit/tui_add.py:602 src/skit/tui_add.py:655 +#: src/skit/tui_add.py:681 src/skit/tui_add.py:910 src/skit/tui_add.py:1219 +#: src/skit/tui_add.py:1380 src/skit/tui_form.py:326 src/skit/tui_form.py:348 #: src/skit/tui_form.py:392 src/skit/tui_form.py:447 src/skit/tui_form.py:481 #: src/skit/tui_form.py:511 src/skit/tui_form.py:547 src/skit/tui_form.py:737 #: src/skit/tui_pathpick.py:285 src/skit/tui_pathpick.py:341 @@ -2692,236 +2710,236 @@ msgstr "✓ 已添加" msgid "Cancel" msgstr "取消" -#: src/skit/tui_add.py:156 src/skit/tui_add.py:180 +#: src/skit/tui_add.py:157 src/skit/tui_add.py:181 msgid "Delete" msgstr "删除" -#: src/skit/tui_add.py:176 +#: src/skit/tui_add.py:177 #, python-format msgid "Delete the draft \"%(name)s\"? It is the only copy." msgstr "删除草稿「%(name)s」?这是唯一的副本。" -#: src/skit/tui_add.py:200 +#: src/skit/tui_add.py:201 msgid "Write a new script" msgstr "编写新脚本" -#: src/skit/tui_add.py:201 +#: src/skit/tui_add.py:202 msgid "Draft a prompt" msgstr "起草提示词" -#: src/skit/tui_add.py:204 +#: src/skit/tui_add.py:205 msgid "Delete a kept draft" msgstr "删除一份保留的草稿" -#: src/skit/tui_add.py:226 +#: src/skit/tui_add.py:227 msgid "Add an entry" msgstr "添加条目" -#: src/skit/tui_add.py:234 +#: src/skit/tui_add.py:235 msgid "Path to a script, executable, or prompt:" msgstr "脚本、可执行文件或提示词的路径:" -#: src/skit/tui_add.py:238 +#: src/skit/tui_add.py:239 #, python-brace-format msgid "…or register a command template below (e.g. ffmpeg -i {input}):" msgstr "……或在下方登记一条命令模板(例如 ffmpeg -i {input}):" -#: src/skit/tui_add.py:287 +#: src/skit/tui_add.py:288 msgid "…or resume a kept draft:" msgstr "…或继续一份保留的草稿:" -#: src/skit/tui_add.py:300 +#: src/skit/tui_add.py:301 msgid "Delete draft…" msgstr "删除草稿…" -#: src/skit/tui_add.py:305 +#: src/skit/tui_add.py:306 msgid "…or start from a blank page:" msgstr "…或从空白页开始:" -#: src/skit/tui_add.py:310 +#: src/skit/tui_add.py:311 msgid "Write a script…" msgstr "编写脚本…" -#: src/skit/tui_add.py:311 +#: src/skit/tui_add.py:312 msgid "Draft a prompt…" msgstr "起草提示词…" -#: src/skit/tui_add.py:319 +#: src/skit/tui_add.py:320 msgid "Continue" msgstr "继续" -#: src/skit/tui_add.py:496 +#: src/skit/tui_add.py:497 #, python-format msgid "Deleted the draft %(name)s." msgstr "已删除草稿 %(name)s。" -#: src/skit/tui_add.py:538 +#: src/skit/tui_add.py:539 msgid "Nothing was written, so nothing was added." msgstr "没有写入任何内容,因此没有添加任何条目。" -#: src/skit/tui_add.py:602 src/skit/tui_add.py:653 src/skit/tui_add.py:684 -#: src/skit/tui_add.py:888 src/skit/tui_add.py:1210 src/skit/tui_add.py:1361 +#: src/skit/tui_add.py:603 src/skit/tui_add.py:654 src/skit/tui_add.py:685 +#: src/skit/tui_add.py:898 src/skit/tui_add.py:1222 src/skit/tui_add.py:1377 msgid "Add" msgstr "添加" -#: src/skit/tui_add.py:631 src/skit/tui_add.py:805 src/skit/tui_add.py:1295 +#: src/skit/tui_add.py:632 src/skit/tui_add.py:815 src/skit/tui_add.py:1311 #, python-format msgid "Add %(name)s" msgstr "添加 %(name)s" -#: src/skit/tui_add.py:643 +#: src/skit/tui_add.py:644 msgid "(shown in the Library — you can write one line)" msgstr "(显示在工具库中——可以自己写一句)" -#: src/skit/tui_add.py:647 +#: src/skit/tui_add.py:648 msgid "The program runs from its own location; skit never copies a binary." msgstr "程序从它自己的位置运行;skit 永远不会复制二进制文件。" -#: src/skit/tui_add.py:683 src/skit/tui_add.py:899 +#: src/skit/tui_add.py:684 src/skit/tui_add.py:909 msgid "Edit script" msgstr "编辑脚本" -#: src/skit/tui_add.py:833 +#: src/skit/tui_add.py:843 msgid "Link the original: skit never writes to the file." msgstr "链接原文件:skit 永远不会写入这个文件。" -#: src/skit/tui_add.py:836 +#: src/skit/tui_add.py:846 msgid "Link the original: parameter setup is skipped — skit never writes to the file." msgstr "链接原文件:跳过参数设置——skit 永远不会写入这个文件。" -#: src/skit/tui_add.py:841 +#: src/skit/tui_add.py:851 msgid "npm dependencies apply to stored copies only, so none are recorded." msgstr "npm 依赖仅适用于存储的副本,因此不会记录任何依赖。" -#: src/skit/tui_add.py:860 +#: src/skit/tui_add.py:870 msgid "(the script has no docstring — you can write one line)" msgstr "(脚本开头没有说明文字,可以自己写一句)" -#: src/skit/tui_add.py:865 src/skit/tui_add.py:1373 -#: src/skit/tui_settings.py:427 +#: src/skit/tui_add.py:875 src/skit/tui_add.py:1389 +#: src/skit/tui_settings.py:422 msgid "Storage" msgstr "存法" -#: src/skit/tui_add.py:872 src/skit/tui_add.py:1377 +#: src/skit/tui_add.py:882 src/skit/tui_add.py:1393 msgid "Keep a copy — skit stores it; your original file is never modified" msgstr "复制一份——skit 保管副本,你的原始文件永远不会被改动" -#: src/skit/tui_add.py:878 +#: src/skit/tui_add.py:888 msgid "Link the original — edits take effect immediately, but skit won't write to the file, so parameter definitions are yours to maintain" msgstr "链接原文件——改动立即生效;但 skit 不写入这个文件,参数定义要自己维护" -#: src/skit/tui_add.py:897 src/skit/tui_add.py:1362 +#: src/skit/tui_add.py:907 src/skit/tui_add.py:1378 msgid "Toggle" msgstr "切换" -#: src/skit/tui_add.py:916 src/skit/tui_settings.py:835 +#: src/skit/tui_add.py:926 src/skit/tui_settings.py:830 msgid "Dependencies" msgstr "依赖" -#: src/skit/tui_add.py:921 +#: src/skit/tui_add.py:931 msgid "comma separated, e.g. chalk, @scope/pkg" msgstr "逗号分隔,例如 chalk, @scope/pkg" -#: src/skit/tui_add.py:925 src/skit/tui_add.py:951 +#: src/skit/tui_add.py:935 src/skit/tui_add.py:961 msgid "detected from the script's imports — edit freely" msgstr "检测自脚本的 import——可自由修改" -#: src/skit/tui_add.py:932 +#: src/skit/tui_add.py:942 msgid "The script declares its own dependencies (PEP 723):" msgstr "脚本已用 PEP 723 语法声明依赖:" -#: src/skit/tui_add.py:935 +#: src/skit/tui_add.py:945 #, python-format msgid "needs Python %(python)s" msgstr "需要 Python %(python)s" -#: src/skit/tui_add.py:938 +#: src/skit/tui_add.py:948 #, python-format msgid "installs %(dep)s" msgstr "会自动装 %(dep)s" -#: src/skit/tui_add.py:940 +#: src/skit/tui_add.py:950 msgid "(none declared)" msgstr "(没有声明任何依赖)" -#: src/skit/tui_add.py:947 src/skit/tui_settings.py:838 +#: src/skit/tui_add.py:957 src/skit/tui_settings.py:833 msgid "comma separated, e.g. requests>=2,<3, rich" msgstr "逗号分隔,例如 requests>=2,<3, rich" -#: src/skit/tui_add.py:959 +#: src/skit/tui_add.py:969 msgid "(automatic)" msgstr "(自动)" -#: src/skit/tui_add.py:964 +#: src/skit/tui_add.py:974 msgid "Python version (requires-python) — prefilled from the #! line when it pins one; empty means automatic" msgstr "Python 版本(requires-python)——若 #! 行指定了版本则预先填入;留空表示自动" -#: src/skit/tui_add.py:973 +#: src/skit/tui_add.py:983 msgid "Parameters" msgstr "参数" -#: src/skit/tui_add.py:1003 src/skit/tui_add.py:1413 +#: src/skit/tui_add.py:1013 src/skit/tui_add.py:1429 msgid "Tick the ones the run form should ask for:" msgstr "勾选运行表单要询问的项目:" -#: src/skit/tui_add.py:1008 +#: src/skit/tui_add.py:1018 #, python-format msgid "input() #%(n)s: %(prompt)s" msgstr "input() #%(n)s:%(prompt)s" -#: src/skit/tui_add.py:1018 +#: src/skit/tui_add.py:1028 msgid "looks like a loop accumulator — probably not a parameter" msgstr "看起来是循环的累加变量,多半不是参数" -#: src/skit/tui_add.py:1026 +#: src/skit/tui_add.py:1036 #, python-format msgid "%(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (Ctrl+E edits it now)." msgstr "%(names)s 直接写在代码里,skit 没法把它们变成表单字段。想管理其中一个,先在脚本开头给它取个名字,例如 OUTPUT = '…'(Ctrl+E 现在就改)。" -#: src/skit/tui_add.py:1209 src/skit/tui_add.py:1363 +#: src/skit/tui_add.py:1221 src/skit/tui_add.py:1379 msgid "Edit prompt" msgstr "编辑提示词" -#: src/skit/tui_add.py:1211 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 -#: src/skit/tui_settings.py:270 +#: src/skit/tui_add.py:1223 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 +#: src/skit/tui_settings.py:269 msgid "New agent" msgstr "新增 agent" -#: src/skit/tui_add.py:1215 src/skit/tui_add.py:1430 -#: src/skit/tui_settings.py:274 src/skit/tui_settings.py:712 +#: src/skit/tui_add.py:1231 src/skit/tui_add.py:1446 +#: src/skit/tui_settings.py:275 src/skit/tui_settings.py:707 msgid "Choose variables…" msgstr "选择变量…" -#: src/skit/tui_add.py:1336 +#: src/skit/tui_add.py:1352 msgid "(taken from the first line — you can write your own)" msgstr "(取自第一行——也可以自己写一句)" -#: src/skit/tui_add.py:1341 +#: src/skit/tui_add.py:1357 msgid "Variable insertion" msgstr "变量插入" -#: src/skit/tui_add.py:1343 +#: src/skit/tui_add.py:1359 msgid "Fill {{name}} placeholders from a form before each run" msgstr "每次运行前,先用表单填入 {{name}} 占位符" -#: src/skit/tui_add.py:1349 src/skit/tui_settings.py:544 +#: src/skit/tui_add.py:1365 src/skit/tui_settings.py:539 msgid "Runner (the agent this prompt runs with)" msgstr "执行器(此提示词使用的 AI agent)" -#: src/skit/tui_add.py:1351 src/skit/tui_add.py:1503 -#: src/skit/tui_settings.py:554 src/skit/tui_settings.py:576 +#: src/skit/tui_add.py:1367 src/skit/tui_add.py:1519 +#: src/skit/tui_settings.py:549 src/skit/tui_settings.py:571 msgid "ask on the run form" msgstr "在运行表单上选择" -#: src/skit/tui_add.py:1382 +#: src/skit/tui_add.py:1398 msgid "Link the original — edits take effect immediately; skit never writes to the file" msgstr "链接原文件——改动立即生效;skit 永远不会写入这个文件" -#: src/skit/tui_add.py:1398 +#: src/skit/tui_add.py:1414 msgid "No {{name}} placeholders detected — the body travels to the agent as written." msgstr "未检测到 {{name}} 占位符——正文会原封不动送达 agent。" -#: src/skit/tui_add.py:1406 +#: src/skit/tui_add.py:1422 #, python-format msgid "Detected %(count)s placeholders — probably not written for insertion. Tick only the ones you need, or untick the switch above." msgstr "检测到 %(count)s 个占位符——这份提示词多半不是为插值而写的。只勾选需要的项目,或取消上方的开关。" @@ -2962,7 +2980,7 @@ msgstr "这个脚本有 skit 无法建模的子命令——请把全部参数打 msgid "skit couldn't read this script's argument declarations — type everything into the extra-arguments field." msgstr "skit 读不懂这个脚本的参数声明——请把全部参数打进「额外参数」栏。" -#: src/skit/tui_form.py:195 src/skit/tui_settings.py:212 +#: src/skit/tui_form.py:195 src/skit/tui_settings.py:211 msgid "required" msgstr "必填" @@ -3008,9 +3026,9 @@ msgstr "⚠ 目前没有匹配的文件" msgid "Save as preset" msgstr "存成参数组合" -#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:303 -#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:268 -#: src/skit/tui_settings.py:408 +#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:304 +#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:267 +#: src/skit/tui_settings.py:403 msgid "Save" msgstr "保存" @@ -3102,10 +3120,10 @@ msgid "Preset \"%(preset)s\" saved." msgstr "已存成参数组合“%(preset)s”。" #: src/skit/tui_health.py:31 src/skit/tui_health.py:148 -#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:304 +#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:305 #: src/skit/tui_runner.py:211 src/skit/tui_runner.py:263 #: src/skit/tui_runner.py:354 src/skit/tui_runner.py:422 -#: src/skit/tui_settings.py:267 src/skit/tui_settings.py:418 +#: src/skit/tui_settings.py:266 src/skit/tui_settings.py:413 msgid "Back" msgstr "返回" @@ -3172,123 +3190,123 @@ msgstr "教 AI agent 使用 skit" msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Install by hand with: skit agent install --to DIR" msgstr "没有检测到 agent 目录(~/.claude、~/.codex、./.agents 等)。可用 skit agent install --to DIR 手动安装。" -#: src/skit/tui_prefs.py:126 +#: src/skit/tui_prefs.py:127 msgid "Manage agents" msgstr "管理 agent" -#: src/skit/tui_prefs.py:127 +#: src/skit/tui_prefs.py:128 msgid "Teach an AI agent" msgstr "教 AI agent" -#: src/skit/tui_prefs.py:173 +#: src/skit/tui_prefs.py:174 #, python-format msgid "%(count)s agent configured: %(names)s" msgid_plural "%(count)s agents configured: %(names)s" msgstr[0] "已配置 %(count)s 个 agent:%(names)s" -#: src/skit/tui_prefs.py:179 +#: src/skit/tui_prefs.py:180 msgid "No agents configured." msgstr "尚未配置任何 agent。" -#: src/skit/tui_prefs.py:202 +#: src/skit/tui_prefs.py:203 msgid "Interface language" msgstr "界面语言" -#: src/skit/tui_prefs.py:204 +#: src/skit/tui_prefs.py:205 msgid "Automatic (follow the system)" msgstr "自动(跟随系统)" -#: src/skit/tui_prefs.py:210 +#: src/skit/tui_prefs.py:211 #, python-format msgid "Currently in effect: %(locale)s" msgstr "当前生效:%(locale)s" -#: src/skit/tui_prefs.py:214 +#: src/skit/tui_prefs.py:215 msgid "Editor" msgstr "编辑器" -#: src/skit/tui_prefs.py:217 +#: src/skit/tui_prefs.py:218 msgid "e.g. code --wait (empty = use $VISUAL / $EDITOR)" msgstr "例如 code --wait(留空=用 $VISUAL / $EDITOR)" -#: src/skit/tui_prefs.py:223 +#: src/skit/tui_prefs.py:224 #, python-format msgid "Empty means: %(cmd)s (from $VISUAL / $EDITOR)" msgstr "留空表示:%(cmd)s(来自 $VISUAL / $EDITOR)" -#: src/skit/tui_prefs.py:228 +#: src/skit/tui_prefs.py:229 msgid "Interactive form" msgstr "交互表单" -#: src/skit/tui_prefs.py:231 +#: src/skit/tui_prefs.py:232 msgid "Mini form — opens in place, fully clickable" msgstr "迷你表单——就地展开、可戳可点" -#: src/skit/tui_prefs.py:235 +#: src/skit/tui_prefs.py:236 msgid "Line-by-line prompts — plainest, best over slow terminals" msgstr "逐行问答——最朴素,适合慢速终端" -#: src/skit/tui_prefs.py:240 +#: src/skit/tui_prefs.py:241 msgid "Used by terminal runs: `skit run` parameter prompts and the `skit add` review panel." msgstr "用于终端流程:`skit run` 的参数询问与 `skit add` 的检视面板。" -#: src/skit/tui_prefs.py:246 +#: src/skit/tui_prefs.py:247 msgid "After a run (from this menu)" msgstr "运行结束后(从这个菜单启动时)" -#: src/skit/tui_prefs.py:249 +#: src/skit/tui_prefs.py:250 msgid "Quit skit — leave the run's output in the terminal" msgstr "退出 skit——将本次运行的输出留在终端" -#: src/skit/tui_prefs.py:253 +#: src/skit/tui_prefs.py:254 msgid "Return to the Library immediately" msgstr "立即返回工具库" -#: src/skit/tui_prefs.py:257 +#: src/skit/tui_prefs.py:258 msgid "JavaScript runtime" msgstr "JavaScript 运行时" -#: src/skit/tui_prefs.py:261 +#: src/skit/tui_prefs.py:262 msgid "Automatic — the first of deno / bun / node found" msgstr "自动——deno / bun / node 中第一个找到的" -#: src/skit/tui_prefs.py:267 +#: src/skit/tui_prefs.py:268 msgid "Runs js/ts entries that don't pin their own runtime." msgstr "用来运行未自行钉选运行时的 js/ts 条目。" -#: src/skit/tui_prefs.py:274 +#: src/skit/tui_prefs.py:275 msgid "Shell on Windows" msgstr "Windows 上的 Shell" -#: src/skit/tui_prefs.py:277 +#: src/skit/tui_prefs.py:278 msgid "Path to bash.exe (empty = Git Bash / WSL detection)" msgstr "bash.exe 的路径(留空 = 检测 Git Bash / WSL)" -#: src/skit/tui_prefs.py:281 +#: src/skit/tui_prefs.py:282 msgid "Shell scripts need an explicit bash here." msgstr "Shell 脚本在这里需要明确指定 bash。" -#: src/skit/tui_prefs.py:286 src/skit/tui_runner.py:381 +#: src/skit/tui_prefs.py:287 src/skit/tui_runner.py:381 msgid "Agents (prompt runners)" msgstr "agent(提示词执行器)" -#: src/skit/tui_prefs.py:290 +#: src/skit/tui_prefs.py:291 msgid "Manage agents…" msgstr "管理 agent…" -#: src/skit/tui_prefs.py:292 +#: src/skit/tui_prefs.py:293 msgid "Teach an AI agent skit…" msgstr "教 AI agent 使用 skit…" -#: src/skit/tui_prefs.py:316 +#: src/skit/tui_prefs.py:317 msgid "Download mirrors (mainland-China acceleration)" msgstr "下载镜像(中国大陆加速)" -#: src/skit/tui_prefs.py:318 +#: src/skit/tui_prefs.py:319 msgid "Each ecosystem is its own choice — mirror vendors differ per axis." msgstr "每个生态各自选择——不同生态的镜像供应商并不相同。" -#: src/skit/tui_prefs.py:323 +#: src/skit/tui_prefs.py:324 msgid "Master switch — \"off\" pauses mirrors but keeps the saved URLs." msgstr "总开关——“off”暂停镜像但保留已存的 URL。" @@ -3362,221 +3380,221 @@ msgstr "执行器配置已发生变化;没有移除任何内容。请重新选 msgid "New agent…" msgstr "新增 agent…" -#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 +#: src/skit/tui_settings.py:43 src/skit/tui_settings.py:59 msgid "Discard" msgstr "放弃" -#: src/skit/tui_settings.py:45 src/skit/tui_settings.py:61 +#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 msgid "Keep editing" msgstr "继续编辑" -#: src/skit/tui_settings.py:57 +#: src/skit/tui_settings.py:56 msgid "Discard unsaved changes?" msgstr "放弃未保存的更改?" -#: src/skit/tui_settings.py:103 src/skit/tui_settings.py:214 +#: src/skit/tui_settings.py:102 src/skit/tui_settings.py:213 msgid "Form label:" msgstr "字段提示:" -#: src/skit/tui_settings.py:107 src/skit/tui_settings.py:218 +#: src/skit/tui_settings.py:106 src/skit/tui_settings.py:217 msgid "secret (never saved to disk)" msgstr "机密(不会存盘)" -#: src/skit/tui_settings.py:111 src/skit/tui_settings.py:222 +#: src/skit/tui_settings.py:110 src/skit/tui_settings.py:221 msgid "env variable to read it from (optional)" msgstr "从哪个环境变量读取(选填)" -#: src/skit/tui_settings.py:137 src/skit/tui_settings.py:257 +#: src/skit/tui_settings.py:136 src/skit/tui_settings.py:256 msgid "Anything skit previously remembered for this value will be deleted too." msgstr "skit 之前记住的这个值也会一并删掉。" -#: src/skit/tui_settings.py:177 +#: src/skit/tui_settings.py:176 msgid "Type:" msgstr "类型:" -#: src/skit/tui_settings.py:180 +#: src/skit/tui_settings.py:179 msgid "type: str / int / float / bool / choice / path" msgstr "类型:str / int / float / bool / choice / path" -#: src/skit/tui_settings.py:184 +#: src/skit/tui_settings.py:183 msgid "Default:" msgstr "默认值:" -#: src/skit/tui_settings.py:187 +#: src/skit/tui_settings.py:186 msgid "default value (optional)" msgstr "默认值(选填)" -#: src/skit/tui_settings.py:191 +#: src/skit/tui_settings.py:190 msgid "Choices:" msgstr "可选值:" -#: src/skit/tui_settings.py:194 +#: src/skit/tui_settings.py:193 msgid "comma separated (for type: choice)" msgstr "逗号分隔(用于 type: choice)" -#: src/skit/tui_settings.py:198 +#: src/skit/tui_settings.py:197 msgid "Help:" msgstr "帮助:" -#: src/skit/tui_settings.py:201 +#: src/skit/tui_settings.py:200 msgid "one-line help shown under the field (optional)" msgstr "显示在字段下方的一行帮助(选填)" -#: src/skit/tui_settings.py:206 +#: src/skit/tui_settings.py:205 msgid "Flag:" msgstr "选项:" -#: src/skit/tui_settings.py:209 +#: src/skit/tui_settings.py:208 msgid "--flag (empty = positional)" msgstr "--flag(留空为位置参数)" -#: src/skit/tui_settings.py:269 src/skit/tui_settings.py:416 +#: src/skit/tui_settings.py:268 src/skit/tui_settings.py:411 msgid "Resync" msgstr "重新同步" -#: src/skit/tui_settings.py:390 +#: src/skit/tui_settings.py:385 msgid "Basics" msgstr "基本资料" -#: src/skit/tui_settings.py:393 +#: src/skit/tui_settings.py:388 msgid "Renaming keeps everything — remembered values, presets, the stored copy." msgstr "改名不影响任何东西——记住的值、参数组合、保管的副本都会保留。" -#: src/skit/tui_settings.py:398 +#: src/skit/tui_settings.py:393 msgid "Description (shown in the Library)" msgstr "说明(显示在工具库)" -#: src/skit/tui_settings.py:430 +#: src/skit/tui_settings.py:425 #, python-format msgid "Keep a copy — your original file is never modified. Source: %(path)s" msgstr "复制一份——你的原始文件永远不会被改动。来源:%(path)s" -#: src/skit/tui_settings.py:447 +#: src/skit/tui_settings.py:442 msgid "Run in (working directory)" msgstr "运行位置(工作目录)" -#: src/skit/tui_settings.py:453 +#: src/skit/tui_settings.py:448 msgid "The source file's folder" msgstr "来源文件所在的文件夹" -#: src/skit/tui_settings.py:455 +#: src/skit/tui_settings.py:450 msgid "skit's stored-copy folder" msgstr "skit 保管副本的文件夹" -#: src/skit/tui_settings.py:456 +#: src/skit/tui_settings.py:451 msgid "Wherever skit is run from" msgstr "运行 skit 的所在位置" -#: src/skit/tui_settings.py:457 +#: src/skit/tui_settings.py:452 msgid "A fixed folder (type it below)" msgstr "固定文件夹(在下方输入)" -#: src/skit/tui_settings.py:468 +#: src/skit/tui_settings.py:463 msgid "/absolute/path" msgstr "/绝对/路径" -#: src/skit/tui_settings.py:472 +#: src/skit/tui_settings.py:467 msgid "Interpreter / runtime" msgstr "解释器 / 运行时" -#: src/skit/tui_settings.py:475 +#: src/skit/tui_settings.py:470 msgid "empty = automatic (shebang, then detection order)" msgstr "留空 = 自动(先看 shebang,再按检测顺序)" -#: src/skit/tui_settings.py:589 +#: src/skit/tui_settings.py:584 msgid "Parameters (the run form's fields)" msgstr "参数(运行表单的字段)" -#: src/skit/tui_settings.py:595 +#: src/skit/tui_settings.py:590 msgid "(programs have no managed parameters)" msgstr "(程序没有管理的参数)" -#: src/skit/tui_settings.py:600 +#: src/skit/tui_settings.py:595 msgid "skit doesn't write to this file — maintain the [tool.skit] definitions in the source directly." msgstr "skit 不写入这个文件——请直接在源码里维护 [tool.skit] 定义。" -#: src/skit/tui_settings.py:619 +#: src/skit/tui_settings.py:614 msgid "This script's run form comes from its own command-line arguments. Managing a hardcoded constant here would replace that form — leave it as is." msgstr "这个脚本的运行表单来自它自己的命令行参数。在这里管理一个写死的常量会取代那张表单——保持原样即可。" -#: src/skit/tui_settings.py:625 src/skit/tui_settings.py:694 +#: src/skit/tui_settings.py:620 src/skit/tui_settings.py:689 msgid "Detected but not yet managed — tick to manage:" msgstr "检测到但尚未管理——勾选即可管理:" -#: src/skit/tui_settings.py:634 +#: src/skit/tui_settings.py:629 msgid "Every input() is managed — this script can now run with --no-input." msgstr "所有 input() 都已管理——这个脚本现在可以用 --no-input 自动化。" -#: src/skit/tui_settings.py:650 +#: src/skit/tui_settings.py:645 #, python-brace-format msgid "Saving re-reads the {placeholders} from the template." msgstr "保存时会从模板重新读取 {placeholders}。" -#: src/skit/tui_settings.py:663 +#: src/skit/tui_settings.py:658 msgid "Variable insertion ({{name}} placeholders become form fields)" msgstr "变量插入({{name}} 占位符会成为表单字段)" -#: src/skit/tui_settings.py:674 +#: src/skit/tui_settings.py:669 msgid "Off — the body travels to the agent exactly as written." msgstr "已关闭——正文会原封不动送达 agent。" -#: src/skit/tui_settings.py:718 +#: src/skit/tui_settings.py:713 msgid "Add a parameter — type a name, then Save:" msgstr "新增参数——输入名称后保存:" -#: src/skit/tui_settings.py:719 +#: src/skit/tui_settings.py:714 msgid "new parameter name" msgstr "新参数名称" -#: src/skit/tui_settings.py:759 +#: src/skit/tui_settings.py:754 #, python-format msgid "%(name)s has an unknown type — use str, int, float, bool, choice, or path." msgstr "%(name)s 的类型未知——请使用 str、int、float、bool、choice 或 path。" -#: src/skit/tui_settings.py:771 +#: src/skit/tui_settings.py:766 #, python-format msgid "%(name)s: the default doesn't match its type." msgstr "%(name)s:默认值与其类型不符。" -#: src/skit/tui_settings.py:779 +#: src/skit/tui_settings.py:774 #, python-format msgid "%(name)s is a choice parameter but has no choices — fill its Choices field." msgstr "%(name)s 是 choice 参数但没有可选值——请填写它的「可选值」字段。" -#: src/skit/tui_settings.py:805 +#: src/skit/tui_settings.py:800 msgid "None yet — press Ctrl+S inside the run form to save one." msgstr "还没有——在运行表单里按 Ctrl+S 就能存一组。" -#: src/skit/tui_settings.py:809 +#: src/skit/tui_settings.py:804 msgid "Untick a preset to delete it on save:" msgstr "取消勾选的参数组合会在保存时删除:" -#: src/skit/tui_settings.py:840 +#: src/skit/tui_settings.py:835 msgid "comma separated, e.g. chalk@^5, zod" msgstr "逗号分隔,例如 chalk@^5, zod" -#: src/skit/tui_settings.py:846 +#: src/skit/tui_settings.py:841 msgid "Python constraint, e.g. \">=3.11\" (empty = automatic)" msgstr "Python 约束,例如 \">=3.11\"(留空=自动)" -#: src/skit/tui_settings.py:862 +#: src/skit/tui_settings.py:857 msgid "Needs (external commands)" msgstr "所需命令(外部命令)" -#: src/skit/tui_settings.py:865 +#: src/skit/tui_settings.py:860 msgid "comma separated, e.g. ffmpeg, jq" msgstr "逗号分隔,例如 ffmpeg, jq" -#: src/skit/tui_settings.py:870 +#: src/skit/tui_settings.py:865 #, python-format msgid "Entry settings · %(name)s" msgstr "条目设置 · %(name)s" -#: src/skit/tui_settings.py:925 +#: src/skit/tui_settings.py:920 msgid "Everything still matches the script." msgstr "所有定义仍与脚本相符。" -#: src/skit/tui_settings.py:1080 src/skit/tui_settings.py:1099 +#: src/skit/tui_settings.py:1069 src/skit/tui_settings.py:1088 #, python-format msgid "Deleted previously remembered value(s): %(names)s" msgstr "已删除先前记住的值:%(names)s" diff --git a/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo b/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo index f9902180d2831b9a5a940d8faedf99340674a46e..383443600bdc37ff2a03b0ec6e2ddf6dd7111e73 100644 GIT binary patch delta 13359 zcmYM)cX(CB_W$vHC`o_-A&}4#2qCl(Ac=v5fP~OXXrVU=C7~sh(4?Nwq)BfYngzu| z5s_-{1w;`Q0V#?FM5Rd=e~4aCzwggk`+0u%pVzE?&fYV#X3gvqydNy{du^4U_i}KN zwGRJ_DeO35IHRJX|Np-q;vJ_q;Uz4F4WDtGKup2in1*Ti4<=&A1ji|bb8#oG!fu$_ z&~cjMYMg`TahKzGo!*W35*Pfrdq3u545lYJPE+(^cPzjx3~KB+%Ww*oM!#go>5HM* z9LHf<+>K4}6P$t`0>QG>xrXUj|4_=L>8_@BJ~TmE(k9Hde(sSPx&r)_4X{*WPhn#Q}I0|H1wERR_n}fs05p zmz+f9Uo)Rg1*u5(A>VNV$lF2ej!eQS+1WZy$F;qGrC#wE!tA=N68@ zYF%w97UM_67m<2)HgqHZ6s~iM-UnbI7F+`wgv!7gRI$~e0?EE}0IT5>R3^fEup_WD z>N)eU79Kzi;0iKUr#xxa#Impf?#9yilky)-<2NjUMS3|-eGEcm%s|S=Ifp9RkUrEI_Qg!}mS*pe4QCQo#^b0|{)`%NP(NFO z{y3fZ6`X=$RG?ggtdbK*`r@%4DpN~Q137~v(fJECk+gw!KI9@3@jA0=vyWm{F zy2OFc+4Uq$B<_jLa4Bl$C$S0^rKHPZ6l&l}sAA1RO=t;Lz-_3^9>Gy~AAS43Uxwop z))z1k-$f1J05-&{sOyym+X+|;RjhHSC7O$xz-rWPcn{0tIaFqDV>C7%V!LA)RwLey z;a(c2Xef1eun1NiY8^(QA8{PEz$Da+r{gKicRwFF%yA-!$6+L{#3uLw`r`u(z(27P zdRTa6Dh$0EX$Klg;Skh}Cu33Ej(WiR*cK0A5Eja^j>@7sYJ?hCE^5tJqL%CsCgVeF zjP-_FJRF-6zc!rp*T{b40tM;ZM9nmHgq>9LP^r6$dO+!sR{ia<25}y0Y2L)DxChxt z&N*z2by#RkXe?^GzJdPuHLBmsqsYG>jT>B02JT@L78-49TMO$DC!)6BVAs{C;`#*j zpr23!xrNdA7_}6U*>=*k#!%wrsO|bDmcS3aG!kfh>VDxV>c+A;RwRvF`(Oyyr=f0K zfqA$Qk6`#1o8e`wMO=EUWikGE_pud5O|a*TL}hpkY65diud~d(kdG>^H&GARk6M~L zSPy?k6;-u~c4GEL4Imp;)zh&Yu0s{|yQt?KLuKGRYH2G?vhSzh^LqdHqp^w)&Y_Ah zYqI^IIE?d%D^9V`H)9fU-1ByZkHQe*e2l^OP#s^w2z-PUFm$SAr~z`~IjL9|pU0xw z|66IO>fdp_i^GUxr&+4j;vC{vQA-dt-HNR{79q~SqL_)w$Y@N)Y1kO|qMmmjpTQn8 z?CrS_ef$3e4UPN?2I8OC6N|FYO|d_w;7h0uE@ExGfvWbPS=M0_RLUn~FfKq}hESP! z4^`ZsVR5`Oi~Q@0kGW71i<1t`r~3*&N(#nm_nKSpINYM%RA_tMbJC!ta|7d65)sI@zas{SXawX4IZ!f_zR z<8;@Zs0sXp&9Tf2c3(PnAkIT=(=Sk&yo4IK_ZE%nG>R>-k=Mf_#N$wFJPFI;X4H*) zPy;=J8sJx05le7LL}3lo=N(W3>W{i_32GuQp(c17i|PIUI}O#q->5GJEwY(~qt>=7 zl4YkqvLl_#I0}0%wjw=(8bIh0E4EnFnzzAf=tXt>Dr#v?V+sbpsLZhb=`>hoXA=&` zs!Q#?y#RX=U%(L28QA`RI2}r1MyGydaqTs9dj^D-^XJ3G8V_zQQLYaM&fa7hQFfv ztG$}+JWr!1joNq$Rb-Db2+b(oxS$*MpsM>6mcl#O37?=I(0-l06S`p=;y%~_w_rCsjrxAI^|q8_ zF^>2fWKK@8m+VixO{k3h`V#q9M}9BcgF`TuI1Cf86KX(nP$%IlsF_|u)k;9VWo{g5 z*Sw6nZ!>!EoEu-jNyNoB*xv(rm`HruOQR$W|BY6qA=rhuC8|nSqpJTLPQ_lE?05Vb zY(iXaGlh#?(GTyTQu_##F<^_Gj4e@_7=+5)N-U4wi!`)vA7CvEdBwKLv#2-^!|*Mv zho4|m{2jYv?5lRL%)?aTbyyBBVh6m7&tlSR_Dg9p7A5v#C^++JsMt25X0QXR;Rjd) zzsAD&6qRDVW7=blIP^9M$1|Nl*+0v95-S|%E! zcEfY1HCuqG_&0XNlsBxZSD+re5!K;79DwK1ALHJ%rD%+*i8NGZW@CF?hehb$xk5v0 za}8CD<+s_|*F+5@3rpj2Y=~P>GrNF(_`vlsYR&&arM&T5j`Jc;L4B{t+ctm_sHKTO zZ%Z1ZX=nhuFdl!xwpi{R`*V8$CJ{fx=2-V#tKw`NOZ*|`>H2nC+wG_&eIF0uF|32L zcG&iO3swDp>>&SvH0tlPB5H!2h&!PkumLsl?dXqtunc~PO8u9p)ZajLP;{5=k_M=y zcpFpD?>&A)U>o#ZN7anqZt|Z+qy28OfbXHIJnVheA3LB<#J5oox`Z_`e2*Q;t#Byu zEL0|cMm@OZ2ll;@sNJ#x8{?N)9sT!O@z(XyXvKvj9EdYf5BeSlW7U23mYa>;iNC=$ zSa&}^Jn;y2#Qq1YgKd~W{1bM=76+}vHK-y!fD`b4*bcp8SfFQU>_?6CHrB%0AKI?y zilc}}qB3(H+hd^u`;#jTbv+xk6!};M4`2p@sj`AM9Ur~E873#X+-lu zp(FNos)wqHY}DFM#Uhx88t8m1iU+Yc9>!|;IrhLin1HEA?Rk?ijCe0L!n0T(i+!Y` z_cAgX7r8JB$)?ltm~D?Is1DnFY^j@ujfo3TGyVgcVbmw~dL4>2h+n`$xE*707dF5v zsP6|Iw+wZ`*7Wa;q0s_AK+WJW24mW%wvPwlFyc+v1q+?9KTOhbF7Y?$tAWq#eAtG) zxqb8C!kWg3^lN0sBQQ)YOQZ# zbM*hxYN8$XA|8SI{4mzS%czX_ow5uko+AGxxG;wc0k{mKaXps8kFYF$h3fE{8$ZSh z#3fE!jYMH2aa~kLov}Jj#t6(uJ@-Q_hS$*_ADkxtdhnlIsEozW*n?|gJaGzY#JL!U zucJD+fT8#|YT)I*vLX$~RN_d~%!i>eI2ZkJGX~)ssQdPLX(ZD)j1BOASRbR$+5yrR z%M<6JGVu}y<94izhcOXvV@s@l&bHxjR7dks9k0W}xCvF9uc9*H-Rgc}C#vJUsF7X8 zns^tBV7c>_vWlqBtDznojoMyu*ai8J;!V}mAtFl+MwpmyP7o!Hg5%v8qP|rJqy6+YSV(PaRcgCW`Z(yMI z|GPAlngiGq3vea|e`hILfg!~CsI`3$E8#=b8khXuK99lD#4Rw}!>NbOh!3*|E8st< z=aj!>&#i}n+W(0(!m$-W(Lf$KWC!ocN;+XHaWg z;kx~s@Lb$M{4Z+YJ8sx7rK8xGIQXW`JPi|xhhQe=-z5Lf&~SdW4iZo!%f;S!0=r?v zEjx;Hu?O*+SPh?GI)>l2>K})Vh(Ew6yp63f=#D+V7xpDygPQQYJLF%fnswLC?l(~b zIEm31dCy*#oiKy=D88%f_w6-&3O^zq`oOBc=5Mw=C!z+p0AulOOu+9j8B0I3qV3?N zp(>q&LJTamgn(utZd~7hpqt zg8Dq}so5Xv63<6XcsKUd{tx(@v~pnxDpgVc*o=DO7~(pJN&P*R@O`kME?bjw;d^tb!S+ zrSf7Ud=(quMQn-zh3z@5u{d#WR4om`xwsVdy)s1@0R20iX&k~eI0L&B_4um(B&wq- zejeXiH^UHilvb@e=e_qH%{t zDfBPy@l|IiRweF+DzfpY2W>_T=rk%Lzq!^ZVR2uK;QCSw!JVjc<6~3?3I$pQ>tRLW zv_Oy7cOYbOA&U!}F&&GR^!Q#beXum~Ak+iKViU~AK>XVE7wkd&0E4l0DUa_bS|5xc zegW0tHtdK;QM;sMX|KmuTn$Uxj7Ol(>V>EozKVW$0oCCTuD_y+@()y|>I8Y5eb^rL zeW#2K*xxlCRbvCO7_P$NxY0{PDcy!Drq58RbAoN=p_oEg6ZPOM*BneGo`@RQKGeXE zp`P;tYQ|;DdVGIF)%#>S`_ z^+eUei>TCZL>)AzQEOSWf_=XpY5?s~&*_C4*g*I5IhaPg8B1&b-=-17g(s*R0xQ}L zWl`rr5^CRfK&|yi*F~rSy^Wf{L0pW-a5r|YWHsbew&x|FQr{P~E0$po`gcy!=!Vt8 zY^D=j-$4!NCsZbGx^awxnJz@lWR>fuSdRE9D)nU|tQf1H;zZQ*TcNgR5A1=% zBiR3{{sJz9;$<9*k8nO_R`dA&;<=6bJUQ+5N1YF!qMq{*m6>8u zHi7!6Vr+#v@Sbz8PmS_g5iRF}QuGOGhQFa^`~*8;gBl+D^Bgsk@3AG`#s>Plrp;g*1IM6> zcNbR2BdD2Q#TYDB#|GBOwI%BLT~SNx9Z93M8mJTLEi8|FP;2)EYDO2a1ODh59c`aa zLVZ35Yv3aH`YsG3Ep<7VsQtblIq{rF*dD9LalUB(=g{!21!}}=QPsL1 z8{=1~6qks%11K0(gz>2RT3}i1gLN<)RXgiY9qvV~`7fx6JVi~g-ZSifRedrInT7gb z6{-fdpgP)*I)Xn%Ezv)?76TGIzW)!%W*kNw+R%2%bksz?MIBsEP)k+5k-c;>u?g{- zM(qDI8fUnml!PbRFO}h#L%b88!>}Yfv!`Q!;uAO;qZ-?OUV)mxyQt@UgW4_ClI<5! z25Le-qKf@7YGM&h*#Am#Y!i>~SLrC6NqiJ_J*BCw`Bcde6ESenzF}KI#bdPqoxmLCvf&YVFfeFOdPL zneBA1@5MgEhf%NR3N7q}?1=iAz8rPm1yoViYU$BGV0xXNG&GY3sH&~s%0|`+)xkhi zs)wPr-%F_PeTgcfJ8oRNwa53@Z(EGu`Zm=4Col<5qXz@p*fuSWqqP4MY3M9|4b_of zTT5Xw)*)_-dcb(p4~=(l5}w7W*sh(`%17wiUbvF$_1fD24x=)A9QB;bs0ozsK&<`W zh=x)$7WI1Eiz<#2`U2jN)j@p)IP$~CMvt5#mdd>pui20}vuj4>0 z+R45@6unB(IvR2l>Ok3wDvm>_qWj#v{tfCucTq=aU}r1FAk_UKsQbcD6N*7)EEiRL z`KZ_LF|2@>JG1|N`;iMOu81ynLo}+Rbkq`zcin(G7d}D#fOw{>{YYJi>i8GbeZQkJ z7~IX{`}h6?)b3b`SHf4?{*qWLi?l zN^t>dh9_Omp=#ti)QNWkwYG)(+3$fc)PP!G2L1tsREi@8*cUS} zm-r~^_d&uytKPoYfOtRZ`!`VAtJZUNM5o|*;u)yV?_o2nKgen$2NQ{3^y#Hz8dbUQ z6jg*_8FnPsM4bzLP^m4z!RSBO-ex0F+iO2+O$!h4`2KIYYp@M*@=)t&Hg+dIhF*o`BH z+x1k`gffsb$oUw(Yq?Nrq`k$q;%efjsC~O)lx>p(s5Sl_wf4?v(;rpcrBIn`h1zDF zQ3L6V{qT9zb~}eUvP)%Kt#rs{|LcUy;zA5=L9Ojcyp2!sFy73u>VIpD?T+8DCD-G} z+8R$ry;Sz0GI15Pq;<#H_UwonpcmtCk?V)!yjG2Oxe&nz@#AexyQ3bo1+{k9Q5i_i zwHdd?Uc@s|OLiWIWBCal-~U&{i>i&As7xkIwCz0rzbAehwY2lSlk6Zlfcgn_3f1v5 zlPyKlP#v#BrTTT$KK>Tns0H_f6DEQ zx^F9LV#iPeJdcIx-}zn*{1KIbU(g@#Vhlb)-4Hp;W?BaocXRFII>UuEb#C z_fXsH7-~X~P)qnPW?{%&kMBSG&6vyn*Y;Y-1wHr&*I!T>xrJTv9U%$3;@zn!)*YcL! rFUVhiw1TH{iKh0LYb%yETOSwV*}c9^Gf(}(_Y1b{zY^laj(N zP_~{PJR~iMN>PbC$`VQ8|M|}EdcFR=p4am}zw5s4>zwmDzjLmeTDPM3drOP^&xZy) z=lG{~QRkwuZ=|CC|L5fd=Xwynf+g`b2H`{8jzLYF>xla>8A~TR7mS&>6}w%|RSK&L(`CWKZ=T>mxFz&+MJf$9nHghf&TVq$8hJ*1WF2&^L&V}HAuos@jG;EaO zTzMRWEpQ%A#DmCKTyrkA!;#pL{@pGbd0Ysfw+`3~>BGH=UGO}{V&j(13N}u+6mP~ii66^w z?idEpdmlWB6|r6$=c-}`*2l-OEk27R#hpfFv@932wy0$hbp!!NcP<%DzO^wMGfdXq>x>3o;{}x z*3x~b0lbck(Vam}tS0Ghh+{AW*DC*^G`3@D{5Lkhlim#vu?BHScj^I?P!Ar0D$-Z6 z0-nKA_&=CjOT@&hn5jX^u*_l{??_lBnkIHwhsBXX@ z9EKXeBuv6LP}k3)PQYJK#d-s^hM5Cw0$owNVKj#0N>pYxVLbkY+8uF^TMZ4z%6={^ zrJ>Yq#sK^t)!|QA9B*I--bKy0#S_k*z}~3O>kMR_u@P3sTx@~kFc7z43H$(~@E|Hv z-=be5Ekz|Lh1F3rZi>ZlIO+joQ6ry%W$_TIqc2e%-9Zg3VX&=vE^5i9U<$sAsdx=F zkoY0aQFgA$5Y}HaTfzmF!+nNIRl}ina`i@v#I3>_cnp=98(0+|AREM0dD2e2aj0#% z2LtgcmcUzB9RER0v?v7-hv5a}Uu&4k1#Pd6o-Nxk;nv8+id-0krEx0G#07W&ON_9Y9mQJ2_fVOu zKGJ4B2#*kNLUx)PH_B@2bI*X$mXT!CZfK1G=fJ2<@$2e_tzmy;&*3gs2b0siYDkOI}zJpFmVr5 z)ec6bb{48=pGQ4#E4Ib`s5LD%&hC%LNyKe&1@1!?UG8}My|5AI(!UFsU^gtnX50`x z(az{@s3n<)^>7V_;}ML(A5f|E(Tg$^g-x*@*2O2V7%oFC)pMR-<3Qp{lgSkQyO}g* z;}X;ogiNtwYlT{)4yXrpMrEWsrr;oKj<2I0cn+Ij>#6p3d432QW#4401U;kSePMfm>*gQ29a8Kz=O9FJP69jN{P1M0rZ z*bqZ!+JTjU^N456OSf4KRsSW_+EtiqKdstf0`Xwa7f~}fj%oN0>U%AjMm7#Z z72!@)CXb*7ehOpp56r-b`Ig~6s3rF2)2KjW5$cPtqDHz2HNp=u67OIfmR(?)fICNNAziUB*thp>?2fCwJfN6`YN;jbf;9G3P zRtdG{jj589SqAqGq-l zAH~C{>JMLP1MPy!*eTRL{}oGP*fM*2R!4Q*6Q9KOSP#oQYo=g4@emB7f472$QvEON zhd=5AOkZx>u_vndhM-b97faz%)c$@EtK)WTgjS<8Pv=!qDK51*22hDcEqNmKJSZxIMj1Ah7wQmd>W&OU&YF}8-wx8 zD)OI0;}RG2fW+tQJ)MeK#F^L-7h^8&LERtpysc$#Y)pIz8Jqh9JK)0Ama(r;9o@j< z=w7hx8iA`Z2r?Qy*J|5zI7x)D2L{2Nx)Q&A6I zfa-89_Qn0EVvT;&mLd*S6UnH|49E633j^>Nsz^_wim}KBTl;cYoBmxcjS!rINw^p_ zvjbQhzwx|?TJtNYl*hfrsfUlF?)x1zfIFzADgCz9Qg_q4%=u|+yh4uZ@`(lzRA{hHEK;?!9BPY>)?}{ZF{c7Wa0}LgppgUhH7FCaWmBS z=WikZ8u@B21mde$4mY4u{{brXU!Xd;j@l(rTWu*;VQb(GxX;`R6x{*3Lg*KV8OI@CZA zH)1t>AM@}GCStt<_P_xcP5e4G#gDK7{(+4#_8^B8c0-cp8XU6SaS5Af|2O{FQa1>- zhHs%VaRD{rkbm1j(g|x2kHI3i8XMtD*btAQ?*ALBV9F=<2a{f?nt2U1fr}W5$^T&+ z)4$86F%TExqj(MHV#~w)oWes`SOZ7wd{}`!xPAn`#h9Zuko#DPxWRv|>a(#k@pg>F zBF8MF(Wp$-LVpyE$7m>}lTjnviskSiY9?P|8s5aJ*yOl=I#IBw&o^Q%Jc`Q54a~;s zpV~<}0!t82#(12K!MNp9@?W0Dhg?vHCsFZ5RCV6L;uvzmmLLq(QFDyN0T_exP!HaK zCGm3%#BWf~{SlR!+oYsN!6L%EU77^A}Mazm6K%XIK;cU(?Xc9->kf@TJ`lgnDpA)b@(T z9BhsSxB&CsNN7RXt@zItv$beHW_uGEQ49^u@NsLy>;{ZYz!MT-bqGSn76c~oZBV>f&Yr{jGrsr^6otoNr_ zOy`3&s5Si#wZ?b7&%@8zYq~xT_pv>(6`ybX#)|3+RwORMo>b~1Py?urIyoC6|8qI~ zUwMrGj()WNThTazy>Jl^u72JIviW;^&~F&b=XbF`2L510It2CLWf+26@E!aJHPaCn ztY$vOWyFCO`GYO4MlDU+kK}(ljbk)g;`E>FhrwRd8W;W9{?2zKZXx~^HS!lO*}<|I zn-kwh%{=)RE7lwwL_80h;8oP~t6sK&^}`;-J1&#|TpFdX*p2;AN91y>hL^AlmiX0X z(g&LozlL#m8r$OEs1DMv+TZQ?Q8PY^weZRR+0nfmm6=@_k7cg;?RDAgnpN{=+@ueF zv)Ax$JVe~-cdPz#*KK?DM-6Z+Ho{e?6Z8{I!F#BpO}b%4IS_UKUeo}-!{@Mx|EB$N zIf`?+Q12Em7~GEaF!m2iac7JrUW}U2R?pLzLHq!>WBP4QKfHzS;wN|P-ziP{)7~u) zQ3HGQuD#~{GiWs9!V%nzzQ62)tyqWnE^1)a?^(6Kg-OJhP@hNtZDwIz;?byW_%imw zTX+z2{;^Dj+_wp(;YjWODKrwe;R-&1(GN%~&P1*4Ev$mgAKC+YVrAk*sF}QlF?bp^ zfIqPV#_|U=eSZ`t;`>+*fA#zaOKATG`+S8}8jk9y18S`bP{pZ5}75BLrSSrw0_^(_Sp^EP`>f{V8;VZ0-PMA+T0i*B?2IEakL0?H<;cGk<{VKA) zH1wcFsG09U?b~lX%a*dZ4aRVN5>~_)u_eBP%D^>L1|v%Q3XkYyOeW67!MF&!;B_pH znL$2(;mMU9WDn?#E%;y_2H`=^6PQQ*4TfT)GQPr}Xqgy8JO(3i1#0a#qjt$%RBgos z+l;$n1o2a-2`<+ei*@K^-3U{0jBO3s@B^g!tS}Ohk?RDr&|zJu8>BB5Q{wxjr3B z;Q~}fSD%bQm?`2Ur^;LTw4t zu`%&jRMo$MdhS{5jX$7@vqgE!sDBcTnq2r4wV(gM>R6?MukeUXM-5;ws>7$S4?ctK z@Cw$)go-xveAN8~sE#LMC7gj8_)Dmne~z3Zes_U})+#*AZfuQuKp)fthN8~;QQqgP zup{v{)PwG0Su7K7_g6vPUjwyGvr$Fa7qygAJ=b7}_WxcQTC-!g7*FAL%#W}l35~P| zW}#9#3{~~7ql)r8=3!!GV>1g})txQQxbH%3M>=OjNOTMpgS@)I?vx!rDN;8fR%#z~CzO zfLK(qHS*&2s1CZLw%=gP!^x=XK7nC)9YuTR8BiSMJ{_c<}X!k=Dm;ymKm zYV7}pH2zi1w$oYEg955s$}6E}kcKM0?x+)Ow0He!q=?*FREEx=W>_@VW?Tkyh|{qj zE<;V^8fN1CSoXgLkRE3*Q77R|EQ*QsY{{D8 zFyf)8lz!z|qQ1>66}8swQ7^Y%sA6A?TJrsvr2YRp4b8Mh150^^=P1vYQTzQU>ZA*7 z=qvmik|Zog>_;uda@2s|LKW*#Y>q#oGF-Wl9YArYfwjSk+W&bp%3}f6!5OIHc^lQ? zVbq%6M$II+vCXhGs_Hv>PV%n5fy(4|R7XcqNAFqG5|vHx75?~)LjOcAY@;y{Yd5iN z@(gMwS5YTdnMB)0bx=RKCSnWRfI5mVVtZ_uWIseE;|Ssd_!u^5YWF>ZeTdJYmab_s z`(OKcU9!#OBh-VgU@L6W%zp8VMa}32s@h97x0xlNQk;R^aT-p?FTLxzDYoWMquzQ4 zP)lY$}>R0Sq)E9q26=m~uU*TU+ z4nfVNNQM<{8fswuQ5}pzrFsHt`)x!O_xDH*`P~EWLQ1C3_2+|L7=yb}4>*U-@B;cU zs*P>aXe=OZk2+Z1Lv<9<)-u=;>k#)seSZ$>cgBY}9)H5g+W)pbny)s3ka#D!OmI>sL_Ed5Ahmt8`@l zs~W4*(1U8BzSsaYqgJSt%|#X8W>is~!U(+XT`!qq#g%}%KNZ!{K-3b<@!Wzs7tWx5 zKeX+{{_joWrB2rIZPXWwJ!&b8!@k5>sNJz1E8td4!H-chet>CMp|ee-8!DrHQJGwZ zt#KV{X)dC^7m(|>qKV428FfNsVuozTka6wu^Nz8Fdma!8AOJ8bDN6 ztNI*lM?4#KKppe0xA*7S+ieDB^T7d}f~C9J$u}R15f|%jGcS#=5XWFc+=oi#Z>Tdq zvxlwu6wd{y6LJ-5X-}Xgc;3_hGYu8V|4=91U#PXM*wcOwG(Zg~5A*ReY=ez@Sv($f zrk_ToIH9*)AB$s&zr;Aq>SM(_3^nkh$o+oz7Y%K%=6&sq&c!jrOHmJW{p?+khN=xe zCgZD~r?Dz=@MBhl4X`nBGt{|IfXeI%9DtGi?PWF<_1~YmqcpUp74v@HT4T-+11`k;IV$*-iBC7Sd3M z>rprUit$)-kj06fT~RX{iyRs5G(JZhGsIqEJ8>m(@KD=^>rlJozo;cH_N1+SsAnYl zRoyW(^m6Eq+GhEvfegdmxES@IpOG`$#S~br^hKSBldv9cN3HF7yo$lYd~QG9LskEt z;kG-9j<9!2n-T2))_ky#3wo&>L1p3&R>hVhZF}}Z4RASXZP$2yiYms37=vv_*@-y_ z^_=afrMrvDK*!OxoqM5vFDx6){@0pa;=&NDGsai=--0Yh9liHZCtB86+utMc8{)mF zwO##`9VGw7fy6(cI&M48GPDHM@!P0OzmGb=uA<&G&OhGvWn<5DjN*DO>TNb0wG^+T zmSR8Z1dEzrrlM-)F?<9Up*s8=i{p8$fj@h3>4~Ej5gMXqrj+kWkXQGxU8_QxI>Uo2(BF;hmc6*KYQ1{(Or8HoQ#gV8EVo)9SKuusiDwCU02i!3X(*FO6hK|M? zsNZb9skXNDu^Vwd>We#3GdqRq_!1VyYo0ey8Muvs_z<-#0;k#iiKvOT@Zu*V{ksD1 z!Whp}y>vs0)U1\n" "Language: zh_TW\n" @@ -18,61 +18,61 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/skit/analysis.py:147 +#: src/skit/analysis.py:162 #, python-format msgid "The parameter definitions for %(name)s have drifted from the script:" msgstr "%(name)s 的參數定義與腳本內容已漂移:" -#: src/skit/analysis.py:156 +#: src/skit/analysis.py:171 #, python-brace-format, python-format msgid "%(name)s is no longer read from the environment (its ${...:-default} was removed or overridden by a plain assignment) — your value would be silently ignored. Re-add or resync." msgstr "%(name)s 不再從環境變數讀取(其 ${...:-default} 已被移除,或被普通賦值覆蓋)——你設定的值會被默默忽略。請重新加入或執行 resync。" -#: src/skit/analysis.py:166 +#: src/skit/analysis.py:181 #, python-format msgid "%(name)s: injection target no longer exists (dropped from this run's form)" msgstr "%(name)s:找不到注入目標(已從本次表單剔除)" -#: src/skit/analysis.py:174 +#: src/skit/analysis.py:189 #, python-format msgid "%(name)s: type changed from %(old)s to %(new)s in the source (still injected — double-check the value)" msgstr "%(name)s:原始碼中的型別已從 %(old)s 變為 %(new)s(仍會注入,請確認值)" -#: src/skit/analysis.py:182 +#: src/skit/analysis.py:197 #, python-format msgid "%(name)s: its prompt no longer matches a unique input/read call; falling back to position (still injected — double-check this lands on the right question, especially if it's a secret)" msgstr "%(name)s:其提示文字已無法對應到唯一的 input/read 呼叫;改以位置對應(仍會注入——請再次確認其對應到正確的問題,尤其是機密參數)" -#: src/skit/analysis.py:190 +#: src/skit/analysis.py:205 #, python-format msgid "To refresh the definitions, run: skit params %(name)s --resync" msgstr "若要更新定義,請執行:skit params %(name)s --resync" -#: src/skit/analysis.py:204 +#: src/skit/analysis.py:219 #, python-format msgid "%(name)s isn't a managed parameter; skipped." msgstr "%(name)s 不是受管理的參數;已略過。" -#: src/skit/analysis.py:205 +#: src/skit/analysis.py:220 #, python-format msgid "Dropped %(name)s: it no longer exists in the script." msgstr "已剔除 %(name)s:腳本中已找不到。" -#: src/skit/analysis.py:206 +#: src/skit/analysis.py:221 #, python-format msgid "%(name)s is already managed; skipped." msgstr "%(name)s 已在管理中;已略過。" -#: src/skit/analysis.py:208 +#: src/skit/analysis.py:223 #, python-format msgid "%(name)s isn't a detectable parameter in the current script; skipped." msgstr "%(name)s 在當前腳本中偵測不到;已略過。" -#: src/skit/analysis.py:211 +#: src/skit/analysis.py:226 msgid "Could not parse the script (syntax error); resync skipped. Parameter definitions are unchanged." msgstr "無法解析腳本(語法錯誤);已略過重新同步。參數定義維持不變。" -#: src/skit/analysis.py:215 +#: src/skit/analysis.py:230 #, python-format msgid "%(name)s: re-anchored to its current position after its prompt stopped matching uniquely; double-check the prompt/secret assignment is still correct." msgstr "%(name)s:因提示文字無法唯一對應,已重新錨定到其目前位置;請再次確認提示文字/機密設定仍然正確。" @@ -129,19 +129,19 @@ msgstr "%(kind)s 條目不接受依賴套件——拿掉 --dep。" msgid "A Python constraint doesn't apply to %(kind)s scripts." msgstr "Python 約束不適用於 %(kind)s 腳本。" -#: src/skit/cli.py:390 src/skit/cli.py:775 src/skit/cli.py:1213 -#: src/skit/cli.py:2012 +#: src/skit/cli.py:390 src/skit/cli.py:763 src/skit/cli.py:1201 +#: src/skit/cli.py:2000 msgid "Name in skit" msgstr "在 skit 裡的名稱" -#: src/skit/cli.py:396 src/skit/cli.py:1483 src/skit/cli.py:2018 -#: src/skit/tui_add.py:245 +#: src/skit/cli.py:396 src/skit/cli.py:1471 src/skit/cli.py:2006 +#: src/skit/tui_add.py:246 msgid "Description (optional)" msgstr "描述(可留空)" #: src/skit/cli.py:420 src/skit/cli.py:428 src/skit/store.py:224 #: src/skit/store.py:355 src/skit/store.py:427 src/skit/store.py:639 -#: src/skit/tui_add.py:356 +#: src/skit/tui_add.py:357 #, python-format msgid "File not found: %(path)s" msgstr "找不到檔案:%(path)s" @@ -151,8 +151,8 @@ msgstr "找不到檔案:%(path)s" msgid "Not a file: %(path)s" msgstr "不是檔案:%(path)s" -#: src/skit/cli.py:457 src/skit/cli.py:1131 src/skit/cli.py:2646 -#: src/skit/tui_add.py:1415 +#: src/skit/cli.py:457 src/skit/cli.py:1119 src/skit/cli.py:2640 +#: src/skit/tui_add.py:1431 msgid " (secret)" msgstr " (機密)" @@ -170,7 +170,7 @@ msgstr "%(num)s. input() 第 %(ordinal)s 個:%(prompt)s%(secret)s" msgid "⚠ looks like a loop accumulator — probably not a parameter" msgstr "⚠ 看起來是迴圈的累加變數,多半不是參數" -#: src/skit/cli.py:491 src/skit/tui_add.py:1039 +#: src/skit/cli.py:491 src/skit/tui_add.py:1049 msgid "This script reads command-line arguments; the run form has an extra-arguments field for them." msgstr "這支腳本會吃命令列參數;執行表單裡有「額外參數」欄可以填。" @@ -179,13 +179,13 @@ msgstr "這支腳本會吃命令列參數;執行表單裡有「額外參數」 msgid "💡 %(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (skit edit %(script)s)." msgstr "💡 %(names)s 直接寫在程式碼裡,skit 沒辦法把它們變成表單欄位。想管理其中一個,先在腳本開頭給它取個名字,例如 OUTPUT = '…'(skit edit %(script)s)。" -#: src/skit/cli.py:521 src/skit/tui_add.py:981 +#: src/skit/cli.py:521 src/skit/tui_add.py:991 #, python-format msgid "✓ skit read this script's own arguments (%(count)s field). Running it opens a form — nothing to memorize." msgid_plural "✓ skit read this script's own arguments (%(count)s fields). Running it opens a form — nothing to memorize." msgstr[0] "✓ skit 看懂了這支腳本自己的參數(共 %(count)s 項)。執行時會開表單——不用背任何指令。" -#: src/skit/cli.py:531 src/skit/tui_add.py:996 +#: src/skit/cli.py:531 src/skit/tui_add.py:1006 #, python-format msgid "This script parses its own arguments (%(names)s); skit couldn't model them statically, so the run form offers an extra-arguments field." msgstr "這支腳本自己解析參數(%(names)s);skit 無法靜態讀懂它們,執行表單會提供「額外參數(原樣傳給腳本)」欄。" @@ -204,1720 +204,1738 @@ msgstr "哪些交給 skit 管理?(編號如 1,3 / all / none)" msgid "Reference mode never touches the original file, so parameter setup was skipped." msgstr "reference 模式絕不改動原檔;略過參數設定。" -#: src/skit/cli.py:719 +#: src/skit/cli.py:707 #, python-format msgid "The draft's #! names no interpreter skit knows — add it with: skit add %(path)s --kind " msgstr "草稿的 #! 指定了 skit 不認識的直譯器——請用以下指令加入:skit add %(path)s --kind <語言>" -#: src/skit/cli.py:732 +#: src/skit/cli.py:720 #, python-format msgid "--dep/--python are python flags, but the draft's shebang names %(kind)s — drop them, or keep the python shebang." msgstr "--dep/--python 是 python 專用旗標,但草稿的 shebang 指定的是 %(kind)s——拿掉它們,或改回 python shebang。" -#: src/skit/cli.py:760 +#: src/skit/cli.py:748 msgid "--edit opens your editor, which --no-input forbids — pipe the script in instead: skit add - -n NAME" msgstr "--edit 會開啟你的編輯器,而 --no-input 禁止這麼做——請改用管道把腳本傳進來:skit add - -n NAME" -#: src/skit/cli.py:768 +#: src/skit/cli.py:756 msgid "Writing a new script in an editor needs an interactive terminal." msgstr "用編輯器新建腳本需要互動式終端機。" -#: src/skit/cli.py:777 src/skit/cli.py:1215 src/skit/cli.py:3385 -#: src/skit/cli.py:3429 src/skit/config.py:620 src/skit/store.py:1135 -#: src/skit/tui_add.py:446 src/skit/tui_runner.py:160 +#: src/skit/cli.py:765 src/skit/cli.py:1203 src/skit/cli.py:3388 +#: src/skit/cli.py:3432 src/skit/config.py:620 src/skit/store.py:1133 +#: src/skit/tui_add.py:447 src/skit/tui_runner.py:160 msgid "A name is required." msgstr "必須提供名稱。" -#: src/skit/cli.py:788 src/skit/cli.py:1224 src/skit/store.py:707 +#: src/skit/cli.py:776 src/skit/cli.py:1212 src/skit/store.py:707 #, python-format msgid "The name %(name)s is already taken — pick another name." msgstr "名稱 %(name)s 已被使用——請換一個名稱。" -#: src/skit/cli.py:800 src/skit/cli.py:1239 +#: src/skit/cli.py:788 src/skit/cli.py:1227 msgid "Opening your editor…" msgstr "正在開啟編輯器…" -#: src/skit/cli.py:808 src/skit/cli.py:1109 src/skit/cli.py:1245 -#: src/skit/cli.py:2152 src/skit/editor.py:105 src/skit/langs/launch.py:712 -#: src/skit/store.py:445 src/skit/tui_add.py:531 src/skit/tui_add.py:747 -#: src/skit/tui_add.py:1079 src/skit/tui_add.py:1265 src/skit/tui_add.py:1543 -#: src/skit/tui_settings.py:364 +#: src/skit/cli.py:796 src/skit/cli.py:1097 src/skit/cli.py:1233 +#: src/skit/cli.py:2140 src/skit/editor.py:105 src/skit/langs/launch.py:712 +#: src/skit/store.py:445 src/skit/tui_add.py:532 src/skit/tui_add.py:749 +#: src/skit/tui_add.py:1089 src/skit/tui_add.py:1281 src/skit/tui_add.py:1559 +#: src/skit/tui_settings.py:359 #, python-format msgid "Can't read %(path)s: %(error)s" msgstr "無法讀取 %(path)s:%(error)s" -#: src/skit/cli.py:813 +#: src/skit/cli.py:801 msgid "Nothing was written, so no script was added." msgstr "沒有寫入任何內容,因此沒有加入腳本。" -#: src/skit/cli.py:883 src/skit/cli.py:936 src/skit/cli.py:1285 +#: src/skit/cli.py:871 src/skit/cli.py:924 src/skit/cli.py:1273 msgid "Reading the script from stdin needs an explicit --name." msgstr "從 stdin 讀腳本必須用 --name 指定名稱。" -#: src/skit/cli.py:893 src/skit/cli.py:943 src/skit/cli.py:1305 +#: src/skit/cli.py:881 src/skit/cli.py:931 src/skit/cli.py:1293 msgid "Nothing arrived on stdin, so there is nothing to add." msgstr "stdin 沒有收到任何內容,沒有東西可加入。" -#: src/skit/cli.py:991 +#: src/skit/cli.py:979 msgid "# New prompt" msgstr "# 新提示詞" -#: src/skit/cli.py:1007 +#: src/skit/cli.py:995 msgid "Custom agents: skit runner add mycli -- mycli run {{prompt}} (see skit runner --help)" msgstr "自訂 agent:skit runner add mycli -- mycli run {{prompt}}(詳見 skit runner --help)" -#: src/skit/cli.py:1031 src/skit/cli.py:3482 +#: src/skit/cli.py:1019 src/skit/cli.py:3485 #, python-format msgid "Unknown runner: %(runner)s. Configured runners: %(names)s" msgstr "未知的執行器:%(runner)s。已設定的執行器:%(names)s" -#: src/skit/cli.py:1046 +#: src/skit/cli.py:1034 #, python-format msgid "The draft is no longer at %(path)s, so nothing was added." msgstr "草稿已不在 %(path)s,因此沒有加入任何內容。" -#: src/skit/cli.py:1050 +#: src/skit/cli.py:1038 #, python-format msgid "Your draft was kept at %(path)s — fix the problem and add it with: skit add %(path)s" msgstr "你的草稿保留在 %(path)s——修正問題後用以下指令加入:skit add %(path)s" -#: src/skit/cli.py:1054 src/skit/tui_add.py:545 src/skit/tui_add.py:571 +#: src/skit/cli.py:1042 src/skit/tui_add.py:546 src/skit/tui_add.py:572 #, python-format msgid "Your draft was kept at %(path)s" msgstr "你的草稿保留在 %(path)s" -#: src/skit/cli.py:1071 +#: src/skit/cli.py:1059 msgid "- = no pin (the run form asks each time)" msgstr "- = 不釘選(每次在執行表單選擇)" -#: src/skit/cli.py:1074 src/skit/cli.py:2887 +#: src/skit/cli.py:1062 src/skit/cli.py:2881 msgid "Run this prompt with which agent?" msgstr "用哪個執行器執行這個提示詞?" -#: src/skit/cli.py:1129 src/skit/cli.py:2644 +#: src/skit/cli.py:1117 src/skit/cli.py:2638 msgid "Detected placeholders (each becomes a form field):" msgstr "偵測到的佔位符(每個都會成為表單欄位):" -#: src/skit/cli.py:1136 src/skit/cli.py:2651 src/skit/tui_add.py:295 -#: src/skit/tui_add.py:1423 src/skit/tui_settings.py:704 +#: src/skit/cli.py:1124 src/skit/cli.py:2645 src/skit/tui_add.py:296 +#: src/skit/tui_add.py:1439 src/skit/tui_settings.py:699 #, python-format msgid "…and %(count)s more" msgstr "……還有 %(count)s 個" -#: src/skit/cli.py:1140 +#: src/skit/cli.py:1128 msgid "Manage which? (all / none / off / numbers like 1,3)" msgstr "要管理哪些?(all / none / off / 或編號如 1,3)" -#: src/skit/cli.py:1169 +#: src/skit/cli.py:1157 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written (turn it on with: skit params %(name)s --interpolate)" msgstr "變量插入已關閉——內文會原封不動送達 agent(開啟:skit params %(name)s --interpolate)" -#: src/skit/cli.py:1174 +#: src/skit/cli.py:1162 #, python-format msgid "Detected %(count)s placeholders — too many to manage automatically, so none were. Manage the ones you need with: skit params %(name)s --add NAME, or turn insertion off with --no-interpolate." msgstr "偵測到 %(count)s 個佔位符——數量太多,未自動管理任何一個。需要的請用 skit params %(name)s --add 名稱 管理,或用 --no-interpolate 關閉插值。" -#: src/skit/cli.py:1178 +#: src/skit/cli.py:1166 #, python-format msgid "Runs with %(runner)s (change it with: skit params %(name)s --runner NAME)" msgstr "以 %(runner)s 執行(更改:skit params %(name)s --runner NAME)" -#: src/skit/cli.py:1206 +#: src/skit/cli.py:1194 msgid "--prompt with no path opens your editor, which --no-input forbids — pipe the body in instead: skit add - --prompt -n NAME" msgstr "--prompt 未帶路徑時會開啟你的編輯器,而 --no-input 禁止這麼做——請改用管道把內文傳進來:skit add - --prompt -n NAME" -#: src/skit/cli.py:1250 +#: src/skit/cli.py:1238 msgid "Nothing was written, so no prompt was added." msgstr "沒有寫入任何內容,因此沒有加入提示詞。" -#: src/skit/cli.py:1356 +#: src/skit/cli.py:1344 #, python-format msgid "Unknown kind: %(kind)s. Choose from: %(kinds)s" msgstr "未知類型:%(kind)s。可選:%(kinds)s" -#: src/skit/cli.py:1376 src/skit/tui_add.py:120 +#: src/skit/cli.py:1364 src/skit/tui_add.py:121 #, python-format msgid "The #! in %(file)s names no interpreter skit knows. What is it?" msgstr "%(file)s 的 #! 指定了 skit 不認識的直譯器。這是什麼?" -#: src/skit/cli.py:1378 src/skit/tui_add.py:122 +#: src/skit/cli.py:1366 src/skit/tui_add.py:123 #, python-format msgid "What is %(file)s? skit can't tell from the name." msgstr "%(file)s 是什麼?skit 無法從名稱判斷。" -#: src/skit/cli.py:1383 +#: src/skit/cli.py:1371 msgid "- = cancel" msgstr "- = 取消" -#: src/skit/cli.py:1385 src/skit/cli.py:1465 +#: src/skit/cli.py:1373 src/skit/cli.py:1453 msgid "Which one?" msgstr "選哪個?" -#: src/skit/cli.py:1399 +#: src/skit/cli.py:1387 #, python-format msgid "Detected parameters: %(names)s (the run form asks for them; your last values are remembered)" msgstr "偵測到參數:%(names)s(執行表單會詢問;會記住你上次的值)" -#: src/skit/cli.py:1459 +#: src/skit/cli.py:1447 msgid "What would you like to add?" msgstr "你想加入什麼?" -#: src/skit/cli.py:1460 +#: src/skit/cli.py:1448 msgid "A file you already have — a script, program, or prompt" msgstr "你已有的檔案——腳本、程式或提示詞" -#: src/skit/cli.py:1461 +#: src/skit/cli.py:1449 msgid "A new script, written in your editor" msgstr "一支新腳本,在你的編輯器裡寫" -#: src/skit/cli.py:1462 +#: src/skit/cli.py:1450 msgid "A new AI-agent prompt, written in your editor" msgstr "一條給 AI agent 的新提示詞,在你的編輯器裡寫" -#: src/skit/cli.py:1463 +#: src/skit/cli.py:1451 #, python-brace-format msgid "A command template (e.g. ffmpeg -i {input})" msgstr "一條命令模板(例如 ffmpeg -i {input})" -#: src/skit/cli.py:1476 +#: src/skit/cli.py:1464 msgid "Command template" msgstr "命令模板" -#: src/skit/cli.py:1479 src/skit/tui_add.py:242 +#: src/skit/cli.py:1467 src/skit/tui_add.py:243 msgid "Name for the command" msgstr "命令的名稱" -#: src/skit/cli.py:1489 +#: src/skit/cli.py:1477 msgid "Path to the file" msgstr "檔案路徑" -#: src/skit/cli.py:1496 +#: src/skit/cli.py:1484 msgid "Add a script, executable, prompt, or command to skit." msgstr "把腳本、可執行檔、提示詞或命令加入 skit。" -#: src/skit/cli.py:1498 +#: src/skit/cli.py:1486 #, python-brace-format msgid "Examples: skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" msgstr "例:skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" -#: src/skit/cli.py:1505 +#: src/skit/cli.py:1493 msgid "Path to a script, executable, or prompt, or '-' to read one from stdin (omit it in a terminal and skit asks what to add)" msgstr "腳本、可執行檔或提示詞的路徑;「-」表示從 stdin 讀取(在終端機裡省略它,skit 會問你要加入什麼)" -#: src/skit/cli.py:1510 +#: src/skit/cli.py:1498 msgid "Name / alias (defaults to the file name)" msgstr "名稱 / alias(預設用檔名)" -#: src/skit/cli.py:1516 +#: src/skit/cli.py:1504 msgid "Description (inferred from the source when possible)" msgstr "說明(可行時從來源推斷)" -#: src/skit/cli.py:1519 +#: src/skit/cli.py:1507 msgid "Write a brand-new script in your editor, then add it" msgstr "在編輯器裡新寫一支腳本,再加入 skit" -#: src/skit/cli.py:1524 +#: src/skit/cli.py:1512 msgid "Reference mode: link to the original file instead of copying it" msgstr "reference 模式:不複製,連結原檔" -#: src/skit/cli.py:1529 +#: src/skit/cli.py:1517 msgid "Force the executable kind (normally inferred from the file itself)" msgstr "強制當作可執行檔(一般會自動判斷)" -#: src/skit/cli.py:1534 +#: src/skit/cli.py:1522 msgid "Force the language kind (e.g. shell, js) for an extensionless file" msgstr "強制指定語言類型(如 shell、js),用於沒有副檔名的檔案" -#: src/skit/cli.py:1537 +#: src/skit/cli.py:1525 #, python-brace-format msgid "Register a command template, e.g. --cmd 'ffmpeg -i {input}'" msgstr "登記命令模板,如 --cmd 'ffmpeg -i {input}'" -#: src/skit/cli.py:1543 +#: src/skit/cli.py:1531 msgid "Add the file as a prompt for an AI agent (with no path: draft one in your editor)" msgstr "將檔案加入為 AI agent 的提示詞(不給路徑則在編輯器中撰寫新的)" -#: src/skit/cli.py:1549 +#: src/skit/cli.py:1537 msgid "Pin the agent a prompt entry runs with (see skit runner list)" msgstr "釘選提示詞項目使用的執行器(見 skit runner list)" -#: src/skit/cli.py:1555 +#: src/skit/cli.py:1543 msgid "Prompt only: no variable insertion at all — the body travels exactly as written" msgstr "僅提示詞:完全不做變量插入——內文原封不動送達" -#: src/skit/cli.py:1561 +#: src/skit/cli.py:1549 msgid "A dependency (repeat for more; skips the interactive question)" msgstr "一項依賴(可重複;會跳過互動詢問)" -#: src/skit/cli.py:1564 src/skit/cli.py:4884 +#: src/skit/cli.py:1552 src/skit/cli.py:4912 msgid "Python version constraint, e.g. \">=3.11\"" msgstr "Python 版本約束,如 \">=3.11\"" -#: src/skit/cli.py:1567 +#: src/skit/cli.py:1555 msgid "Never prompt; accept the detected suggestions" msgstr "永不詢問;直接採用偵測到的建議" -#: src/skit/cli.py:1576 +#: src/skit/cli.py:1564 msgid "--prompt names the kind outright — drop --edit/--exe/--kind/--cmd." msgstr "--prompt 已直接指定類型——請去掉 --edit/--exe/--kind/--cmd。" -#: src/skit/cli.py:1589 +#: src/skit/cli.py:1577 msgid "stdin ('-')" msgstr "stdin('-')" -#: src/skit/cli.py:1590 +#: src/skit/cli.py:1578 msgid "a file path" msgstr "檔案路徑" -#: src/skit/cli.py:1598 +#: src/skit/cli.py:1586 #, python-format msgid "%(flags)s each pick a different way to add — use exactly one (nothing was added)." msgstr "%(flags)s 各自代表一種不同的加入方式——請只用其中一種(未加入任何內容)。" -#: src/skit/cli.py:1639 +#: src/skit/cli.py:1627 msgid "a --cmd template takes only --name/--description" msgstr "--cmd 樣板只接受 --name/--description" -#: src/skit/cli.py:1642 +#: src/skit/cli.py:1630 msgid "stdin authors a brand-new copy, and --ref/--exe need an existing file" msgstr "stdin 會撰寫一份全新副本,而 --ref/--exe 需要現成的檔案" -#: src/skit/cli.py:1644 +#: src/skit/cli.py:1632 msgid "--edit drafts a fresh script: its kind comes from the shebang you write (e.g. #!/usr/bin/env bash), --ref/--exe need an existing file, and a prompt is drafted with skit add --prompt" msgstr "--edit 會草擬一支全新腳本:它的類型取自你寫的 shebang(例如 #!/usr/bin/env bash),--ref/--exe 需要現成的檔案,而提示詞要用 skit add --prompt 草擬" -#: src/skit/cli.py:1649 +#: src/skit/cli.py:1637 msgid "a drafted prompt takes only --name/--description/--runner/--no-interpolate" msgstr "草稿提示詞只接受 --name/--description/--runner/--no-interpolate" -#: src/skit/cli.py:1657 +#: src/skit/cli.py:1645 #, python-format msgid "%(flags)s can't apply here — %(hint)s (nothing was added)." msgstr "%(flags)s 在這裡無法套用——%(hint)s(未加入任何內容)。" -#: src/skit/cli.py:1670 src/skit/cli.py:1988 +#: src/skit/cli.py:1658 src/skit/cli.py:1976 msgid "--no-interpolate only applies to prompt entries — add one with --prompt." msgstr "--no-interpolate 只適用於提示詞項目——用 --prompt 加入一個。" -#: src/skit/cli.py:1675 src/skit/cli.py:1983 +#: src/skit/cli.py:1663 src/skit/cli.py:1971 msgid "--runner only applies to prompt entries — add one with --prompt." msgstr "--runner 只適用於提示詞項目——用 --prompt 加入一個。" -#: src/skit/cli.py:1690 +#: src/skit/cli.py:1678 msgid "--exe needs an existing program on disk — stdin and the editor author scripts." msgstr "--exe 需要磁碟上既有的程式——stdin 與編輯器用來撰寫腳本。" -#: src/skit/cli.py:1699 +#: src/skit/cli.py:1687 msgid "--dep/--python are script flags — a prompt has no dependencies." msgstr "--dep/--python 是腳本旗標——提示詞沒有依賴。" -#: src/skit/cli.py:1708 +#: src/skit/cli.py:1696 msgid "--runner/--no-interpolate only apply to prompt entries — pipe one with --prompt." msgstr "--runner/--no-interpolate 只適用於提示詞項目——用 --prompt 以管道傳入一個。" -#: src/skit/cli.py:1726 +#: src/skit/cli.py:1714 msgid "The piped text's #! names no interpreter skit knows — pass --kind to choose one." msgstr "管道文字的 #! 指定了 skit 不認識的直譯器——請用 --kind <語言> 指定一個。" -#: src/skit/cli.py:1739 +#: src/skit/cli.py:1727 #, python-format msgid "--dep/--python don't apply to a %(kind)s entry (piped text's kind)." msgstr "--dep/--python 不適用於 %(kind)s 項目(管道文字的種類)。" -#: src/skit/cli.py:1759 +#: src/skit/cli.py:1747 msgid "A --cmd entry needs a --name" msgstr "--cmd 條目必須指定 --name" -#: src/skit/cli.py:1779 +#: src/skit/cli.py:1767 msgid "Provide a source path — or pipe the text in (skit add - -n NAME; add --prompt for an AI-agent prompt), or register a command template with --cmd." msgstr "請提供來源路徑——或把文字管線進來(skit add - -n NAME;AI-agent 提示詞再加 --prompt),或用 --cmd 登記一條命令模板。" -#: src/skit/cli.py:1811 +#: src/skit/cli.py:1799 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …), or pick a lane outright with %(lanes)s (nothing was added)." msgstr "%(flags)s 需要一個來源——在同一條命令裡給出路徑(skit add PATH …),或直接用 %(lanes)s 選定一條加入方式(沒有加入任何東西)。" -#: src/skit/cli.py:1818 +#: src/skit/cli.py:1806 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …) (nothing was added)." msgstr "%(flags)s 需要一個來源——在同一條命令裡給出路徑(skit add PATH …)(沒有加入任何東西)。" -#: src/skit/cli.py:1840 +#: src/skit/cli.py:1828 msgid "Use --kind or --exe, not both." msgstr "--kind 與 --exe 只能擇一。" -#: src/skit/cli.py:1868 +#: src/skit/cli.py:1856 #, python-format msgid "%(file)s is a directory — pass --exe to add it as a program that runs directly." msgstr "%(file)s 是一個目錄——加 --exe 可把它作為直接執行的程式加入。" -#: src/skit/cli.py:1876 +#: src/skit/cli.py:1864 #, python-format msgid "%(file)s is a directory. Add it as a program that runs directly?" msgstr "%(file)s 是一個目錄。要當作直接執行的程式加入嗎?" -#: src/skit/cli.py:1910 +#: src/skit/cli.py:1898 #, python-format msgid "%(file)s is one of skit's own kept drafts — a resumed draft is always added as a copy (and consumed on success), which a reference or program entry can't be. Drop %(flags)s." msgstr "%(file)s 是 skit 自己保留的草稿——恢復草稿一律以副本加入(成功後即消耗),而 reference 或程式項目做不到這點。請拿掉 %(flags)s。" -#: src/skit/cli.py:1917 +#: src/skit/cli.py:1905 #, python-format msgid "%(file)s is one of skit's own kept drafts, and a draft is always added as a script or prompt copy — pass --kind to name its language." msgstr "%(file)s 是 skit 自己保留的草稿,而草稿一律以腳本或提示詞副本加入——請用 --kind <語言> 指定它的語言。" -#: src/skit/cli.py:1941 +#: src/skit/cli.py:1929 #, python-format msgid "%(file)s looks like a prompt. Add it as one?" msgstr "%(file)s 看起來是提示詞。要當作提示詞加入嗎?" -#: src/skit/cli.py:2034 +#: src/skit/cli.py:2022 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one." msgstr "%(file)s 的 #! 指定了 skit 不認識的直譯器——請用 --kind <語言> 指定一個。" -#: src/skit/cli.py:2039 +#: src/skit/cli.py:2027 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one, or --exe to run it directly." msgstr "%(file)s 的 #! 指定了 skit 不認識的直譯器——請用 --kind <語言> 指定一個,或用 --exe 直接執行。" -#: src/skit/cli.py:2051 +#: src/skit/cli.py:2039 #, python-format msgid "%(file)s is a kept draft skit can't classify — pass --kind to add it as a script, or --prompt for an AI-agent prompt." msgstr "%(file)s 是 skit 無法分類的保留草稿——請用 --kind <語言> 將它加入為腳本,或用 --prompt 加入為 AI agent 提示詞。" -#: src/skit/cli.py:2060 +#: src/skit/cli.py:2048 #, python-format msgid "%(file)s isn't a script or an executable — pass --kind for an extensionless script, --prompt for an AI-agent prompt, --exe for a program, or --cmd for a command template." msgstr "%(file)s 不是腳本也不是可執行檔——沒有副檔名的腳本請加 --kind ,AI agent 提示詞請加 --prompt,程式請加 --exe,命令模板請用 --cmd。" -#: src/skit/cli.py:2210 +#: src/skit/cli.py:2198 #, python-format msgid "(%(mode)s mode)" msgstr "(%(mode)s 模式)" -#: src/skit/cli.py:2215 +#: src/skit/cli.py:2203 #, python-format msgid "Added: %(name)s" msgstr "已加入:%(name)s" -#: src/skit/cli.py:2219 +#: src/skit/cli.py:2207 #, python-format msgid "Description: %(desc)s" msgstr "描述:%(desc)s" -#: src/skit/cli.py:2223 src/skit/cli.py:2381 +#: src/skit/cli.py:2211 src/skit/cli.py:2369 #, python-format msgid "Dependencies: %(deps)s" msgstr "依賴:%(deps)s" -#: src/skit/cli.py:2227 +#: src/skit/cli.py:2215 #, python-format msgid "Managed parameters: %(names)s" msgstr "管理中的參數:%(names)s" -#: src/skit/cli.py:2229 src/skit/cli.py:2422 +#: src/skit/cli.py:2217 src/skit/cli.py:2410 #, python-format msgid "Run it: skit run %(name)s" msgstr "執行:skit run %(name)s" -#: src/skit/cli.py:2232 +#: src/skit/cli.py:2220 #, python-format msgid "Secret parameter values are never saved by skit: %(names)s" msgstr "skit 永遠不會儲存機密參數的值:%(names)s" -#: src/skit/cli.py:2238 +#: src/skit/cli.py:2226 msgid "When this prompt runs, the selected agent receives those values as plaintext and may log or sync them." msgstr "執行此提示詞時,所選 agent 會以明文收到這些值,並可能記錄或同步它們。" -#: src/skit/cli.py:2250 +#: src/skit/cli.py:2238 msgid "List every registered entry." msgstr "列出所有已登記的條目。" -#: src/skit/cli.py:2252 src/skit/cli.py:2463 src/skit/cli.py:3247 -#: src/skit/cli.py:3624 src/skit/cli.py:4896 src/skit/cli.py:5123 -#: src/skit/cli.py:5467 +#: src/skit/cli.py:2240 src/skit/cli.py:2451 src/skit/cli.py:3250 +#: src/skit/cli.py:3627 src/skit/cli.py:4924 src/skit/cli.py:5151 +#: src/skit/cli.py:5495 msgid "Output as JSON" msgstr "以 JSON 輸出" -#: src/skit/cli.py:2275 +#: src/skit/cli.py:2263 msgid "No entries yet. Add one with: skit add " msgstr "還沒有任何條目。用 skit add 加入一個。" -#: src/skit/cli.py:2280 src/skit/tui.py:370 src/skit/tui.py:1059 -#: src/skit/tui_add.py:638 src/skit/tui_add.py:855 src/skit/tui_add.py:1328 +#: src/skit/cli.py:2268 src/skit/tui.py:377 src/skit/tui.py:1088 +#: src/skit/tui_add.py:639 src/skit/tui_add.py:865 src/skit/tui_add.py:1344 msgid "Name" msgstr "名稱" -#: src/skit/cli.py:2281 src/skit/cli.py:3942 src/skit/tui.py:370 -#: src/skit/tui.py:1059 +#: src/skit/cli.py:2269 src/skit/cli.py:3974 src/skit/tui.py:377 +#: src/skit/tui.py:1088 msgid "Kind" msgstr "類型" -#: src/skit/cli.py:2282 src/skit/tui_add.py:640 src/skit/tui_add.py:857 -#: src/skit/tui_add.py:1333 +#: src/skit/cli.py:2270 src/skit/tui_add.py:641 src/skit/tui_add.py:867 +#: src/skit/tui_add.py:1349 msgid "Description" msgstr "說明" -#: src/skit/cli.py:2307 src/skit/cli.py:2308 src/skit/cli.py:2448 -#: src/skit/cli.py:3689 src/skit/cli.py:3690 +#: src/skit/cli.py:2295 src/skit/cli.py:2296 src/skit/cli.py:2436 +#: src/skit/cli.py:3716 src/skit/cli.py:3717 msgid "yes" msgstr "是" -#: src/skit/cli.py:2363 +#: src/skit/cli.py:2351 #, python-format msgid "Source: %(path)s" msgstr "來源:%(path)s" -#: src/skit/cli.py:2366 +#: src/skit/cli.py:2354 #, python-format msgid "Working directory: %(dir)s" msgstr "工作目錄:%(dir)s" -#: src/skit/cli.py:2370 +#: src/skit/cli.py:2358 #, python-format msgid "Interpreter: %(program)s" msgstr "直譯器:%(program)s" -#: src/skit/cli.py:2385 src/skit/cli.py:4862 +#: src/skit/cli.py:2373 src/skit/cli.py:4890 #, python-format msgid "Python constraint: %(python)s" msgstr "Python 版本約束:%(python)s" -#: src/skit/cli.py:2389 +#: src/skit/cli.py:2377 #, python-format msgid "Needs: %(needs)s" msgstr "所需命令:%(needs)s" -#: src/skit/cli.py:2393 +#: src/skit/cli.py:2381 #, python-format msgid "Command template: %(template)s" msgstr "命令模板:%(template)s" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 #, python-format msgid "Runner: %(runner)s" msgstr "執行器:%(runner)s" -#: src/skit/cli.py:2397 +#: src/skit/cli.py:2385 msgid "(asks at run time)" msgstr "(執行時詢問)" -#: src/skit/cli.py:2400 +#: src/skit/cli.py:2388 msgid "Variable insertion: off (the body travels as written)" msgstr "變量插入:關閉(內文原樣送達)" -#: src/skit/cli.py:2404 src/skit/cli.py:3067 +#: src/skit/cli.py:2392 src/skit/cli.py:3061 msgid "skit could not model this script's own arguments; pass them after -- instead." msgstr "skit 無法讀懂這支腳本自己的參數宣告;請把參數放在 -- 之後直接傳入。" -#: src/skit/cli.py:2410 +#: src/skit/cli.py:2398 msgid "No form fields — arguments after -- go to the selected agent." msgstr "沒有表單欄位——接在 -- 之後的參數會傳給所選 agent。" -#: src/skit/cli.py:2412 +#: src/skit/cli.py:2400 msgid "No form fields — arguments after -- are appended to the command." msgstr "沒有表單欄位——接在 -- 之後的參數會附加到指令末尾。" -#: src/skit/cli.py:2415 +#: src/skit/cli.py:2403 msgid "No form fields — arguments after -- pass straight through to the script." msgstr "沒有表單欄位——接在 -- 之後的參數會透傳給腳本。" -#: src/skit/cli.py:2420 +#: src/skit/cli.py:2408 #, python-format msgid "Presets: %(names)s" msgstr "參數組合:%(names)s" -#: src/skit/cli.py:2429 src/skit/cli.py:3728 src/skit/cli.py:3941 +#: src/skit/cli.py:2417 src/skit/cli.py:3755 src/skit/cli.py:3973 msgid "Parameter" msgstr "參數" -#: src/skit/cli.py:2430 src/skit/cli.py:3730 src/skit/cli.py:3943 +#: src/skit/cli.py:2418 src/skit/cli.py:3757 src/skit/cli.py:3975 msgid "Type" msgstr "型別" -#: src/skit/cli.py:2431 +#: src/skit/cli.py:2419 msgid "Required" msgstr "必填" -#: src/skit/cli.py:2432 src/skit/cli.py:3731 src/skit/cli.py:3944 +#: src/skit/cli.py:2420 src/skit/cli.py:3758 src/skit/cli.py:3976 msgid "Default" msgstr "預設值" -#: src/skit/cli.py:2433 +#: src/skit/cli.py:2421 msgid "Choices" msgstr "可選值" -#: src/skit/cli.py:2434 src/skit/cli.py:3732 src/skit/cli.py:3945 +#: src/skit/cli.py:2422 src/skit/cli.py:3759 src/skit/cli.py:3977 msgid "Secret" msgstr "機密" -#: src/skit/cli.py:2435 src/skit/tui.py:340 src/skit/tui.py:486 +#: src/skit/cli.py:2423 src/skit/tui.py:343 src/skit/tui.py:493 msgid "Help" msgstr "說明" -#: src/skit/cli.py:2440 src/skit/cli.py:3696 src/skit/cli.py:3704 -#: src/skit/cli.py:3714 src/skit/cli.py:3949 src/skit/cli.py:3954 +#: src/skit/cli.py:2428 src/skit/cli.py:3723 src/skit/cli.py:3731 +#: src/skit/cli.py:3741 src/skit/cli.py:3981 src/skit/cli.py:3986 msgid "•••" msgstr "•••" -#: src/skit/cli.py:2458 +#: src/skit/cli.py:2446 msgid "Show everything about one entry: metadata, dependencies, parameters, presets." msgstr "顯示一個條目的全部資訊:中繼資料、依賴、參數和參數組合。" -#: src/skit/cli.py:2459 +#: src/skit/cli.py:2447 msgid "Examples: skit show resize · skit show resize --json" msgstr "例:skit show resize · skit show resize --json" -#: src/skit/cli.py:2520 +#: src/skit/cli.py:2508 msgid "Remove a registered entry (an original source file is left untouched)." msgstr "移除一個已登記的條目(原始來源檔案不受影響)。" -#: src/skit/cli.py:2523 src/skit/cli.py:3421 +#: src/skit/cli.py:2511 src/skit/cli.py:3424 src/skit/cli.py:3655 msgid "Skip confirmation" msgstr "跳過確認" -#: src/skit/cli.py:2533 src/skit/tui.py:149 +#: src/skit/cli.py:2512 src/skit/cli.py:3425 src/skit/cli.py:3656 +msgid "Never prompt" +msgstr "絕不提示" + +#: src/skit/cli.py:2523 +msgid "Confirmation is required; pass --yes to remove the entry." +msgstr "需要確認;請傳 --yes 以移除條目。" + +#: src/skit/cli.py:2527 src/skit/tui.py:149 #, python-format msgid "Remove \"%(name)s\"?" msgstr "移除「%(name)s」?" -#: src/skit/cli.py:2535 +#: src/skit/cli.py:2529 #, python-format msgid "Remove \"%(name)s\"? Your original file will not be deleted." msgstr "移除「%(name)s」?你的原始檔不會被刪除。" -#: src/skit/cli.py:2546 +#: src/skit/cli.py:2540 #, python-format msgid "Removed: %(name)s" msgstr "已移除:%(name)s" -#: src/skit/cli.py:2549 +#: src/skit/cli.py:2543 msgid "Rename an entry (presets, remembered values and history survive)." msgstr "重新命名條目(參數組合、記住的值與歷史都會保留)。" -#: src/skit/cli.py:2552 +#: src/skit/cli.py:2546 msgid "The new name" msgstr "新名稱" -#: src/skit/cli.py:2561 +#: src/skit/cli.py:2555 #, python-format msgid "Renamed to %(name)s." msgstr "已重新命名為 %(name)s。" -#: src/skit/cli.py:2565 +#: src/skit/cli.py:2559 msgid "Set an entry's description (shown in the Library and skit list)." msgstr "設定條目的說明(顯示在工具庫和 skit list 中)。" -#: src/skit/cli.py:2568 +#: src/skit/cli.py:2562 msgid "The description (empty text clears it)" msgstr "說明(留空即清除)" -#: src/skit/cli.py:2578 +#: src/skit/cli.py:2572 #, python-format msgid "Description updated for %(name)s." msgstr "已更新 %(name)s 的說明。" -#: src/skit/cli.py:2582 +#: src/skit/cli.py:2576 #, python-format msgid "Description cleared for %(name)s." msgstr "已清除 %(name)s 的說明。" -#: src/skit/cli.py:2590 +#: src/skit/cli.py:2584 #, python-format msgid "No editable entry named %(name)s." msgstr "找不到名為 %(name)s 的可編輯條目。" -#: src/skit/cli.py:2594 +#: src/skit/cli.py:2588 #, python-format msgid "No editable entry named \"%(name)s\". Create a script now?" msgstr "找不到名為「%(name)s」的可編輯條目。要現在建立腳本嗎?" -#: src/skit/cli.py:2622 src/skit/cli.py:3812 +#: src/skit/cli.py:2616 src/skit/cli.py:3839 #, python-format msgid "Detected but not yet managed: %(names)s … and %(count)d more candidate (use --add to manage them)" msgid_plural "Detected but not yet managed: %(names)s … and %(count)d more candidates (use --add to manage them)" msgstr[0] "偵測到但尚未管理:%(names)s……另有 %(count)d 個(用 --add 管理)" -#: src/skit/cli.py:2630 src/skit/cli.py:3820 +#: src/skit/cli.py:2624 src/skit/cli.py:3847 #, python-format msgid "Detected but not yet managed: %(names)s (use --add to manage them)" msgstr "偵測到但尚未管理:%(names)s(用 --add 管理)" -#: src/skit/cli.py:2655 +#: src/skit/cli.py:2649 msgid "Manage which? (all / none / numbers like 1,3)" msgstr "要管理哪些?(all / none / 或編號如 1,3)" -#: src/skit/cli.py:2671 src/skit/tui.py:930 +#: src/skit/cli.py:2665 src/skit/tui.py:959 #, python-format msgid "Now managed: %(names)s" msgstr "已加入管理:%(names)s" -#: src/skit/cli.py:2677 +#: src/skit/cli.py:2671 msgid "Open a script or prompt source in your editor (offers to create a script if the name is new)." msgstr "在編輯器中開啟腳本或提示詞來源(名稱不存在時可建立腳本)。" -#: src/skit/cli.py:2679 +#: src/skit/cli.py:2673 msgid "Example: skit edit resize" msgstr "例:skit edit resize" -#: src/skit/cli.py:2691 src/skit/tui.py:884 +#: src/skit/cli.py:2685 src/skit/tui.py:913 #, python-format msgid "%(name)s has no editable source (programs and command templates run as-is)." msgstr "%(name)s 沒有可編輯的原始碼(程式與命令模板按原樣執行)。" -#: src/skit/cli.py:2699 +#: src/skit/cli.py:2693 #, python-format msgid "%(name)s: the referenced source file is gone: %(path)s" msgstr "%(name)s:reference 原檔已消失:%(path)s" -#: src/skit/cli.py:2704 +#: src/skit/cli.py:2698 #, python-format msgid "Editing the original file (reference mode): %(path)s" msgstr "正在編輯原始檔案(reference 模式):%(path)s" -#: src/skit/cli.py:2711 src/skit/cli.py:4484 src/skit/cli.py:4626 +#: src/skit/cli.py:2705 src/skit/cli.py:4520 src/skit/cli.py:4652 #, python-format msgid "%(name)s has no stored copy to edit." msgstr "%(name)s 沒有可編輯的副本。" -#: src/skit/cli.py:2718 +#: src/skit/cli.py:2712 #, python-format msgid "Saved %(name)s." msgstr "已儲存 %(name)s。" -#: src/skit/cli.py:2726 +#: src/skit/cli.py:2720 #, python-format msgid "skit reconciles parameter drift at run time; review managed parameters with: skit params %(name)s" msgstr "skit 會在執行時自動校對參數差異;可用 skit params %(name)s 檢視管理中的參數" -#: src/skit/cli.py:2742 src/skit/cli.py:3666 +#: src/skit/cli.py:2736 src/skit/cli.py:3693 #, python-format msgid "Unknown preset \"%(preset)s\". Available: %(presets)s" msgstr "沒有名為「%(preset)s」的參數組合。現有:%(presets)s" -#: src/skit/cli.py:2772 +#: src/skit/cli.py:2766 #, python-format msgid "Malformed --set (expected NAME=VALUE): %(items)s" msgstr "--set 格式錯誤(應為 NAME=VALUE):%(items)s" -#: src/skit/cli.py:2782 +#: src/skit/cli.py:2776 #, python-format msgid "Unknown parameter for --set: %(names)s. This entry's parameters: %(valid)s" msgstr "--set 指定了未知參數:%(names)s。此條目的參數:%(valid)s" -#: src/skit/cli.py:2833 +#: src/skit/cli.py:2827 #, python-format msgid "Parameters for %(name)s (press Enter to keep the value shown):" msgstr "%(name)s 的參數(按 Enter 沿用顯示的值):" -#: src/skit/cli.py:2876 src/skit/cli.py:4378 +#: src/skit/cli.py:2870 src/skit/cli.py:4410 msgid "--runner only applies to prompt entries." msgstr "--runner 只適用於提示詞項目。" -#: src/skit/cli.py:2896 src/skit/cli.py:3280 src/skit/cli.py:3312 +#: src/skit/cli.py:2890 src/skit/cli.py:3283 src/skit/cli.py:3315 #: src/skit/langs/launch.py:663 msgid "No agents are configured. Add one with: skit runner add mycli -- mycli run {{prompt}}" msgstr "尚未設定任何執行器。請新增一個:skit runner add mycli -- mycli run {{prompt}}" -#: src/skit/cli.py:2904 src/skit/langs/launch.py:669 +#: src/skit/cli.py:2898 src/skit/langs/launch.py:669 #, python-format msgid "No runner selected for %(name)s. Pass --runner NAME, or pin one with: skit params %(name)s --runner NAME" msgstr "%(name)s 尚未選擇執行器。請帶上 --runner NAME,或釘選一個:skit params %(name)s --runner NAME" -#: src/skit/cli.py:2914 src/skit/cli.py:3106 src/skit/cli.py:4385 +#: src/skit/cli.py:2908 src/skit/cli.py:3100 src/skit/cli.py:4417 #: src/skit/langs/launch.py:678 #, python-format msgid "The runner %(runner)s isn't configured (known: %(names)s). Manage runners with: skit runner list" msgstr "執行器 %(runner)s 未設定(已知:%(names)s)。管理執行器:skit runner list" -#: src/skit/cli.py:2926 +#: src/skit/cli.py:2920 msgid "Run a registered entry in the terminal." msgstr "在終端機執行已登記的項目。" -#: src/skit/cli.py:2928 +#: src/skit/cli.py:2922 msgid "Examples: skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" msgstr "例:skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" -#: src/skit/cli.py:2933 +#: src/skit/cli.py:2927 msgid "Arguments passed through after --" msgstr "接在 -- 之後傳入的參數" -#: src/skit/cli.py:2935 +#: src/skit/cli.py:2929 msgid "Never prompt; reuse last values and defaults" msgstr "永不詢問;沿用上次的值 / 預設值" -#: src/skit/cli.py:2941 +#: src/skit/cli.py:2935 msgid "Named preset of parameter values to prefill the form with" msgstr "用哪個參數組合(preset)預填表單" -#: src/skit/cli.py:2948 +#: src/skit/cli.py:2942 #, python-brace-format msgid "Set a parameter value by name, as NAME=VALUE (repeatable; values may use tokens like {cwd} or {env:VAR}; the form no longer asks for a field you set)" msgstr "直接以 NAME=VALUE 指定參數值(可重複;值可用 {cwd}、{env:VAR} 等代換符;已指定的欄位表單不再詢問)" -#: src/skit/cli.py:2952 +#: src/skit/cli.py:2946 msgid "Save this run's values as a named preset" msgstr "把這次的值存成參數組合" -#: src/skit/cli.py:2955 +#: src/skit/cli.py:2949 msgid "Line-by-line prompts instead of the inline form" msgstr "用逐行問答取代迷你表單" -#: src/skit/cli.py:2961 +#: src/skit/cli.py:2955 msgid "Skip the parameter form and injection and run the script as-is (escape hatch)" msgstr "跳過參數表單與注入,原樣執行腳本(逃生門)" -#: src/skit/cli.py:2968 +#: src/skit/cli.py:2962 msgid "Print the exact command that would run (tokens and globs expanded), then exit" msgstr "印出實際會執行的完整命令(token 與萬用字元已展開)後結束" -#: src/skit/cli.py:2974 +#: src/skit/cli.py:2968 msgid "Run a prompt entry with this agent (overrides its pin for one run)" msgstr "以此執行器執行提示詞項目(僅本次執行,覆蓋其釘選)" -#: src/skit/cli.py:2981 +#: src/skit/cli.py:2975 msgid "Forget the remembered extra arguments before this run (they are otherwise reused when you pass none)" msgstr "在本次執行前忘掉記住的額外參數(未傳入任何參數時,原本會沿用它們)" -#: src/skit/cli.py:2998 +#: src/skit/cli.py:2992 msgid "--raw runs the script as-is; --set, --preset, and --save-preset do not apply." msgstr "--raw 會原樣執行腳本;--set、--preset、--save-preset 都不適用。" -#: src/skit/cli.py:3013 +#: src/skit/cli.py:3007 msgid "--raw doesn't apply to a prompt entry: its {{placeholders}} are the artifact itself — there is no as-is without them." msgstr "--raw 不適用於 prompt 條目:它的 {{placeholders}} 本身就是產出物——沒有它們就沒有「原樣」可言。" -#: src/skit/cli.py:3018 +#: src/skit/cli.py:3012 #, python-brace-format, python-format msgid "--raw doesn't apply to a %(kind)s entry: its {placeholders} are the artifact itself — there is no as-is without them." msgstr "--raw 不適用於 %(kind)s 條目:它的 {placeholders} 本身就是產出物——沒有它們就沒有「原樣」可言。" -#: src/skit/cli.py:3045 +#: src/skit/cli.py:3039 msgid "Raw mode: skipping the parameter form and injection." msgstr "raw 模式:已跳過參數表單與注入。" -#: src/skit/cli.py:3059 src/skit/cli.py:3562 src/skit/tui_form.py:935 +#: src/skit/cli.py:3053 src/skit/cli.py:3565 src/skit/tui_form.py:935 #, python-format msgid "%(name)s has no form fields, so there's nothing to save." msgstr "%(name)s 沒有表單欄位,沒有東西可存。" -#: src/skit/cli.py:3139 +#: src/skit/cli.py:3140 #, python-format msgid "Reusing your last arguments: %(args)s" msgstr "沿用上次的參數:%(args)s" -#: src/skit/cli.py:3166 src/skit/cli.py:3617 +#: src/skit/cli.py:3167 src/skit/cli.py:3620 #, python-format msgid "Preset \"%(preset)s\" saved for %(name)s." msgstr "已為 %(name)s 儲存參數組合「%(preset)s」。" -#: src/skit/cli.py:3227 +#: src/skit/cli.py:3230 #, python-format msgid "Run exited with code %(code)s" msgstr "本次執行以結束碼 %(code)s 結束" -#: src/skit/cli.py:3237 +#: src/skit/cli.py:3240 msgid "Manage the agents (runners) that prompt entries run with." msgstr "管理提示詞項目使用的執行器(AI agent)。" -#: src/skit/cli.py:3244 +#: src/skit/cli.py:3247 msgid "List the configured runners (seeds them into config on first use)." msgstr "列出已設定的執行器(首次使用時會寫入設定檔)。" -#: src/skit/cli.py:3251 +#: src/skit/cli.py:3254 msgid "Include malformed raw rows and their repair indexes" msgstr "包含格式錯誤的原始列及其修復索引" -#: src/skit/cli.py:3288 +#: src/skit/cli.py:3291 msgid "Row" msgstr "列" -#: src/skit/cli.py:3289 src/skit/cli.py:3320 +#: src/skit/cli.py:3292 src/skit/cli.py:3323 msgid "Runner" msgstr "執行器" -#: src/skit/cli.py:3290 src/skit/cli.py:3321 src/skit/kindnames.py:29 +#: src/skit/cli.py:3293 src/skit/cli.py:3324 src/skit/kindnames.py:29 msgid "Command" msgstr "指令" -#: src/skit/cli.py:3291 +#: src/skit/cli.py:3294 msgid "Status" msgstr "狀態" -#: src/skit/cli.py:3294 +#: src/skit/cli.py:3297 msgid "container" msgstr "容器" -#: src/skit/cli.py:3328 +#: src/skit/cli.py:3331 msgid "The built-in amp preset uses amp -x and runs the prompt once; it does not open an interactive session." msgstr "內建 amp 預設使用 amp -x,只執行提示詞一次;它不會開啟互動工作階段。" -#: src/skit/cli.py:3345 +#: src/skit/cli.py:3348 msgid "A runner needs a command — e.g. skit runner add mycli mycli run {{prompt}}" msgstr "執行器需要一條命令——例如 skit runner add mycli mycli run {{prompt}}" -#: src/skit/cli.py:3348 +#: src/skit/cli.py:3351 msgid "A runner command must contain the {{prompt}} slot exactly once — that's where the rendered prompt lands." msgstr "執行器命令必須恰好包含一個 {{prompt}} 槽位——渲染後的提示詞就放在那裡。" -#: src/skit/cli.py:3352 src/skit/config.py:614 src/skit/tui_runner.py:42 +#: src/skit/cli.py:3355 src/skit/config.py:614 src/skit/tui_runner.py:42 msgid "{{prompt}} can't be the command itself — the first word must be the program to run." msgstr "{{prompt}} 不能是命令本身——第一個詞必須是要執行的程式。" -#: src/skit/cli.py:3355 src/skit/config.py:617 src/skit/tui_runner.py:45 +#: src/skit/cli.py:3358 src/skit/config.py:617 src/skit/tui_runner.py:45 msgid "Runner commands take only the {{prompt}} slot — single-brace text is literal, and other {{holes}} aren't supported." msgstr "執行器命令只接受 {{prompt}} 槽位——單大括號是字面文字,其他 {{佔位符}} 不支援。" -#: src/skit/cli.py:3364 +#: src/skit/cli.py:3367 msgid "Register a runner: skit runner add NAME COMMAND… ({{prompt}} marks where the rendered prompt goes; each shell word becomes one argument, no shell involved)" msgstr "註冊執行器:skit runner add 名稱 命令…({{prompt}} 標記渲染後提示詞的位置;每個 shell 詞就是一個參數,不經過 shell)" -#: src/skit/cli.py:3373 +#: src/skit/cli.py:3376 msgid "Runner name (e.g. claude)" msgstr "執行器名稱(例如 claude)" -#: src/skit/cli.py:3375 +#: src/skit/cli.py:3378 msgid "The command, word by word, with one {{prompt}} slot" msgstr "命令,逐詞給出,含一個 {{prompt}} 槽位" -#: src/skit/cli.py:3380 +#: src/skit/cli.py:3383 msgid "Replace the runner if the name already exists (the edit path)" msgstr "名稱已存在時取代該執行器(編輯路徑)" -#: src/skit/cli.py:3395 +#: src/skit/cli.py:3398 #, python-format msgid "The runner %(name)s already exists — pass --force to replace its command." msgstr "執行器 %(name)s 已存在——加上 --force 以取代其命令。" -#: src/skit/cli.py:3403 +#: src/skit/cli.py:3406 #, python-format msgid "Runner %(name)s updated: %(command)s" msgstr "已更新執行器 %(name)s:%(command)s" -#: src/skit/cli.py:3407 +#: src/skit/cli.py:3410 #, python-format msgid "Runner %(name)s added: %(command)s" msgstr "已新增執行器 %(name)s:%(command)s" -#: src/skit/cli.py:3411 +#: src/skit/cli.py:3414 msgid "Remove a configured runner." msgstr "移除已設定的執行器。" -#: src/skit/cli.py:3414 +#: src/skit/cli.py:3417 msgid "Runner name" msgstr "執行器名稱" -#: src/skit/cli.py:3419 +#: src/skit/cli.py:3422 msgid "Remove one raw row index from 'runner list --all' (or 'container')" msgstr "移除「runner list --all」中的一個原始列索引(或「container」)" -#: src/skit/cli.py:3422 -msgid "Never prompt" -msgstr "絕不提示" - -#: src/skit/cli.py:3425 +#: src/skit/cli.py:3428 msgid "Pass exactly one runner name or --row INDEX." msgstr "請只傳一個執行器名稱或 --row INDEX。" -#: src/skit/cli.py:3445 src/skit/cli.py:3449 +#: src/skit/cli.py:3448 src/skit/cli.py:3452 msgid "--row must be a non-negative index or 'container'." msgstr "--row 必須是非負索引或「container」。" -#: src/skit/cli.py:3454 +#: src/skit/cli.py:3457 #, python-format msgid "Unknown runner row: %(row)s. Inspect with: skit runner list --all" msgstr "未知執行器列:%(row)s。請用 skit runner list --all 檢查" -#: src/skit/cli.py:3462 +#: src/skit/cli.py:3465 #, python-format msgid "Runner row %(row)s is valid. Remove the agent by name instead: skit runner remove \"%(name)s\"" msgstr "執行器第 %(row)s 列有效。請改用名稱移除代理:skit runner remove \"%(name)s\"" -#: src/skit/cli.py:3471 src/skit/tui_runner.py:315 +#: src/skit/cli.py:3474 src/skit/tui_runner.py:315 msgid "Remove the malformed prompt runner container?" msgstr "要移除格式錯誤的提示詞執行器容器嗎?" -#: src/skit/cli.py:3473 +#: src/skit/cli.py:3476 #, python-format msgid "Remove runner row %(row)d (\"%(name)s\")?" msgstr "要移除執行器列 %(row)d(「%(name)s」)嗎?" -#: src/skit/cli.py:3486 src/skit/tui_runner.py:321 +#: src/skit/cli.py:3489 src/skit/tui_runner.py:321 #, python-format msgid "Remove the agent \"%(name)s\"?" msgstr "要移除 agent「%(name)s」嗎?" -#: src/skit/cli.py:3496 src/skit/tui_runner.py:326 +#: src/skit/cli.py:3499 src/skit/tui_runner.py:326 #, python-format msgid "%(count)d prompt pins this runner and will need another runner before it can run again." msgid_plural "%(count)d prompts pin this runner and will need another runner before they can run again." msgstr[0] "有 %(count)d 個提示詞釘選此執行器;再次執行前需要選擇另一個執行器。" -#: src/skit/cli.py:3505 +#: src/skit/cli.py:3508 msgid "Confirmation is required; pass --yes to remove the runner." msgstr "需要確認;請傳 --yes 以移除執行器。" -#: src/skit/cli.py:3520 +#: src/skit/cli.py:3523 msgid "The runner row changed before it could be removed; inspect again." msgstr "執行器列在移除前已發生變更;請重新檢查。" -#: src/skit/cli.py:3523 +#: src/skit/cli.py:3526 #, python-format msgid "Runner %(name)s removed." msgstr "已移除執行器 %(name)s。" -#: src/skit/cli.py:3526 +#: src/skit/cli.py:3529 msgid "Malformed prompt runner container removed." msgstr "已移除格式錯誤的提示詞執行器容器。" -#: src/skit/cli.py:3529 +#: src/skit/cli.py:3532 #, python-format msgid "Malformed runner row %(row)d removed." msgstr "已移除格式錯誤的執行器第 %(row)d 列。" -#: src/skit/cli.py:3538 +#: src/skit/cli.py:3541 msgid "Manage named parameter presets for an entry." msgstr "管理條目的具名參數組合(preset)。" -#: src/skit/cli.py:3543 +#: src/skit/cli.py:3546 msgid "Save a set of parameter values as a named preset." msgstr "把一組參數值儲存成參數組合(preset)。" -#: src/skit/cli.py:3546 src/skit/cli.py:3651 src/skit/tui_form.py:343 +#: src/skit/cli.py:3549 src/skit/cli.py:3654 src/skit/tui_form.py:343 msgid "Preset name" msgstr "組合名稱" -#: src/skit/cli.py:3550 +#: src/skit/cli.py:3553 msgid "Save the last run's values without asking (automation-friendly)" msgstr "直接存上次執行的值,不再詢問(適合自動化)" -#: src/skit/cli.py:3570 src/skit/cli.py:3586 +#: src/skit/cli.py:3573 src/skit/cli.py:3589 #, python-format msgid "%(name)s has no remembered values yet — run it once first." msgstr "%(name)s 還沒有記住的值——先執行一次。" -#: src/skit/cli.py:3604 +#: src/skit/cli.py:3607 #, python-format msgid "Secret values are never stored in presets; skipped: %(names)s" msgstr "機密值不會存入參數組合;已略過:%(names)s" -#: src/skit/cli.py:3621 +#: src/skit/cli.py:3624 msgid "List an entry's saved presets." msgstr "列出條目已儲存的參數組合(preset)。" -#: src/skit/cli.py:3638 +#: src/skit/cli.py:3641 #, python-format msgid "No presets for %(name)s yet. Create one with: skit run %(name)s --save-preset " msgstr "%(name)s 還沒有參數組合。建立一個:skit run %(name)s --save-preset <組合名>" -#: src/skit/cli.py:3648 +#: src/skit/cli.py:3651 msgid "Delete a named preset from an entry." msgstr "從條目刪除一個具名參數組合(preset)。" -#: src/skit/cli.py:3660 +#: src/skit/cli.py:3673 +msgid "Confirmation is required; pass --yes to delete the preset." +msgstr "需要確認;請傳 --yes 以刪除參數組合。" + +#: src/skit/cli.py:3677 +#, python-format +msgid "Delete preset \"%(preset)s\" from %(name)s?" +msgstr "從 %(name)s 刪除參數組合「%(preset)s」?" + +#: src/skit/cli.py:3683 #, python-format msgid "Preset \"%(preset)s\" deleted from %(name)s." msgstr "已從 %(name)s 刪除參數組合「%(preset)s」。" -#: src/skit/cli.py:3715 +#: src/skit/cli.py:3742 #, python-format msgid "default %(value)s" msgstr "預設 %(value)s" -#: src/skit/cli.py:3717 +#: src/skit/cli.py:3744 msgid "optional" msgstr "選填" -#: src/skit/cli.py:3719 +#: src/skit/cli.py:3746 msgid "secret" msgstr "機密" -#: src/skit/cli.py:3729 +#: src/skit/cli.py:3756 msgid "Delivery" msgstr "傳遞方式" -#: src/skit/cli.py:3733 src/skit/cli.py:3946 +#: src/skit/cli.py:3760 src/skit/cli.py:3978 msgid "Last value" msgstr "上次的值" -#: src/skit/cli.py:3777 +#: src/skit/cli.py:3804 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written. Turn it on with: skit params %(name)s --interpolate" msgstr "變量插入已關閉——內文會原封不動送達 agent。開啟:skit params %(name)s --interpolate" -#: src/skit/cli.py:3788 src/skit/cli.py:3917 src/skit/cli.py:3926 +#: src/skit/cli.py:3815 src/skit/cli.py:3949 src/skit/cli.py:3958 #, python-format msgid "%(name)s has no managed parameters." msgstr "%(name)s 沒有管理的參數。" -#: src/skit/cli.py:3793 +#: src/skit/cli.py:3820 msgid "Prompt placeholders (the run form asks for them):" msgstr "提示詞的佔位符(執行表單會詢問):" -#: src/skit/cli.py:3795 +#: src/skit/cli.py:3822 msgid "Command template placeholders (the run form asks for them):" msgstr "命令模板的佔位符(執行表單會詢問):" -#: src/skit/cli.py:3802 +#: src/skit/cli.py:3829 msgid "Declared environment variables (set on the run):" msgstr "宣告的環境變數(執行時設定):" -#: src/skit/cli.py:3825 +#: src/skit/cli.py:3852 #, python-format msgid "No longer in the prompt (the value would be ignored): %(names)s — remove with --rm, or edit the body." msgstr "提示詞中已不存在(其值會被忽略):%(names)s——用 --rm 移除,或編輯內文。" -#: src/skit/cli.py:3932 +#: src/skit/cli.py:3964 #, python-format msgid "%(name)s has no managed parameters. Use --manage to bring a detected candidate under management." msgstr "%(name)s 沒有管理的參數。用 --manage 把偵測到的候選納入管理。" -#: src/skit/cli.py:3970 +#: src/skit/cli.py:4002 #, python-format msgid "Detected but not yet managed: %(names)s" msgstr "偵測到但尚未管理:%(names)s" -#: src/skit/cli.py:3977 +#: src/skit/cli.py:4009 msgid "Reference mode: skit never writes the original file — manage parameters by editing its [tool.skit] block in the source directly." msgstr "參照模式:skit 絕不寫入原始檔案——請直接編輯原始碼中的 [tool.skit] 區塊來管理參數。" -#: src/skit/cli.py:3985 +#: src/skit/cli.py:4017 #, python-format msgid "Detected but not yet managed: %(names)s (use --manage to manage them)" msgstr "偵測到但尚未管理:%(names)s(用 --manage 管理)" -#: src/skit/cli.py:3990 +#: src/skit/cli.py:4022 #, python-brace-format, python-format msgid "This script locates itself ($0 / BASH_SOURCE). Injecting a constant runs it from a temporary copy, so it would see that copy path instead. Rewriting the constant as NAME=\"${NAME:-value}\" delivers the value through the environment with no copy at all — `skit params %(name)s --normalize NAME` does the rewrite for you on the stored copy." msgstr "這個腳本會讀取自己的位置($0 / BASH_SOURCE)。注入常數時它會從臨時副本執行,因此看到的是那個副本的路徑。把常數改寫成 NAME=\"${NAME:-value}\",值就會改由環境變數傳遞,完全不產生副本——`skit params %(name)s --normalize NAME` 會在儲存的副本上幫你完成改寫。" -#: src/skit/cli.py:4009 +#: src/skit/cli.py:4041 msgid "Show or edit an entry's managed or declared parameters." msgstr "檢視或編輯條目的管理參數或宣告參數。" -#: src/skit/cli.py:4011 +#: src/skit/cli.py:4043 msgid "Examples: skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" msgstr "例:skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" -#: src/skit/cli.py:4019 +#: src/skit/cli.py:4051 msgid "Prune definitions that no longer match the script and refresh changed types" msgstr "修剪已對不上腳本的定義,並更新型別已變的項" -#: src/skit/cli.py:4024 +#: src/skit/cli.py:4056 msgid "Bring a currently detected candidate under management (repeatable)" msgstr "把當前偵測到的候選納入管理(可重複)" -#: src/skit/cli.py:4027 +#: src/skit/cli.py:4059 msgid "Drop a managed parameter (repeatable)" msgstr "移除一個受管理的參數(可重複)" -#: src/skit/cli.py:4032 +#: src/skit/cli.py:4064 msgid "Declare a new parameter on an exe/command entry, by name (repeatable)" msgstr "為 exe/command 條目宣告一個新參數,依名稱指定(可重複)" -#: src/skit/cli.py:4035 +#: src/skit/cli.py:4067 msgid "Remove a declared parameter, by name (repeatable)" msgstr "移除一個宣告的參數,依名稱指定(可重複)" -#: src/skit/cli.py:4040 +#: src/skit/cli.py:4072 msgid "Set a declared parameter's type, as NAME=str|int|float|bool|choice|path" msgstr "設定宣告參數的類型,格式 NAME=str|int|float|bool|choice|path" -#: src/skit/cli.py:4043 +#: src/skit/cli.py:4075 msgid "Set a declared parameter's default, as NAME=VALUE" msgstr "設定宣告參數的預設值,格式 NAME=VALUE" -#: src/skit/cli.py:4048 +#: src/skit/cli.py:4080 msgid "Set a declared parameter's choices, as NAME=a,b,c (comma separated)" msgstr "設定宣告參數的可選值,格式 NAME=a,b,c(逗號分隔)" -#: src/skit/cli.py:4054 +#: src/skit/cli.py:4086 msgid "Set how a declared parameter reaches the program, as NAME=env|flag|placeholder" msgstr "設定宣告參數如何傳遞給程式,格式 NAME=env|flag|placeholder" -#: src/skit/cli.py:4060 +#: src/skit/cli.py:4092 msgid "Set a declared flag parameter's option, as NAME=--out (empty = positional)" msgstr "設定宣告的 flag 參數選項,格式 NAME=--out(留空為位置參數)" -#: src/skit/cli.py:4063 +#: src/skit/cli.py:4095 msgid "Mark a declared parameter as required (repeatable)" msgstr "把某個宣告參數標為必填(可重複)" -#: src/skit/cli.py:4066 +#: src/skit/cli.py:4098 msgid "Mark a declared parameter as optional (repeatable)" msgstr "把某個宣告參數標為選填(可重複)" -#: src/skit/cli.py:4069 +#: src/skit/cli.py:4101 msgid "Set a declared parameter's help text, as NAME=text" msgstr "設定宣告參數的說明文字,格式 NAME=text" -#: src/skit/cli.py:4072 +#: src/skit/cli.py:4104 msgid "Mark a managed parameter as secret (repeatable)" msgstr "把某個受管參數標為機密(可重複)" -#: src/skit/cli.py:4077 +#: src/skit/cli.py:4109 msgid "Remove the secret mark from a managed parameter (repeatable)" msgstr "取消某個受管參數的機密標記(可重複)" -#: src/skit/cli.py:4080 +#: src/skit/cli.py:4112 msgid "Set a parameter's form prompt, as NAME=text (repeatable)" msgstr "以 NAME=text 設定參數的表單提示(可重複)" -#: src/skit/cli.py:4086 +#: src/skit/cli.py:4118 msgid "Read a secret parameter from an environment variable at run time, as NAME=ENVVAR (empty ENVVAR clears it; repeatable)" msgstr "執行時從環境變數讀取機密參數,格式 NAME=ENVVAR(ENVVAR 留空表示清除;可重複)" -#: src/skit/cli.py:4093 +#: src/skit/cli.py:4125 #, python-brace-format msgid "Shell only: rewrite a constant into the ${NAME:-default} idiom in the stored copy, so its value is delivered as an environment variable instead of a rewritten temporary copy (repeatable)" msgstr "僅限 Shell:把常數改寫成 ${NAME:-default} 寫法(改的是倉庫副本),之後它的值就用環境變數傳遞,不必再改寫臨時副本(可重複)" -#: src/skit/cli.py:4100 +#: src/skit/cli.py:4132 msgid "Prompt only: pin the agent this prompt runs with (empty value clears the pin)" msgstr "僅提示詞:釘選此提示詞使用的執行器(空值清除釘選)" -#: src/skit/cli.py:4108 +#: src/skit/cli.py:4140 msgid "Prompt only: turn variable insertion on/off for this prompt (off = the body travels exactly as written)" msgstr "僅提示詞:開關此提示詞的變量插入(關閉 = 內文原封不動送達)" -#: src/skit/cli.py:4116 +#: src/skit/cli.py:4148 msgid "Set where the entry runs: origin (its own folder), store, invoke (where you run skit from), or an absolute path" msgstr "設定項目的執行位置:origin(它自己的資料夾)、store、invoke(你執行 skit 的位置),或一個絕對路徑" -#: src/skit/cli.py:4123 +#: src/skit/cli.py:4155 #, python-brace-format msgid "Command only: rewrite the template ({placeholders} are re-read from it)" msgstr "僅限 command:改寫模板({placeholders} 會從中重新讀取)" -#: src/skit/cli.py:4129 +#: src/skit/cli.py:4161 msgid "Pin the interpreter/runtime an interpreted entry runs with (e.g. zsh, bun; empty value returns to automatic)" msgstr "釘選直譯型項目使用的直譯器/執行環境(例如 zsh、bun;留空即回到自動偵測)" -#: src/skit/cli.py:4133 +#: src/skit/cli.py:4165 msgid "Output the read view as JSON" msgstr "讀取檢視以 JSON 輸出" -#: src/skit/cli.py:4195 +#: src/skit/cli.py:4227 #, python-format msgid "%(op)s is its own operation — run it in a separate skit params call (nothing was changed)." msgstr "%(op)s 是獨立的操作——請在另一次 skit params 呼叫中執行(這次未變更任何內容)。" -#: src/skit/cli.py:4265 +#: src/skit/cli.py:4297 #, python-format msgid "%(name)s manages its parameters from the script itself — use --manage / --unmanage, or edit the [tool.skit] block." msgstr "%(name)s 由腳本自身管理參數——請使用 --manage / --unmanage,或直接編輯 [tool.skit] 區塊。" -#: src/skit/cli.py:4353 +#: src/skit/cli.py:4385 #, python-format msgid "Template updated. Placeholders: %(names)s" msgstr "模板已更新。佔位符:%(names)s" -#: src/skit/cli.py:4357 +#: src/skit/cli.py:4389 #, python-format msgid "%(name)s now runs in: %(dir)s" msgstr "%(name)s 現在於此執行:%(dir)s" -#: src/skit/cli.py:4362 +#: src/skit/cli.py:4394 #, python-format msgid "%(name)s now runs with: %(program)s" msgstr "%(name)s 現在以此執行:%(program)s" -#: src/skit/cli.py:4366 +#: src/skit/cli.py:4398 #, python-format msgid "%(name)s is back to automatic interpreter detection." msgstr "%(name)s 已回到自動偵測直譯器。" -#: src/skit/cli.py:4397 +#: src/skit/cli.py:4429 #, python-format msgid "%(name)s now runs with %(runner)s." msgstr "%(name)s 現在以 %(runner)s 執行。" -#: src/skit/cli.py:4401 +#: src/skit/cli.py:4433 #, python-format msgid "Cleared the runner pin — %(name)s asks at run time." msgstr "已清除執行器釘選——%(name)s 會在執行時詢問。" -#: src/skit/cli.py:4410 +#: src/skit/cli.py:4442 msgid "--interpolate only applies to prompt entries." msgstr "--interpolate 只適用於提示詞項目。" -#: src/skit/cli.py:4417 +#: src/skit/cli.py:4449 #, python-format msgid "Variable insertion is on — %(name)s fills its managed placeholders again." msgstr "變量插入已開啟——%(name)s 會再次填入已管理的佔位符。" -#: src/skit/cli.py:4421 +#: src/skit/cli.py:4453 #, python-format msgid "Variable insertion is off — %(name)s travels to the agent exactly as written." msgstr "變量插入已關閉——%(name)s 會原封不動送達 agent。" -#: src/skit/cli.py:4433 +#: src/skit/cli.py:4465 #, python-format msgid "%(name)s isn't a managed parameter; --env-source skipped." msgstr "%(name)s 不是管理的參數;--env-source 已略過。" -#: src/skit/cli.py:4440 src/skit/cli.py:4695 +#: src/skit/cli.py:4472 src/skit/cli.py:4723 #, python-format msgid "%(name)s isn't secret; --env-source only applies to secret parameters (mark it with --secret first)." msgstr "%(name)s 不是機密參數;--env-source 只適用於機密參數(先用 --secret 標記)。" -#: src/skit/cli.py:4468 +#: src/skit/cli.py:4499 #, python-format msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from." msgstr "%(name)s 沒有管理的參數——它的種類沒有分析器可以讀取參數。" -#: src/skit/cli.py:4476 +#: src/skit/cli.py:4506 +#, python-format +msgid "Declare one instead: skit params %(name)s --add PARAM" +msgstr "改為手動宣告一個:skit params %(name)s --add <參數名>" + +#: src/skit/cli.py:4512 #, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Edit the [tool.skit] block in the source directly." msgstr "%(name)s 是 reference 模式;skit 絕不寫原檔。請直接改原檔的 [tool.skit] 區塊。" -#: src/skit/cli.py:4497 +#: src/skit/cli.py:4533 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=text)." msgstr "已忽略格式錯誤的值:%(item)s(應為 NAME=text)。" -#: src/skit/cli.py:4536 src/skit/cli.py:4820 +#: src/skit/cli.py:4562 src/skit/cli.py:4848 #, python-format msgid "Removed previously stored plaintext value(s) for now-secret parameter(s): %(names)s" msgstr "已移除下列剛設為機密的參數先前以明文儲存的值:%(names)s" -#: src/skit/cli.py:4543 +#: src/skit/cli.py:4569 #, python-format msgid "Updated %(name)s. Managed parameters: %(names)s" msgstr "已更新 %(name)s。受管理的參數:%(names)s" -#: src/skit/cli.py:4549 +#: src/skit/cli.py:4575 #, python-format msgid "The run form now asks for the managed parameters — the script's own command-line form (%(frameworks)s) is set aside until they are removed (--unmanage)." msgstr "執行表單現在會詢問這些管理的參數——腳本自己的命令列表單(%(frameworks)s)會先擱置,直到它們被移除(--unmanage)為止。" -#: src/skit/cli.py:4565 +#: src/skit/cli.py:4591 #, python-format msgid "%(name)s isn't a plain constant with a literal value, so there's nothing to normalize; skipped." msgstr "%(name)s 不是帶字面值的普通常數,沒有可以正規化的東西;已略過。" -#: src/skit/cli.py:4568 +#: src/skit/cli.py:4594 #, python-format msgid "%(name)s is assigned more than once at the top level; normalizing it would change which value wins. Skipped." msgstr "%(name)s 在頂層被賦值多次;正規化會改變最終生效的值。已略過。" -#: src/skit/cli.py:4571 +#: src/skit/cli.py:4597 #, python-format msgid "%(name)s is readonly, so the script could never take a value from the environment; skipped." msgstr "%(name)s 是 readonly,腳本永遠不可能從環境變數取值;已略過。" -#: src/skit/cli.py:4573 +#: src/skit/cli.py:4599 #, python-format msgid "%(name)s already reads from the environment; nothing to do." msgstr "%(name)s 已經從環境變數讀取了;無需處理。" -#: src/skit/cli.py:4575 +#: src/skit/cli.py:4601 #, python-format msgid "%(name)s's value contains a character that can't be moved into ${...:-...} safely (one of } \" ` $ \\ or a newline); skipped — it keeps being injected into a temporary copy." msgstr "%(name)s 的值裡有無法安全放進 ${...:-...} 的字元(} \" ` $ \\ 或換行);已略過——它會繼續用臨時副本注入。" -#: src/skit/cli.py:4579 +#: src/skit/cli.py:4605 msgid "Could not parse the script (syntax error); nothing was normalized." msgstr "無法解析腳本(語法錯誤);沒有做任何正規化。" -#: src/skit/cli.py:4610 +#: src/skit/cli.py:4636 #, python-brace-format, python-format msgid "%(name)s has no --normalize: it is a shell idiom (VAR=value -> VAR=\"${VAR:-value}\")." msgstr "%(name)s 沒有 --normalize:那是 shell 的寫法(VAR=value -> VAR=\"${VAR:-value}\")。" -#: src/skit/cli.py:4618 +#: src/skit/cli.py:4644 #, python-brace-format, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Change the line to VAR=\"${VAR:-value}\" in the source directly." msgstr "%(name)s 是 reference 模式;skit 絕不寫原檔案。請直接把原檔案裡那行改成 VAR=\"${VAR:-value}\"。" -#: src/skit/cli.py:4637 +#: src/skit/cli.py:4663 #, python-format msgid "%(name)s isn't valid UTF-8, so --normalize can't rewrite it safely; nothing was changed — its constants keep being injected into a temporary copy." msgstr "%(name)s 不是有效的 UTF-8,--normalize 無法安全改寫;什麼都沒改——它的常數會繼續用臨時副本注入。" -#: src/skit/cli.py:4674 +#: src/skit/cli.py:4702 #, python-format msgid "Normalized %(names)s in %(name)s: delivered as environment variables from now on (no temporary copy, and $0 stays your real file)." msgstr "已正規化 %(name)s 中的 %(names)s:今後用環境變數傳值(不再寫臨時副本,$0 也仍指向你的真實檔案)。" -#: src/skit/cli.py:4684 +#: src/skit/cli.py:4712 #, python-format msgid "%(name)s isn't a declared parameter; skipped." msgstr "%(name)s 不是已宣告的參數,已跳過。" -#: src/skit/cli.py:4685 +#: src/skit/cli.py:4713 #, python-format msgid "%(name)s is already declared; skipped." msgstr "%(name)s 已經宣告過,已跳過。" -#: src/skit/cli.py:4686 +#: src/skit/cli.py:4714 #, python-format msgid "%(name)s: that delivery isn't available for this kind; skipped." msgstr "%(name)s:該傳遞方式不適用於此類型,已跳過。" -#: src/skit/cli.py:4688 +#: src/skit/cli.py:4716 #, python-format msgid "%(name)s isn't a template placeholder, so it can't use placeholder delivery; skipped." msgstr "%(name)s 不是模板佔位符,無法使用 placeholder 傳遞方式,已跳過。" -#: src/skit/cli.py:4691 +#: src/skit/cli.py:4719 #, python-format msgid "%(name)s: unknown type; skipped (use str, int, float, bool, choice, or path)." msgstr "%(name)s:未知類型,已跳過(可用 str、int、float、bool、choice 或 path)。" -#: src/skit/cli.py:4693 +#: src/skit/cli.py:4721 #, python-format msgid "%(name)s: the default doesn't fit its type; skipped." msgstr "%(name)s:預設值與其類型不符,已跳過。" -#: src/skit/cli.py:4698 +#: src/skit/cli.py:4726 #, python-format msgid "%(name)s: a choice parameter needs choices; set --choices %(name)s=a,b,c." msgstr "%(name)s:choice 參數需要可選值,請設定 --choices %(name)s=a,b,c。" -#: src/skit/cli.py:4701 +#: src/skit/cli.py:4729 #, python-format msgid "%(name)s is on by default, so its flag could only ever turn it on again. Declare the flag that turns it OFF instead (--no-%(name)s and the like), with default false." msgstr "%(name)s 預設就是開的,它的旗標只會再開一次。請改成宣告用來關掉它的那個旗標(--no-%(name)s 之類),預設 false。" -#: src/skit/cli.py:4757 +#: src/skit/cli.py:4785 #, python-format msgid "Variable insertion is off for %(name)s — turn it on first with: skit params %(name)s --interpolate" msgstr "%(name)s 的變量插入已關閉——請先開啟:skit params %(name)s --interpolate" -#: src/skit/cli.py:4771 +#: src/skit/cli.py:4799 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=VALUE)." msgstr "已忽略格式錯誤的值:%(item)s(應為 NAME=VALUE)。" -#: src/skit/cli.py:4827 +#: src/skit/cli.py:4855 #, python-format msgid "Updated %(name)s. Declared parameters: %(names)s" msgstr "已更新 %(name)s。已宣告的參數:%(names)s" -#: src/skit/cli.py:4855 +#: src/skit/cli.py:4883 #, python-format msgid "Dependencies of %(name)s: %(deps)s" msgstr "%(name)s 的依賴:%(deps)s" -#: src/skit/cli.py:4864 +#: src/skit/cli.py:4892 #, python-format msgid "External commands needed by %(name)s: %(needs)s" msgstr "%(name)s 所需的外部命令:%(needs)s" -#: src/skit/cli.py:4871 +#: src/skit/cli.py:4899 msgid "View or update an entry's package dependencies, Python constraint, and needed commands." msgstr "檢視或更新條目的套件依賴、Python 版本限制和所需命令。" -#: src/skit/cli.py:4874 +#: src/skit/cli.py:4902 msgid "Examples: skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" msgstr "範例:skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" -#: src/skit/cli.py:4880 +#: src/skit/cli.py:4908 msgid "A dependency (repeat for more; replaces the whole list)" msgstr "一項依賴(可重複;整組取代原清單)" -#: src/skit/cli.py:4882 +#: src/skit/cli.py:4910 msgid "Remove every dependency" msgstr "清空所有依賴" -#: src/skit/cli.py:4890 +#: src/skit/cli.py:4918 msgid "An external command the entry needs on PATH (repeat; replaces the whole list)" msgstr "條目在 PATH 上所需的外部命令(可重複;取代整份清單)" -#: src/skit/cli.py:4894 +#: src/skit/cli.py:4922 msgid "Remove every needed external command" msgstr "清空所有所需的外部命令" -#: src/skit/cli.py:4914 +#: src/skit/cli.py:4942 #, python-format msgid "%(name)s doesn't take package dependencies — only --need applies here." msgstr "%(name)s 不接受套件依賴——這裡只能用 --need。" -#: src/skit/cli.py:4920 +#: src/skit/cli.py:4948 msgid "Use --dep to set the list or --clear to empty it — not both." msgstr "--dep 用來設定清單、--clear 用來清空——兩者擇一。" -#: src/skit/cli.py:4925 +#: src/skit/cli.py:4953 msgid "Use --need to set the list or --clear-needs to empty it — not both." msgstr "--need 用來設定清單、--clear-needs 用來清空——兩者擇一。" -#: src/skit/cli.py:4960 +#: src/skit/cli.py:4988 #, python-format msgid "Dependencies of %(name)s updated: %(deps)s" msgstr "%(name)s 的依賴已更新:%(deps)s" -#: src/skit/cli.py:4964 +#: src/skit/cli.py:4992 #, python-format msgid "Python constraint of %(name)s updated: %(value)s" msgstr "%(name)s 的 Python 版本約束已更新:%(value)s" -#: src/skit/cli.py:4974 +#: src/skit/cli.py:5002 #, python-format msgid "Needs of %(name)s updated: %(needs)s" msgstr "%(name)s 所需的命令已更新:%(needs)s" -#: src/skit/cli.py:4987 +#: src/skit/cli.py:5015 msgid "Connect skit to AI agents: install the official Agent Skill." msgstr "把 skit 接上 AI agent:安裝官方 Agent Skill。" -#: src/skit/cli.py:5002 +#: src/skit/cli.py:5030 #, python-format msgid "Could not write the skill there: %(error)s" msgstr "無法把 skill 寫到那裡:%(error)s" -#: src/skit/cli.py:5005 src/skit/tui_prefs.py:195 +#: src/skit/cli.py:5033 src/skit/tui_prefs.py:196 #, python-format msgid "Installed the skit Agent Skill: %(path)s" msgstr "已安裝 skit Agent Skill:%(path)s" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "user" msgstr "使用者" -#: src/skit/cli.py:5011 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 msgid "project" msgstr "專案" -#: src/skit/cli.py:5012 +#: src/skit/cli.py:5040 msgid "Agent directories on this machine:" msgstr "這台機器上的 agent 目錄:" -#: src/skit/cli.py:5016 +#: src/skit/cli.py:5044 msgid "Install where?" msgstr "要裝到哪裡?" -#: src/skit/cli.py:5023 +#: src/skit/cli.py:5051 #, python-format msgid "Write the skill into %(path)s?" msgstr "把 skill 寫入 %(path)s?" -#: src/skit/cli.py:5033 +#: src/skit/cli.py:5061 msgid "Install skit's Agent Skill into an AI agent's skills directory." msgstr "把 skit 的 Agent Skill 安裝到 AI agent 的 skills 目錄。" -#: src/skit/cli.py:5035 +#: src/skit/cli.py:5063 msgid "Examples: skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" msgstr "例:skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" -#: src/skit/cli.py:5042 +#: src/skit/cli.py:5070 msgid "Where to install: claude, codex, or agents (the cross-agent ./.agents directory)" msgstr "安裝目標:claude、codex,或 agents(跨 agent 的 ./.agents 目錄)" -#: src/skit/cli.py:5048 +#: src/skit/cli.py:5076 msgid "Install into this skills directory instead of a named target" msgstr "安裝到這個 skills 目錄,而不是具名目標" -#: src/skit/cli.py:5054 +#: src/skit/cli.py:5082 msgid "Install into the current project (./.claude, ./.codex) instead of your home directory" msgstr "安裝到目前專案(./.claude、./.codex),而不是家目錄" -#: src/skit/cli.py:5063 +#: src/skit/cli.py:5091 msgid "Use a named target (with optional --project) or --to — not both." msgstr "具名目標(可搭配 --project)和 --to 擇一使用,不能同時。" -#: src/skit/cli.py:5075 +#: src/skit/cli.py:5103 #, python-format msgid "Unknown target %(name)s. Valid targets: claude, codex, agents." msgstr "沒有 %(name)s 這個目標。可用目標:claude、codex、agents。" -#: src/skit/cli.py:5084 +#: src/skit/cli.py:5112 msgid "Nothing installed: name a target (claude, codex, agents) or pass --to DIR." msgstr "沒有安裝任何東西:請指定目標(claude、codex、agents)或改用 --to DIR。" -#: src/skit/cli.py:5091 +#: src/skit/cli.py:5119 msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Pass --to DIR to choose one yourself." msgstr "沒有偵測到 agent 目錄(~/.claude、~/.codex、./.agents 等)。可用 --to DIR 自行指定。" -#: src/skit/cli.py:5097 +#: src/skit/cli.py:5125 msgid "Cancelled — nothing was written." msgstr "已取消——沒有寫入任何東西。" -#: src/skit/cli.py:5118 +#: src/skit/cli.py:5146 msgid "Check that uv is available and the entry library is intact." msgstr "檢查 uv 是否可用以及工具庫是否完好。" -#: src/skit/cli.py:5121 +#: src/skit/cli.py:5149 msgid "Rebuild the index from each entry's meta.toml" msgstr "從每個條目的 meta.toml 重建索引" -#: src/skit/cli.py:5137 src/skit/tui_health.py:172 +#: src/skit/cli.py:5165 src/skit/tui_health.py:172 #, python-format msgid "Index rebuilt: %(count)s entry" msgid_plural "Index rebuilt: %(count)s entries" msgstr[0] "索引已重建:%(count)s 筆" -#: src/skit/cli.py:5183 src/skit/tui_health.py:62 +#: src/skit/cli.py:5211 src/skit/tui_health.py:62 #, python-format msgid "uv: %(path)s" msgstr "uv:%(path)s" -#: src/skit/cli.py:5186 src/skit/tui_health.py:65 +#: src/skit/cli.py:5214 src/skit/tui_health.py:65 msgid "uv: not found. Install it from https://docs.astral.sh/uv/getting-started/installation/" msgstr "uv:未找到。安裝方式:https://docs.astral.sh/uv/getting-started/installation/" -#: src/skit/cli.py:5190 src/skit/tui_health.py:72 +#: src/skit/cli.py:5218 src/skit/tui_health.py:72 #, python-format msgid "%(count)s entry registered" msgid_plural "%(count)s entries registered" msgstr[0] "已登記 %(count)s 個條目" -#: src/skit/cli.py:5195 +#: src/skit/cli.py:5223 #, python-format msgid "%(name)s: the launch target is gone from disk" msgstr "%(name)s:執行目標已從磁碟消失" -#: src/skit/cli.py:5199 +#: src/skit/cli.py:5227 #, python-format msgid "%(name)s: form definitions are out of sync — run: skit params %(name)s --resync" msgstr "%(name)s:表單定義過期——執行:skit params %(name)s --resync" -#: src/skit/cli.py:5203 +#: src/skit/cli.py:5231 #, python-format msgid "%(name)s: missing external command(s): %(tools)s" msgstr "%(name)s:缺少外部命令:%(tools)s" -#: src/skit/cli.py:5209 +#: src/skit/cli.py:5237 #, python-format msgid "%(name)s: a run would refuse to start — %(reason)s" msgstr "%(name)s:執行會拒絕啟動——%(reason)s" -#: src/skit/cli.py:5213 +#: src/skit/cli.py:5241 #, python-format msgid "Ignored malformed runner row(s) in config: %(rows)s. Inspect and repair with: skit runner list --all" msgstr "已忽略設定中格式錯誤的執行器列:%(rows)s。檢查並修復:skit runner list --all" -#: src/skit/cli.py:5217 src/skit/tui_health.py:134 +#: src/skit/cli.py:5245 src/skit/tui_health.py:134 #, python-format msgid "Library: %(path)s (%(count)s · %(size)s)" msgstr "工具庫:%(path)s(%(count)s 個 · %(size)s)" -#: src/skit/cli.py:5235 +#: src/skit/cli.py:5263 #, python-format msgid "auto (%(locale)s)" msgstr "自動(%(locale)s)" -#: src/skit/cli.py:5272 +#: src/skit/cli.py:5300 msgid "default ($VISUAL / $EDITOR)" msgstr "預設($VISUAL / $EDITOR)" -#: src/skit/cli.py:5288 +#: src/skit/cli.py:5316 msgid "auto (bash on PATH)" msgstr "自動(使用 PATH 上的 bash)" -#: src/skit/cli.py:5292 +#: src/skit/cli.py:5320 msgid "auto (deno > bun > node)" msgstr "自動(deno > bun > node)" -#: src/skit/cli.py:5327 +#: src/skit/cli.py:5355 msgid "Mirrors are switched off — run `skit config mirror on` to activate them." msgstr "鏡像目前是關閉狀態——執行 `skit config mirror on` 啟用。" -#: src/skit/cli.py:5340 +#: src/skit/cli.py:5368 msgid "Nothing to enable: no mirror URLs are saved. Set an axis first: mirror.pypi / mirror.github / mirror.npm." msgstr "沒有可啟用的鏡像:尚未儲存任何鏡像 URL。請先設定某個軸:mirror.pypi / mirror.github / mirror.npm。" -#: src/skit/cli.py:5345 +#: src/skit/cli.py:5373 #, python-format msgid "Unknown mirror value: %(name)s. \"mirror\" is the master switch (on / off); mirrors are picked per ecosystem: mirror.pypi (%(pypi)s), mirror.github (%(github)s), mirror.npm (%(npm)s) — each also takes a URL or \"off\"." msgstr "未知的 mirror 值:%(name)s。「mirror」只是總開關(on / off);鏡像按生態系各自挑選:mirror.pypi(%(pypi)s)、mirror.github(%(github)s)、mirror.npm(%(npm)s)——每項也接受 URL 或「off」。" -#: src/skit/cli.py:5361 +#: src/skit/cli.py:5389 #, python-format msgid "Unknown %(key)s value: %(name)s. Choose from: %(names)s, off — or give a full URL." msgstr "未知的 %(key)s 值:%(name)s。可選:%(names)s、off——或直接給完整 URL。" -#: src/skit/cli.py:5385 +#: src/skit/cli.py:5413 #, python-format msgid "Unknown mirror.github value: %(name)s. Choose from: %(names)s, off — or give an https:// github-release base URL (the uv binary is downloaded and executed, so https:// is required)." msgstr "未知的 mirror.github 值:%(name)s。可選:%(names)s、off——或給一個 https:// 的 github-release 基底 URL(uv 主程式會被下載並執行,因此必須是 https://)。" -#: src/skit/cli.py:5402 +#: src/skit/cli.py:5430 #, python-format msgid "Unknown language: %(tag)s. Available: %(locales)s" msgstr "未知語言:%(tag)s。可用語言:%(locales)s" -#: src/skit/cli.py:5419 +#: src/skit/cli.py:5447 #, python-format msgid "Unknown form style: %(value)s. Choose from: tui, plain" msgstr "未知的表單形態:%(value)s。可選:tui、plain" -#: src/skit/cli.py:5429 src/skit/tui_prefs.py:482 +#: src/skit/cli.py:5457 src/skit/tui_prefs.py:483 #, python-format msgid "No such file: %(path)s" msgstr "找不到檔案:%(path)s" -#: src/skit/cli.py:5436 +#: src/skit/cli.py:5464 #, python-format msgid "Unknown JS runner: %(value)s. Choose from: %(names)s" msgstr "未知的 JS 執行環境:%(value)s。可選:%(names)s" -#: src/skit/cli.py:5443 +#: src/skit/cli.py:5471 #, python-format msgid "Unknown after-run behavior: %(value)s. Choose from: exit, stay" msgstr "未知的執行後行為:%(value)s。可選:exit、stay" -#: src/skit/cli.py:5451 +#: src/skit/cli.py:5479 msgid "Read or set skit's settings (language, editor, mirror, form style, after-run)." msgstr "讀取或設定 skit 的設定(語言、編輯器、鏡像、表單形態、執行後行為)。" -#: src/skit/cli.py:5453 +#: src/skit/cli.py:5481 msgid "Examples: skit config · skit config lang zh-TW · skit config mirror.pypi tsinghua" msgstr "例:skit config · skit config lang zh-TW · skit config mirror.pypi tsinghua" -#: src/skit/cli.py:5460 +#: src/skit/cli.py:5488 msgid "Setting name: lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" msgstr "設定項:lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" -#: src/skit/cli.py:5465 +#: src/skit/cli.py:5493 msgid "New value (omit to read; lang also accepts \"auto\")" msgstr "新值(省略表示讀取;lang 也接受 \"auto\")" -#: src/skit/cli.py:5482 +#: src/skit/cli.py:5510 #, python-format msgid "Unknown setting: %(key)s. Available: %(keys)s" msgstr "未知的設定:%(key)s。可用:%(keys)s" -#: src/skit/cli.py:5514 src/skit/tui_prefs.py:412 src/skit/tui_prefs.py:452 +#: src/skit/cli.py:5542 src/skit/tui_prefs.py:413 src/skit/tui_prefs.py:453 msgid "A custom choice needs a URL." msgstr "選了 custom 就需要填 URL。" -#: src/skit/cli.py:5521 src/skit/tui_prefs.py:347 +#: src/skit/cli.py:5549 src/skit/tui_prefs.py:348 msgid "github-release mirror base URL" msgstr "github-release 鏡像基底 URL" -#: src/skit/cli.py:5527 src/skit/tui_prefs.py:457 +#: src/skit/cli.py:5555 src/skit/tui_prefs.py:458 #, python-format msgid "The uv binary is downloaded and executed, so the github-release base URL must use https:// (got: %(url)s)." msgstr "uv 主程式會被下載並執行,因此 github-release 基底 URL 必須使用 https://(目前為:%(url)s)。" -#: src/skit/cli.py:5546 src/skit/tui_prefs.py:336 +#: src/skit/cli.py:5574 src/skit/tui_prefs.py:337 msgid "PyPI index (Python packages)" msgstr "PyPI 索引(Python 套件)" -#: src/skit/cli.py:5548 src/skit/tui_prefs.py:340 +#: src/skit/cli.py:5576 src/skit/tui_prefs.py:341 msgid "PyPI index URL" msgstr "PyPI 索引 URL" -#: src/skit/cli.py:5552 src/skit/tui_prefs.py:341 +#: src/skit/cli.py:5580 src/skit/tui_prefs.py:342 msgid "GitHub releases (Python builds, the uv binary)" msgstr "GitHub 釋出(Python 本體、uv 主程式)" -#: src/skit/cli.py:5561 src/skit/tui_prefs.py:350 +#: src/skit/cli.py:5589 src/skit/tui_prefs.py:351 msgid "npm registry (JS/TS packages)" msgstr "npm registry(JS/TS 套件)" -#: src/skit/cli.py:5563 src/skit/tui_prefs.py:354 +#: src/skit/cli.py:5591 src/skit/tui_prefs.py:355 msgid "npm registry URL" msgstr "npm registry URL" -#: src/skit/cli.py:5579 +#: src/skit/cli.py:5607 msgid "Network to PyPI / GitHub looks slow or blocked." msgstr "偵測到存取 PyPI / GitHub 緩慢或受阻。" -#: src/skit/cli.py:5581 +#: src/skit/cli.py:5609 msgid "Configure mirrors for faster installs (mainland China)?" msgstr "是否設定鏡像以加速下載(中國大陸)?" @@ -2018,59 +2036,59 @@ msgstr "%(name)s 需要%(type)s——你輸入的是 %(value)r。" msgid "%(name)s must be one of: %(choices)s" msgstr "%(name)s 必須是其中之一:%(choices)s" -#: src/skit/flows.py:709 +#: src/skit/flows.py:712 #, python-format msgid "%(name)s reads from the environment variable %(env)s, but it isn't set." msgstr "%(name)s 要從環境變數 %(env)s 讀取,但它尚未設定。" -#: src/skit/flows.py:906 +#: src/skit/flows.py:915 #, python-format msgid "→ inject: %(pairs)s" msgstr "→ 注入:%(pairs)s" -#: src/skit/flows.py:909 +#: src/skit/flows.py:918 msgid " (written to a temporary copy, deleted after the run; your original file is untouched)" msgstr " (寫進臨時副本,跑完即刪;你的原始檔不變)" -#: src/skit/flows.py:969 +#: src/skit/flows.py:978 msgid "Secret-marked values are never saved by skit, but this prompt sends them to the selected agent as plaintext; the agent may log or sync them." msgstr "標記為機密的值不會由 skit 儲存,但此提示詞會以明文將它們傳送給所選 agent;agent 可能記錄或同步這些值。" -#: src/skit/flows.py:980 +#: src/skit/flows.py:989 #, python-format msgid "%(name)s can't contain a line break: a shell `read` takes ONE line, so everything after the break would be thrown away." msgstr "%(name)s 不能包含換行:shell 的 `read` 只讀取一行,換行之後的內容會被丟棄。" -#: src/skit/flows.py:984 +#: src/skit/flows.py:993 #, python-format msgid "%(name)s is read on the same line as other values, so its value can't contain spaces or tabs — the shell would split it across the other fields. Only the LAST value on a `read` line may contain spaces." msgstr "%(name)s 與其他值讀在同一行,所以它的值不能包含空格或定位字元——shell 會把它拆散到其他欄位裡。一行 `read` 中只有最後一個值可以包含空格。" -#: src/skit/flows.py:989 +#: src/skit/flows.py:998 #, python-format msgid "%(name)s starts or ends with a space or tab, which a shell `read` strips off the line — the script would receive it trimmed. Remove the surrounding whitespace." msgstr "%(name)s 以空格或定位字元開頭或結尾,而 shell 的 `read` 會把行首尾的空白剝掉——腳本收到的會是被修剪過的值。請去掉兩端的空白。" -#: src/skit/flows.py:1051 +#: src/skit/flows.py:1060 msgid "The built-in amp runner is one-shot: amp -x runs this prompt once and does not open an interactive session." msgstr "內建 amp 執行器為單次執行模式:amp -x 只執行此提示詞一次,不會開啟互動工作階段。" -#: src/skit/flows.py:1098 +#: src/skit/flows.py:1107 #, python-format msgid "%(value)s isn't a valid %(type)s for %(param)s." msgstr "%(value)s 不是 %(param)s 的有效 %(type)s 值。" -#: src/skit/flows.py:1110 +#: src/skit/flows.py:1119 #, python-format msgid "%(empty)s is empty, but %(filled)s is filled and they are read on the same line — a shell `read` would hand your value to %(empty)s. Fill %(empty)s in, or clear %(filled)s." msgstr "%(empty)s 是空的,但 %(filled)s 有值,而它們由同一行 read 讀取——shell 的 `read` 會把你的值給到 %(empty)s。請填上 %(empty)s,或清空 %(filled)s。" -#: src/skit/flows.py:1124 +#: src/skit/flows.py:1133 #, python-format msgid "skit refused to run its own injected copy: %(detail)s" msgstr "skit 拒絕執行自己注入出來的副本:%(detail)s" -#: src/skit/flows.py:1132 +#: src/skit/flows.py:1141 #, python-format msgid "The script and its form definitions don't match anymore: %(detail)s. Run `skit params %(name)s --resync` to fix it." msgstr "腳本內容和表單定義對不上了:%(detail)s。執行 `skit params %(name)s --resync` 即可修復。" @@ -2350,7 +2368,7 @@ msgstr "%(name)s 沒有原始檔案——origin 不適用於它的種類。" msgid "%(name)s has no stored copy — store doesn't apply to its kind." msgstr "%(name)s 沒有儲存的副本——store 不適用於它的種類。" -#: src/skit/store.py:551 src/skit/tui_settings.py:519 +#: src/skit/store.py:551 src/skit/tui_settings.py:514 msgid "The working directory must be origin, store, invoke, or an absolute path." msgstr "工作目錄必須是 origin、store、invoke,或一個絕對路徑。" @@ -2364,7 +2382,7 @@ msgstr "%(name)s 不是透過可釘選的直譯器執行。" msgid "%(name)s isn't a command entry." msgstr "%(name)s 不是 command 項目。" -#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:947 +#: src/skit/store.py:599 src/skit/store.py:666 src/skit/tui_settings.py:942 msgid "Command template must not be empty" msgstr "命令模板不可為空" @@ -2398,22 +2416,22 @@ msgstr "%(name)s 是 reference 模式條目:它從自己的專案執行,套 msgid "%(name)s's stored copy isn't valid UTF-8, so skit can't rewrite the script's own dependency block — and that block is what uv reads. Edit it in the script itself: skit edit %(name)s" msgstr "%(name)s 儲存的副本不是合法的 UTF-8,skit 沒辦法改寫腳本自己的依賴區塊——而 uv 讀的正是那個區塊。請直接在腳本裡改:skit edit %(name)s" -#: src/skit/store.py:1150 src/skit/tui_settings.py:1013 +#: src/skit/store.py:1148 src/skit/tui_settings.py:1008 #, python-format msgid "The name %(name)s is already taken." msgstr "名稱 %(name)s 已被使用。" -#: src/skit/store.py:1192 +#: src/skit/store.py:1190 #, python-format msgid "%(slug)s: meta.toml is missing; skipped" msgstr "%(slug)s:缺 meta.toml,已略過" -#: src/skit/store.py:1198 +#: src/skit/store.py:1196 #, python-format msgid "%(slug)s: meta.toml is corrupt (%(error)s); skipped" msgstr "%(slug)s:meta.toml 損毀(%(error)s),已略過" -#: src/skit/store.py:1204 +#: src/skit/store.py:1202 #, python-format msgid "%(slug)s: the referenced source file is gone: %(path)s" msgstr "%(slug)s:reference 原檔已消失:%(path)s" @@ -2427,7 +2445,7 @@ msgstr "環境變數 %(name)s 尚未設定(%(token)s 需要它)。" msgid "Runner picked on the run form" msgstr "執行器在執行表單上選擇" -#: src/skit/tui.py:81 src/skit/tui_settings.py:556 src/skit/tui_settings.py:579 +#: src/skit/tui.py:81 src/skit/tui_settings.py:551 src/skit/tui_settings.py:574 #, python-format msgid "%(runner)s (no longer configured)" msgstr "%(runner)s(已不在設定中)" @@ -2457,14 +2475,14 @@ msgid "%(days)s d ago" msgstr "%(days)s 天前" #: src/skit/tui.py:125 src/skit/tui.py:162 src/skit/tui.py:204 -#: src/skit/tui.py:330 src/skit/tui.py:476 src/skit/tui_runner.py:210 +#: src/skit/tui.py:333 src/skit/tui.py:483 src/skit/tui_runner.py:210 #: src/skit/tui_runner.py:262 src/skit/tui_runner.py:287 #: src/skit/tui_runner.py:334 msgid "Remove" msgstr "移除" -#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:157 -#: src/skit/tui_add.py:181 src/skit/tui_runner.py:288 +#: src/skit/tui.py:126 src/skit/tui.py:163 src/skit/tui_add.py:158 +#: src/skit/tui_add.py:182 src/skit/tui_runner.py:288 #: src/skit/tui_runner.py:335 msgid "Keep" msgstr "保留" @@ -2477,8 +2495,8 @@ msgstr "你的原始檔不會被刪除。" msgid "Close" msgstr "關閉" -#: src/skit/tui.py:199 src/skit/tui.py:333 src/skit/tui.py:462 -#: src/skit/tui.py:471 src/skit/tui_form.py:554 src/skit/tui_form.py:555 +#: src/skit/tui.py:199 src/skit/tui.py:336 src/skit/tui.py:469 +#: src/skit/tui.py:478 src/skit/tui_form.py:554 src/skit/tui_form.py:555 #: src/skit/tui_form.py:723 msgid "Run" msgstr "執行" @@ -2487,202 +2505,202 @@ msgstr "執行" msgid "Rerun with last values" msgstr "用上次的值重跑" -#: src/skit/tui.py:201 src/skit/tui.py:335 src/skit/tui.py:474 +#: src/skit/tui.py:201 src/skit/tui.py:338 src/skit/tui.py:481 msgid "Entry settings" msgstr "條目設定" -#: src/skit/tui.py:202 src/skit/tui.py:336 src/skit/tui.py:479 -#: src/skit/tui_settings.py:801 +#: src/skit/tui.py:202 src/skit/tui.py:339 src/skit/tui.py:486 +#: src/skit/tui_settings.py:796 msgid "Presets" msgstr "參數組合" -#: src/skit/tui.py:203 src/skit/tui.py:331 src/skit/tui.py:332 -#: src/skit/tui.py:475 +#: src/skit/tui.py:203 src/skit/tui.py:334 src/skit/tui.py:335 +#: src/skit/tui.py:482 msgid "Edit source" msgstr "編輯來源" -#: src/skit/tui.py:205 src/skit/tui.py:337 src/skit/tui.py:478 +#: src/skit/tui.py:205 src/skit/tui.py:340 src/skit/tui.py:485 msgid "Add entry" msgstr "加入條目" -#: src/skit/tui.py:206 src/skit/tui.py:341 src/skit/tui.py:480 +#: src/skit/tui.py:206 src/skit/tui.py:344 src/skit/tui.py:487 msgid "Search" msgstr "搜尋" -#: src/skit/tui.py:207 src/skit/tui.py:345 src/skit/tui.py:372 -#: src/skit/tui.py:483 src/skit/tui.py:1063 +#: src/skit/tui.py:207 src/skit/tui.py:348 src/skit/tui.py:379 +#: src/skit/tui.py:490 src/skit/tui.py:1092 msgid "Detail pane" msgstr "詳情欄" -#: src/skit/tui.py:208 src/skit/tui.py:338 src/skit/tui.py:484 -#: src/skit/tui_prefs.py:155 +#: src/skit/tui.py:208 src/skit/tui.py:341 src/skit/tui.py:491 +#: src/skit/tui_prefs.py:156 msgid "Preferences" msgstr "偏好設定" -#: src/skit/tui.py:209 src/skit/tui.py:339 src/skit/tui.py:485 +#: src/skit/tui.py:209 src/skit/tui.py:342 src/skit/tui.py:492 #: src/skit/tui_health.py:55 msgid "Health check" msgstr "健康檢查" -#: src/skit/tui.py:210 src/skit/tui.py:322 src/skit/tui.py:323 +#: src/skit/tui.py:210 src/skit/tui.py:325 src/skit/tui.py:326 msgid "Quit" msgstr "離開" -#: src/skit/tui.py:256 src/skit/tui.py:371 src/skit/tui.py:1056 -#: src/skit/tui.py:1062 +#: src/skit/tui.py:259 src/skit/tui.py:378 src/skit/tui.py:1085 +#: src/skit/tui.py:1091 msgid "Library" msgstr "工具庫" -#: src/skit/tui.py:334 src/skit/tui.py:473 +#: src/skit/tui.py:337 src/skit/tui.py:480 msgid "Rerun" msgstr "重跑" -#: src/skit/tui.py:381 src/skit/tui.py:1058 +#: src/skit/tui.py:388 src/skit/tui.py:1087 msgid "/ to search names and descriptions…" msgstr "/ 搜尋名稱或說明…" -#: src/skit/tui.py:436 src/skit/tui.py:519 +#: src/skit/tui.py:443 src/skit/tui.py:534 msgid "Your entries will appear here." msgstr "你的條目會顯示在這裡。" -#: src/skit/tui.py:439 +#: src/skit/tui.py:446 #, python-format msgid "%(shown)s/%(total)s entry" msgid_plural "%(shown)s/%(total)s entries" msgstr[0] "%(shown)s/%(total)s 個條目" -#: src/skit/tui.py:463 +#: src/skit/tui.py:470 msgid "Back to list" msgstr "回到清單" -#: src/skit/tui.py:521 +#: src/skit/tui.py:536 msgid "Press a to add the first one," msgstr "按 a 加入第一個," -#: src/skit/tui.py:522 +#: src/skit/tui.py:537 msgid "or run: skit add in a terminal." msgstr "或在終端執行 skit add <路徑>。" -#: src/skit/tui.py:538 +#: src/skit/tui.py:553 msgid "The copy is kept by skit; your original file is never modified." msgstr "副本由 skit 保管;你的原始檔永遠不會被改動。" -#: src/skit/tui.py:542 src/skit/tui_settings.py:436 +#: src/skit/tui.py:557 src/skit/tui_settings.py:431 #, python-format msgid "Linked to the original: %(path)s" msgstr "連結原檔:%(path)s" -#: src/skit/tui.py:552 +#: src/skit/tui.py:567 msgid "(no description — add one in Entry settings)" msgstr "(沒有說明——可在條目設定中加入)" -#: src/skit/tui.py:571 +#: src/skit/tui.py:586 #, python-format msgid "Parameters %(summary)s" msgstr "參數 %(summary)s" -#: src/skit/tui.py:574 +#: src/skit/tui.py:589 #, python-format msgid "Presets %(names)s" msgstr "參數組合 %(names)s" -#: src/skit/tui.py:583 +#: src/skit/tui.py:598 #, python-format msgid "Depends on %(deps)s" msgstr "依賴 %(deps)s" -#: src/skit/tui.py:589 +#: src/skit/tui.py:604 msgid "finished" msgstr "完成" -#: src/skit/tui.py:591 +#: src/skit/tui.py:606 #, python-format msgid "failed (code %(code)s)" msgstr "失敗(代碼 %(code)s)" -#: src/skit/tui.py:594 +#: src/skit/tui.py:609 #, python-format msgid "Last run %(when)s · %(outcome)s" msgstr "上次執行 %(when)s · %(outcome)s" -#: src/skit/tui.py:598 +#: src/skit/tui.py:613 msgid "Not run yet" msgstr "還沒執行過" -#: src/skit/tui.py:604 +#: src/skit/tui.py:619 msgid "The script changed — skit checks the form against it before every run." msgstr "腳本改過了——每次執行前 skit 會自動核對表單。" -#: src/skit/tui.py:692 +#: src/skit/tui.py:707 msgid "Press Ctrl+C again to quit" msgstr "再按一次 Ctrl+C 離開" -#: src/skit/tui.py:712 src/skit/tui.py:743 src/skit/tui.py:755 -#: src/skit/tui.py:793 src/skit/tui.py:820 src/skit/tui.py:844 -#: src/skit/tui.py:898 src/skit/tui.py:963 src/skit/tui.py:1087 +#: src/skit/tui.py:727 src/skit/tui.py:758 src/skit/tui.py:770 +#: src/skit/tui.py:808 src/skit/tui.py:843 src/skit/tui.py:871 +#: src/skit/tui.py:927 src/skit/tui.py:992 src/skit/tui.py:1116 #, python-format msgid "Error: %(error)s" msgstr "錯誤:%(error)s" -#: src/skit/tui.py:723 +#: src/skit/tui.py:738 msgid "A prompt needs a configured agent to run with." msgstr "提示詞需要一個已設定的 agent 才能執行。" -#: src/skit/tui.py:744 +#: src/skit/tui.py:759 msgid "The runner is no longer configured." msgstr "該執行器已不在設定中。" -#: src/skit/tui.py:781 +#: src/skit/tui.py:796 #, python-format msgid "%(name)s hasn't run yet — press Enter to fill the form first." msgstr "%(name)s 還沒執行過——先按 Enter 填一次表單。" -#: src/skit/tui.py:829 src/skit/tui.py:1070 src/skit/tui_form.py:653 +#: src/skit/tui.py:856 src/skit/tui.py:1099 src/skit/tui_form.py:653 #, python-format msgid "Run %(name)s" msgstr "執行 %(name)s" -#: src/skit/tui.py:852 +#: src/skit/tui.py:879 #, python-format msgid "Last: %(name)s ✗ couldn't launch" msgstr "上次:%(name)s ✗ 無法啟動" -#: src/skit/tui.py:858 +#: src/skit/tui.py:887 #, python-format msgid "Last: %(name)s ✓ finished" msgstr "上次:%(name)s ✓ 完成" -#: src/skit/tui.py:860 +#: src/skit/tui.py:889 #, python-format msgid "Last: %(name)s ✗ failed (code %(code)s)" msgstr "上次:%(name)s ✗ 失敗(代碼 %(code)s)" -#: src/skit/tui.py:867 +#: src/skit/tui.py:896 msgid "✓ finished" msgstr "✓ 完成" -#: src/skit/tui.py:869 +#: src/skit/tui.py:898 #, python-format msgid "✗ failed (code %(code)s)" msgstr "✗ 失敗(代碼 %(code)s)" -#: src/skit/tui.py:870 +#: src/skit/tui.py:899 msgid "✗ couldn't launch" msgstr "✗ 無法啟動" -#: src/skit/tui.py:902 src/skit/tui.py:922 +#: src/skit/tui.py:931 src/skit/tui.py:951 #, python-format msgid "Edited %(name)s." msgstr "已編輯 %(name)s。" -#: src/skit/tui.py:974 +#: src/skit/tui.py:1003 msgid "✓ added" msgstr "✓ 已加入" -#: src/skit/tui_add.py:70 src/skit/tui_add.py:139 src/skit/tui_add.py:197 -#: src/skit/tui_add.py:320 src/skit/tui_add.py:601 src/skit/tui_add.py:654 -#: src/skit/tui_add.py:680 src/skit/tui_add.py:900 src/skit/tui_add.py:1207 -#: src/skit/tui_add.py:1364 src/skit/tui_form.py:326 src/skit/tui_form.py:348 +#: src/skit/tui_add.py:71 src/skit/tui_add.py:140 src/skit/tui_add.py:198 +#: src/skit/tui_add.py:321 src/skit/tui_add.py:602 src/skit/tui_add.py:655 +#: src/skit/tui_add.py:681 src/skit/tui_add.py:910 src/skit/tui_add.py:1219 +#: src/skit/tui_add.py:1380 src/skit/tui_form.py:326 src/skit/tui_form.py:348 #: src/skit/tui_form.py:392 src/skit/tui_form.py:447 src/skit/tui_form.py:481 #: src/skit/tui_form.py:511 src/skit/tui_form.py:547 src/skit/tui_form.py:737 #: src/skit/tui_pathpick.py:285 src/skit/tui_pathpick.py:341 @@ -2692,236 +2710,236 @@ msgstr "✓ 已加入" msgid "Cancel" msgstr "取消" -#: src/skit/tui_add.py:156 src/skit/tui_add.py:180 +#: src/skit/tui_add.py:157 src/skit/tui_add.py:181 msgid "Delete" msgstr "刪除" -#: src/skit/tui_add.py:176 +#: src/skit/tui_add.py:177 #, python-format msgid "Delete the draft \"%(name)s\"? It is the only copy." msgstr "刪除草稿「%(name)s」?這是唯一的副本。" -#: src/skit/tui_add.py:200 +#: src/skit/tui_add.py:201 msgid "Write a new script" msgstr "撰寫新腳本" -#: src/skit/tui_add.py:201 +#: src/skit/tui_add.py:202 msgid "Draft a prompt" msgstr "草擬提示詞" -#: src/skit/tui_add.py:204 +#: src/skit/tui_add.py:205 msgid "Delete a kept draft" msgstr "刪除一份保留的草稿" -#: src/skit/tui_add.py:226 +#: src/skit/tui_add.py:227 msgid "Add an entry" msgstr "加入條目" -#: src/skit/tui_add.py:234 +#: src/skit/tui_add.py:235 msgid "Path to a script, executable, or prompt:" msgstr "腳本、可執行檔或提示詞的路徑:" -#: src/skit/tui_add.py:238 +#: src/skit/tui_add.py:239 #, python-brace-format msgid "…or register a command template below (e.g. ffmpeg -i {input}):" msgstr "……或在下方登記一條命令模板(例如 ffmpeg -i {input}):" -#: src/skit/tui_add.py:287 +#: src/skit/tui_add.py:288 msgid "…or resume a kept draft:" msgstr "…或接續一份保留的草稿:" -#: src/skit/tui_add.py:300 +#: src/skit/tui_add.py:301 msgid "Delete draft…" msgstr "刪除草稿…" -#: src/skit/tui_add.py:305 +#: src/skit/tui_add.py:306 msgid "…or start from a blank page:" msgstr "…或從空白頁開始:" -#: src/skit/tui_add.py:310 +#: src/skit/tui_add.py:311 msgid "Write a script…" msgstr "撰寫腳本…" -#: src/skit/tui_add.py:311 +#: src/skit/tui_add.py:312 msgid "Draft a prompt…" msgstr "草擬提示詞…" -#: src/skit/tui_add.py:319 +#: src/skit/tui_add.py:320 msgid "Continue" msgstr "繼續" -#: src/skit/tui_add.py:496 +#: src/skit/tui_add.py:497 #, python-format msgid "Deleted the draft %(name)s." msgstr "已刪除草稿 %(name)s。" -#: src/skit/tui_add.py:538 +#: src/skit/tui_add.py:539 msgid "Nothing was written, so nothing was added." msgstr "沒有寫入任何內容,因此沒有加入任何條目。" -#: src/skit/tui_add.py:602 src/skit/tui_add.py:653 src/skit/tui_add.py:684 -#: src/skit/tui_add.py:888 src/skit/tui_add.py:1210 src/skit/tui_add.py:1361 +#: src/skit/tui_add.py:603 src/skit/tui_add.py:654 src/skit/tui_add.py:685 +#: src/skit/tui_add.py:898 src/skit/tui_add.py:1222 src/skit/tui_add.py:1377 msgid "Add" msgstr "加入" -#: src/skit/tui_add.py:631 src/skit/tui_add.py:805 src/skit/tui_add.py:1295 +#: src/skit/tui_add.py:632 src/skit/tui_add.py:815 src/skit/tui_add.py:1311 #, python-format msgid "Add %(name)s" msgstr "加入 %(name)s" -#: src/skit/tui_add.py:643 +#: src/skit/tui_add.py:644 msgid "(shown in the Library — you can write one line)" msgstr "(顯示在工具庫中——可以自己寫一句)" -#: src/skit/tui_add.py:647 +#: src/skit/tui_add.py:648 msgid "The program runs from its own location; skit never copies a binary." msgstr "程式從它自己的位置執行;skit 永遠不會複製二進位檔。" -#: src/skit/tui_add.py:683 src/skit/tui_add.py:899 +#: src/skit/tui_add.py:684 src/skit/tui_add.py:909 msgid "Edit script" msgstr "編輯腳本" -#: src/skit/tui_add.py:833 +#: src/skit/tui_add.py:843 msgid "Link the original: skit never writes to the file." msgstr "連結原檔:skit 永遠不會寫入這個檔案。" -#: src/skit/tui_add.py:836 +#: src/skit/tui_add.py:846 msgid "Link the original: parameter setup is skipped — skit never writes to the file." msgstr "連結原檔:略過參數設定——skit 永遠不會寫入這個檔案。" -#: src/skit/tui_add.py:841 +#: src/skit/tui_add.py:851 msgid "npm dependencies apply to stored copies only, so none are recorded." msgstr "npm 相依套件只適用於儲存的複本,因此不會記錄任何相依套件。" -#: src/skit/tui_add.py:860 +#: src/skit/tui_add.py:870 msgid "(the script has no docstring — you can write one line)" msgstr "(腳本開頭沒有說明文字,可以自己寫一句)" -#: src/skit/tui_add.py:865 src/skit/tui_add.py:1373 -#: src/skit/tui_settings.py:427 +#: src/skit/tui_add.py:875 src/skit/tui_add.py:1389 +#: src/skit/tui_settings.py:422 msgid "Storage" msgstr "存法" -#: src/skit/tui_add.py:872 src/skit/tui_add.py:1377 +#: src/skit/tui_add.py:882 src/skit/tui_add.py:1393 msgid "Keep a copy — skit stores it; your original file is never modified" msgstr "複製一份——skit 保管副本,你的原始檔永遠不會被改動" -#: src/skit/tui_add.py:878 +#: src/skit/tui_add.py:888 msgid "Link the original — edits take effect immediately, but skit won't write to the file, so parameter definitions are yours to maintain" msgstr "連結原檔——改動立即生效;但 skit 不寫入這個檔案,參數定義要自己維護" -#: src/skit/tui_add.py:897 src/skit/tui_add.py:1362 +#: src/skit/tui_add.py:907 src/skit/tui_add.py:1378 msgid "Toggle" msgstr "切換" -#: src/skit/tui_add.py:916 src/skit/tui_settings.py:835 +#: src/skit/tui_add.py:926 src/skit/tui_settings.py:830 msgid "Dependencies" msgstr "依賴" -#: src/skit/tui_add.py:921 +#: src/skit/tui_add.py:931 msgid "comma separated, e.g. chalk, @scope/pkg" msgstr "逗號分隔,例如 chalk, @scope/pkg" -#: src/skit/tui_add.py:925 src/skit/tui_add.py:951 +#: src/skit/tui_add.py:935 src/skit/tui_add.py:961 msgid "detected from the script's imports — edit freely" msgstr "偵測自腳本的 import——可自由修改" -#: src/skit/tui_add.py:932 +#: src/skit/tui_add.py:942 msgid "The script declares its own dependencies (PEP 723):" msgstr "腳本已用 PEP 723 語法聲明依賴:" -#: src/skit/tui_add.py:935 +#: src/skit/tui_add.py:945 #, python-format msgid "needs Python %(python)s" msgstr "需要 Python %(python)s" -#: src/skit/tui_add.py:938 +#: src/skit/tui_add.py:948 #, python-format msgid "installs %(dep)s" msgstr "會自動裝 %(dep)s" -#: src/skit/tui_add.py:940 +#: src/skit/tui_add.py:950 msgid "(none declared)" msgstr "(沒有聲明任何依賴)" -#: src/skit/tui_add.py:947 src/skit/tui_settings.py:838 +#: src/skit/tui_add.py:957 src/skit/tui_settings.py:833 msgid "comma separated, e.g. requests>=2,<3, rich" msgstr "逗號分隔,例如 requests>=2,<3, rich" -#: src/skit/tui_add.py:959 +#: src/skit/tui_add.py:969 msgid "(automatic)" msgstr "(自動)" -#: src/skit/tui_add.py:964 +#: src/skit/tui_add.py:974 msgid "Python version (requires-python) — prefilled from the #! line when it pins one; empty means automatic" msgstr "Python 版本(requires-python)——若 #! 行有指定版本則預先填入;留空表示自動" -#: src/skit/tui_add.py:973 +#: src/skit/tui_add.py:983 msgid "Parameters" msgstr "參數" -#: src/skit/tui_add.py:1003 src/skit/tui_add.py:1413 +#: src/skit/tui_add.py:1013 src/skit/tui_add.py:1429 msgid "Tick the ones the run form should ask for:" msgstr "勾選執行表單要詢問的項目:" -#: src/skit/tui_add.py:1008 +#: src/skit/tui_add.py:1018 #, python-format msgid "input() #%(n)s: %(prompt)s" msgstr "input() #%(n)s:%(prompt)s" -#: src/skit/tui_add.py:1018 +#: src/skit/tui_add.py:1028 msgid "looks like a loop accumulator — probably not a parameter" msgstr "看起來是迴圈的累加變數,多半不是參數" -#: src/skit/tui_add.py:1026 +#: src/skit/tui_add.py:1036 #, python-format msgid "%(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (Ctrl+E edits it now)." msgstr "%(names)s 直接寫在程式碼裡,skit 沒辦法把它們變成表單欄位。想管理其中一個,先在腳本開頭給它取個名字,例如 OUTPUT = '…'(Ctrl+E 現在就改)。" -#: src/skit/tui_add.py:1209 src/skit/tui_add.py:1363 +#: src/skit/tui_add.py:1221 src/skit/tui_add.py:1379 msgid "Edit prompt" msgstr "編輯提示詞" -#: src/skit/tui_add.py:1211 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 -#: src/skit/tui_settings.py:270 +#: src/skit/tui_add.py:1223 src/skit/tui_form.py:561 src/skit/tui_runner.py:355 +#: src/skit/tui_settings.py:269 msgid "New agent" msgstr "新增 agent" -#: src/skit/tui_add.py:1215 src/skit/tui_add.py:1430 -#: src/skit/tui_settings.py:274 src/skit/tui_settings.py:712 +#: src/skit/tui_add.py:1231 src/skit/tui_add.py:1446 +#: src/skit/tui_settings.py:275 src/skit/tui_settings.py:707 msgid "Choose variables…" msgstr "選擇變數…" -#: src/skit/tui_add.py:1336 +#: src/skit/tui_add.py:1352 msgid "(taken from the first line — you can write your own)" msgstr "(取自第一行——也可以自己寫一句)" -#: src/skit/tui_add.py:1341 +#: src/skit/tui_add.py:1357 msgid "Variable insertion" msgstr "變數插入" -#: src/skit/tui_add.py:1343 +#: src/skit/tui_add.py:1359 msgid "Fill {{name}} placeholders from a form before each run" msgstr "每次執行前,先用表單填入 {{name}} 佔位符" -#: src/skit/tui_add.py:1349 src/skit/tui_settings.py:544 +#: src/skit/tui_add.py:1365 src/skit/tui_settings.py:539 msgid "Runner (the agent this prompt runs with)" msgstr "執行器(此提示詞使用的 AI agent)" -#: src/skit/tui_add.py:1351 src/skit/tui_add.py:1503 -#: src/skit/tui_settings.py:554 src/skit/tui_settings.py:576 +#: src/skit/tui_add.py:1367 src/skit/tui_add.py:1519 +#: src/skit/tui_settings.py:549 src/skit/tui_settings.py:571 msgid "ask on the run form" msgstr "在執行表單上選擇" -#: src/skit/tui_add.py:1382 +#: src/skit/tui_add.py:1398 msgid "Link the original — edits take effect immediately; skit never writes to the file" msgstr "連結原檔——改動立即生效;skit 永遠不會寫入這個檔案" -#: src/skit/tui_add.py:1398 +#: src/skit/tui_add.py:1414 msgid "No {{name}} placeholders detected — the body travels to the agent as written." msgstr "未偵測到 {{name}} 佔位符——正文會原封不動送達 agent。" -#: src/skit/tui_add.py:1406 +#: src/skit/tui_add.py:1422 #, python-format msgid "Detected %(count)s placeholders — probably not written for insertion. Tick only the ones you need, or untick the switch above." msgstr "偵測到 %(count)s 個佔位符——這份提示詞多半不是為插值而寫的。只勾選需要的項目,或取消上方的開關。" @@ -2962,7 +2980,7 @@ msgstr "這支腳本有 skit 無法建模的子命令——請把全部參數打 msgid "skit couldn't read this script's argument declarations — type everything into the extra-arguments field." msgstr "skit 讀不懂這支腳本的參數宣告——請把全部參數打進「額外參數」欄。" -#: src/skit/tui_form.py:195 src/skit/tui_settings.py:212 +#: src/skit/tui_form.py:195 src/skit/tui_settings.py:211 msgid "required" msgstr "必填" @@ -3008,9 +3026,9 @@ msgstr "⚠ 目前沒有符合的檔案" msgid "Save as preset" msgstr "存成參數組合" -#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:303 -#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:268 -#: src/skit/tui_settings.py:408 +#: src/skit/tui_form.py:347 src/skit/tui_prefs.py:121 src/skit/tui_prefs.py:304 +#: src/skit/tui_runner.py:133 src/skit/tui_settings.py:267 +#: src/skit/tui_settings.py:403 msgid "Save" msgstr "儲存" @@ -3102,10 +3120,10 @@ msgid "Preset \"%(preset)s\" saved." msgstr "已存成參數組合「%(preset)s」。" #: src/skit/tui_health.py:31 src/skit/tui_health.py:148 -#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:304 +#: src/skit/tui_prefs.py:120 src/skit/tui_prefs.py:305 #: src/skit/tui_runner.py:211 src/skit/tui_runner.py:263 #: src/skit/tui_runner.py:354 src/skit/tui_runner.py:422 -#: src/skit/tui_settings.py:267 src/skit/tui_settings.py:418 +#: src/skit/tui_settings.py:266 src/skit/tui_settings.py:413 msgid "Back" msgstr "返回" @@ -3172,123 +3190,123 @@ msgstr "教 AI agent 使用 skit" msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Install by hand with: skit agent install --to DIR" msgstr "沒有偵測到 agent 目錄(~/.claude、~/.codex、./.agents 等)。可用 skit agent install --to DIR 手動安裝。" -#: src/skit/tui_prefs.py:126 +#: src/skit/tui_prefs.py:127 msgid "Manage agents" msgstr "管理 agent" -#: src/skit/tui_prefs.py:127 +#: src/skit/tui_prefs.py:128 msgid "Teach an AI agent" msgstr "教 AI agent" -#: src/skit/tui_prefs.py:173 +#: src/skit/tui_prefs.py:174 #, python-format msgid "%(count)s agent configured: %(names)s" msgid_plural "%(count)s agents configured: %(names)s" msgstr[0] "已設定 %(count)s 個 agent:%(names)s" -#: src/skit/tui_prefs.py:179 +#: src/skit/tui_prefs.py:180 msgid "No agents configured." msgstr "尚未設定任何 agent。" -#: src/skit/tui_prefs.py:202 +#: src/skit/tui_prefs.py:203 msgid "Interface language" msgstr "介面語言" -#: src/skit/tui_prefs.py:204 +#: src/skit/tui_prefs.py:205 msgid "Automatic (follow the system)" msgstr "自動(跟隨系統)" -#: src/skit/tui_prefs.py:210 +#: src/skit/tui_prefs.py:211 #, python-format msgid "Currently in effect: %(locale)s" msgstr "目前生效:%(locale)s" -#: src/skit/tui_prefs.py:214 +#: src/skit/tui_prefs.py:215 msgid "Editor" msgstr "編輯器" -#: src/skit/tui_prefs.py:217 +#: src/skit/tui_prefs.py:218 msgid "e.g. code --wait (empty = use $VISUAL / $EDITOR)" msgstr "例如 code --wait(留空=用 $VISUAL / $EDITOR)" -#: src/skit/tui_prefs.py:223 +#: src/skit/tui_prefs.py:224 #, python-format msgid "Empty means: %(cmd)s (from $VISUAL / $EDITOR)" msgstr "留空表示:%(cmd)s(來自 $VISUAL / $EDITOR)" -#: src/skit/tui_prefs.py:228 +#: src/skit/tui_prefs.py:229 msgid "Interactive form" msgstr "互動表單" -#: src/skit/tui_prefs.py:231 +#: src/skit/tui_prefs.py:232 msgid "Mini form — opens in place, fully clickable" msgstr "迷你表單——就地展開、可戳可點" -#: src/skit/tui_prefs.py:235 +#: src/skit/tui_prefs.py:236 msgid "Line-by-line prompts — plainest, best over slow terminals" msgstr "逐行問答——最樸素,適合慢速終端" -#: src/skit/tui_prefs.py:240 +#: src/skit/tui_prefs.py:241 msgid "Used by terminal runs: `skit run` parameter prompts and the `skit add` review panel." msgstr "用於終端機流程:`skit run` 的參數詢問與 `skit add` 的檢視面板。" -#: src/skit/tui_prefs.py:246 +#: src/skit/tui_prefs.py:247 msgid "After a run (from this menu)" msgstr "執行結束後(從這個選單啟動時)" -#: src/skit/tui_prefs.py:249 +#: src/skit/tui_prefs.py:250 msgid "Quit skit — leave the run's output in the terminal" msgstr "退出 skit——將本次執行的輸出留在終端機" -#: src/skit/tui_prefs.py:253 +#: src/skit/tui_prefs.py:254 msgid "Return to the Library immediately" msgstr "立即回到工具庫" -#: src/skit/tui_prefs.py:257 +#: src/skit/tui_prefs.py:258 msgid "JavaScript runtime" msgstr "JavaScript 執行環境" -#: src/skit/tui_prefs.py:261 +#: src/skit/tui_prefs.py:262 msgid "Automatic — the first of deno / bun / node found" msgstr "自動——deno / bun / node 中第一個找到的" -#: src/skit/tui_prefs.py:267 +#: src/skit/tui_prefs.py:268 msgid "Runs js/ts entries that don't pin their own runtime." msgstr "用來執行未自行釘選執行環境的 js/ts 項目。" -#: src/skit/tui_prefs.py:274 +#: src/skit/tui_prefs.py:275 msgid "Shell on Windows" msgstr "Windows 上的 Shell" -#: src/skit/tui_prefs.py:277 +#: src/skit/tui_prefs.py:278 msgid "Path to bash.exe (empty = Git Bash / WSL detection)" msgstr "bash.exe 的路徑(留空 = 偵測 Git Bash / WSL)" -#: src/skit/tui_prefs.py:281 +#: src/skit/tui_prefs.py:282 msgid "Shell scripts need an explicit bash here." msgstr "Shell 腳本在這裡需要明確指定 bash。" -#: src/skit/tui_prefs.py:286 src/skit/tui_runner.py:381 +#: src/skit/tui_prefs.py:287 src/skit/tui_runner.py:381 msgid "Agents (prompt runners)" msgstr "agent(提示詞執行器)" -#: src/skit/tui_prefs.py:290 +#: src/skit/tui_prefs.py:291 msgid "Manage agents…" msgstr "管理 agent…" -#: src/skit/tui_prefs.py:292 +#: src/skit/tui_prefs.py:293 msgid "Teach an AI agent skit…" msgstr "教 AI agent 使用 skit…" -#: src/skit/tui_prefs.py:316 +#: src/skit/tui_prefs.py:317 msgid "Download mirrors (mainland-China acceleration)" msgstr "下載鏡像(中國大陸加速)" -#: src/skit/tui_prefs.py:318 +#: src/skit/tui_prefs.py:319 msgid "Each ecosystem is its own choice — mirror vendors differ per axis." msgstr "每個生態系各自選擇——不同生態系的鏡像供應商並不相同。" -#: src/skit/tui_prefs.py:323 +#: src/skit/tui_prefs.py:324 msgid "Master switch — \"off\" pauses mirrors but keeps the saved URLs." msgstr "總開關——「off」暫停鏡像但保留已存的 URL。" @@ -3362,221 +3380,221 @@ msgstr "執行器設定已發生變更;沒有移除任何內容。請重新選 msgid "New agent…" msgstr "新增 agent…" -#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 +#: src/skit/tui_settings.py:43 src/skit/tui_settings.py:59 msgid "Discard" msgstr "放棄" -#: src/skit/tui_settings.py:45 src/skit/tui_settings.py:61 +#: src/skit/tui_settings.py:44 src/skit/tui_settings.py:60 msgid "Keep editing" msgstr "繼續編輯" -#: src/skit/tui_settings.py:57 +#: src/skit/tui_settings.py:56 msgid "Discard unsaved changes?" msgstr "放棄未儲存的變更?" -#: src/skit/tui_settings.py:103 src/skit/tui_settings.py:214 +#: src/skit/tui_settings.py:102 src/skit/tui_settings.py:213 msgid "Form label:" msgstr "欄位提示:" -#: src/skit/tui_settings.py:107 src/skit/tui_settings.py:218 +#: src/skit/tui_settings.py:106 src/skit/tui_settings.py:217 msgid "secret (never saved to disk)" msgstr "機密(不會存檔)" -#: src/skit/tui_settings.py:111 src/skit/tui_settings.py:222 +#: src/skit/tui_settings.py:110 src/skit/tui_settings.py:221 msgid "env variable to read it from (optional)" msgstr "從哪個環境變數讀取(選填)" -#: src/skit/tui_settings.py:137 src/skit/tui_settings.py:257 +#: src/skit/tui_settings.py:136 src/skit/tui_settings.py:256 msgid "Anything skit previously remembered for this value will be deleted too." msgstr "skit 之前記住的這個值也會一併刪掉。" -#: src/skit/tui_settings.py:177 +#: src/skit/tui_settings.py:176 msgid "Type:" msgstr "類型:" -#: src/skit/tui_settings.py:180 +#: src/skit/tui_settings.py:179 msgid "type: str / int / float / bool / choice / path" msgstr "類型:str / int / float / bool / choice / path" -#: src/skit/tui_settings.py:184 +#: src/skit/tui_settings.py:183 msgid "Default:" msgstr "預設值:" -#: src/skit/tui_settings.py:187 +#: src/skit/tui_settings.py:186 msgid "default value (optional)" msgstr "預設值(選填)" -#: src/skit/tui_settings.py:191 +#: src/skit/tui_settings.py:190 msgid "Choices:" msgstr "可選值:" -#: src/skit/tui_settings.py:194 +#: src/skit/tui_settings.py:193 msgid "comma separated (for type: choice)" msgstr "逗號分隔(用於 type: choice)" -#: src/skit/tui_settings.py:198 +#: src/skit/tui_settings.py:197 msgid "Help:" msgstr "說明:" -#: src/skit/tui_settings.py:201 +#: src/skit/tui_settings.py:200 msgid "one-line help shown under the field (optional)" msgstr "顯示在欄位下方的一行說明(選填)" -#: src/skit/tui_settings.py:206 +#: src/skit/tui_settings.py:205 msgid "Flag:" msgstr "選項:" -#: src/skit/tui_settings.py:209 +#: src/skit/tui_settings.py:208 msgid "--flag (empty = positional)" msgstr "--flag(留空為位置參數)" -#: src/skit/tui_settings.py:269 src/skit/tui_settings.py:416 +#: src/skit/tui_settings.py:268 src/skit/tui_settings.py:411 msgid "Resync" msgstr "重新同步" -#: src/skit/tui_settings.py:390 +#: src/skit/tui_settings.py:385 msgid "Basics" msgstr "基本資料" -#: src/skit/tui_settings.py:393 +#: src/skit/tui_settings.py:388 msgid "Renaming keeps everything — remembered values, presets, the stored copy." msgstr "改名不影響任何東西——記住的值、參數組合、保管的副本都會保留。" -#: src/skit/tui_settings.py:398 +#: src/skit/tui_settings.py:393 msgid "Description (shown in the Library)" msgstr "說明(顯示在工具庫)" -#: src/skit/tui_settings.py:430 +#: src/skit/tui_settings.py:425 #, python-format msgid "Keep a copy — your original file is never modified. Source: %(path)s" msgstr "複製一份——你的原始檔永遠不會被改動。來源:%(path)s" -#: src/skit/tui_settings.py:447 +#: src/skit/tui_settings.py:442 msgid "Run in (working directory)" msgstr "執行位置(工作目錄)" -#: src/skit/tui_settings.py:453 +#: src/skit/tui_settings.py:448 msgid "The source file's folder" msgstr "來源檔案所在的資料夾" -#: src/skit/tui_settings.py:455 +#: src/skit/tui_settings.py:450 msgid "skit's stored-copy folder" msgstr "skit 保管副本的資料夾" -#: src/skit/tui_settings.py:456 +#: src/skit/tui_settings.py:451 msgid "Wherever skit is run from" msgstr "執行 skit 的所在位置" -#: src/skit/tui_settings.py:457 +#: src/skit/tui_settings.py:452 msgid "A fixed folder (type it below)" msgstr "固定資料夾(在下方輸入)" -#: src/skit/tui_settings.py:468 +#: src/skit/tui_settings.py:463 msgid "/absolute/path" msgstr "/絕對/路徑" -#: src/skit/tui_settings.py:472 +#: src/skit/tui_settings.py:467 msgid "Interpreter / runtime" msgstr "直譯器 / 執行環境" -#: src/skit/tui_settings.py:475 +#: src/skit/tui_settings.py:470 msgid "empty = automatic (shebang, then detection order)" msgstr "留空 = 自動(先看 shebang,再依偵測順序)" -#: src/skit/tui_settings.py:589 +#: src/skit/tui_settings.py:584 msgid "Parameters (the run form's fields)" msgstr "參數(執行表單的欄位)" -#: src/skit/tui_settings.py:595 +#: src/skit/tui_settings.py:590 msgid "(programs have no managed parameters)" msgstr "(程式沒有管理的參數)" -#: src/skit/tui_settings.py:600 +#: src/skit/tui_settings.py:595 msgid "skit doesn't write to this file — maintain the [tool.skit] definitions in the source directly." msgstr "skit 不寫入這個檔案——請直接在原始碼裡維護 [tool.skit] 定義。" -#: src/skit/tui_settings.py:619 +#: src/skit/tui_settings.py:614 msgid "This script's run form comes from its own command-line arguments. Managing a hardcoded constant here would replace that form — leave it as is." msgstr "這支腳本的執行表單來自它自己的命令列參數。在這裡管理一個寫死的常數會取代那張表單——維持原樣即可。" -#: src/skit/tui_settings.py:625 src/skit/tui_settings.py:694 +#: src/skit/tui_settings.py:620 src/skit/tui_settings.py:689 msgid "Detected but not yet managed — tick to manage:" msgstr "偵測到但尚未管理——勾選即可管理:" -#: src/skit/tui_settings.py:634 +#: src/skit/tui_settings.py:629 msgid "Every input() is managed — this script can now run with --no-input." msgstr "所有 input() 都已管理——這支腳本現在可以用 --no-input 自動化。" -#: src/skit/tui_settings.py:650 +#: src/skit/tui_settings.py:645 #, python-brace-format msgid "Saving re-reads the {placeholders} from the template." msgstr "儲存時會從模板重新讀取 {placeholders}。" -#: src/skit/tui_settings.py:663 +#: src/skit/tui_settings.py:658 msgid "Variable insertion ({{name}} placeholders become form fields)" msgstr "變量插入({{name}} 佔位符會成為表單欄位)" -#: src/skit/tui_settings.py:674 +#: src/skit/tui_settings.py:669 msgid "Off — the body travels to the agent exactly as written." msgstr "已關閉——內文會原封不動送達 agent。" -#: src/skit/tui_settings.py:718 +#: src/skit/tui_settings.py:713 msgid "Add a parameter — type a name, then Save:" msgstr "新增參數——輸入名稱後儲存:" -#: src/skit/tui_settings.py:719 +#: src/skit/tui_settings.py:714 msgid "new parameter name" msgstr "新參數名稱" -#: src/skit/tui_settings.py:759 +#: src/skit/tui_settings.py:754 #, python-format msgid "%(name)s has an unknown type — use str, int, float, bool, choice, or path." msgstr "%(name)s 的類型未知——請使用 str、int、float、bool、choice 或 path。" -#: src/skit/tui_settings.py:771 +#: src/skit/tui_settings.py:766 #, python-format msgid "%(name)s: the default doesn't match its type." msgstr "%(name)s:預設值與其類型不符。" -#: src/skit/tui_settings.py:779 +#: src/skit/tui_settings.py:774 #, python-format msgid "%(name)s is a choice parameter but has no choices — fill its Choices field." msgstr "%(name)s 是 choice 參數但沒有可選值——請填寫它的「可選值」欄位。" -#: src/skit/tui_settings.py:805 +#: src/skit/tui_settings.py:800 msgid "None yet — press Ctrl+S inside the run form to save one." msgstr "還沒有——在執行表單裡按 Ctrl+S 就能存一組。" -#: src/skit/tui_settings.py:809 +#: src/skit/tui_settings.py:804 msgid "Untick a preset to delete it on save:" msgstr "取消勾選的參數組合會在儲存時刪除:" -#: src/skit/tui_settings.py:840 +#: src/skit/tui_settings.py:835 msgid "comma separated, e.g. chalk@^5, zod" msgstr "逗號分隔,例如 chalk@^5, zod" -#: src/skit/tui_settings.py:846 +#: src/skit/tui_settings.py:841 msgid "Python constraint, e.g. \">=3.11\" (empty = automatic)" msgstr "Python 約束,例如 \">=3.11\"(留空=自動)" -#: src/skit/tui_settings.py:862 +#: src/skit/tui_settings.py:857 msgid "Needs (external commands)" msgstr "所需命令(外部命令)" -#: src/skit/tui_settings.py:865 +#: src/skit/tui_settings.py:860 msgid "comma separated, e.g. ffmpeg, jq" msgstr "逗號分隔,例如 ffmpeg, jq" -#: src/skit/tui_settings.py:870 +#: src/skit/tui_settings.py:865 #, python-format msgid "Entry settings · %(name)s" msgstr "條目設定 · %(name)s" -#: src/skit/tui_settings.py:925 +#: src/skit/tui_settings.py:920 msgid "Everything still matches the script." msgstr "所有定義仍與腳本相符。" -#: src/skit/tui_settings.py:1080 src/skit/tui_settings.py:1099 +#: src/skit/tui_settings.py:1069 src/skit/tui_settings.py:1088 #, python-format msgid "Deleted previously remembered value(s): %(names)s" msgstr "已刪除先前記住的值:%(names)s" diff --git a/src/skit/params.py b/src/skit/params.py index 0d44a1f..3c9c699 100644 --- a/src/skit/params.py +++ b/src/skit/params.py @@ -32,6 +32,7 @@ from __future__ import annotations import math +import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal @@ -44,15 +45,25 @@ Delivery = Literal["inject", "env", "flag", "placeholder"] ParamType = Literal["str", "int", "float", "bool", "choice", "path"] -# Secret pre-check heuristic (matched against the upper-cased name / prompt). Universal: -# python candidates, shell variables, command placeholders, and declared params all run -# their names through the same rule, so "what counts as secret-looking" can never fork. -_SECRET_HINTS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD") +# Secret pre-check heuristic (matched against the name / prompt). Universal: python +# candidates, shell variables, command placeholders, reader-reflected CLI flags, and +# declared params all run their names through the same rule, so "what counts as +# secret-looking" can never fork. +# +# WHOLE words only (snake_case, kebab-case, camelCase and sentence prompts all split): +# API_KEY / githubToken / "Enter your API key:" match; MAX_TOKENS / keyword / monkey / +# maxTokens do not. The substring match this replaces turned `--max-tokens` into a +# password field — masked, never prefilled, never remembered — on the reader lane, +# where no override exists to turn it off. +_SECRET_WORDS = frozenset({"KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD"}) +# lower/digit→Upper and WORDBreak boundaries; separators (_, -, spaces, punctuation) +# are any non-letter run. +_CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") def is_secret_name(text: str) -> bool: - up = text.upper() - return any(h in up for h in _SECRET_HINTS) + words = re.split(r"[^A-Za-z]+", _CAMEL_BOUNDARY.sub(" ", text)) + return any(w.upper() in _SECRET_WORDS for w in words if w) _BINDINGS: tuple[Binding, ...] = ("const", "input", "envdefault", "none") diff --git a/src/skit/rewrite.py b/src/skit/rewrite.py index b82c685..1d24846 100644 --- a/src/skit/rewrite.py +++ b/src/skit/rewrite.py @@ -21,6 +21,8 @@ from dataclasses import dataclass from pathlib import Path +from .atomic import atomic_write_bytes_keep_mode + _UTF8 = "utf-8" # pragma: no mutate — "utf-8"/"UTF-8" codec alias # The exact set of newline sequences CPython's tokenizer/AST count as a line break: \r\n, \r, \n @@ -59,6 +61,34 @@ def restore_newline(text: str, newline: str) -> str: return text if newline == "\n" else text.replace("\n", newline) # pragma: no mutate +def read_for_block_edit(path: Path) -> tuple[str, str]: + """Read a stored copy for a comment-block edit: (LF-folded text, detected newline style). + + THE shared read half of every [tool.skit]/PEP 723 write-back — CLI onboarding, `params` + edits, the TUI add-review panel and Script settings all go through this pair instead of + re-implementing the discipline: raw bytes, surrogateescape (arbitrary shell/fish bytes + round-trip instead of burning down to U+FFFD), folded to LF for the LF-based block + engines. Pair with write_block_edit, which restores the newline style captured here.""" + raw = path.read_bytes() + text = raw.decode(_UTF8, errors="surrogateescape") # pragma: no mutate — codec alias + return text.replace("\r\n", "\n").replace("\r", "\n"), detect_newline(raw) + + +def write_block_edit(path: Path, edited: str, newline: str) -> None: + """Write back an LF-based block edit: re-apply the copy's own newline style, encode with + surrogateescape, and land atomically with the permission bits preserved (a torn write must + not truncate a stored copy; mkstemp's 0600 must not strip its execute bit). Path.write_text + is never acceptable on this path: universal-newline reads plus os.linesep re-expansion + rewrite every line of the file — CRLF-ifying an LF script on Windows — when the edit only + meant to touch the comment block.""" + atomic_write_bytes_keep_mode( + path, + restore_newline(edited, newline).encode( + _UTF8, errors="surrogateescape" + ), # pragma: no mutate — codec alias + ) + + def _physical_lines(text: str) -> list[str]: """Split text into the same physical lines (keeping line endings) that AST linenos count. diff --git a/src/skit/skills/skit/SKILL.md b/src/skit/skills/skit/SKILL.md index 7d42923..bb8e92a 100644 --- a/src/skit/skills/skit/SKILL.md +++ b/src/skit/skills/skit/SKILL.md @@ -31,8 +31,10 @@ is *the user's curated space*: treat it like their dotfiles. Propose `skit add` when you've written something reusable; don't add it silently. 5. **Pass `--no-input` on every `skit run` and `skit add`.** It guarantees those never block on a prompt; if information is missing, skit fails fast with a named error - instead. `skit remove` confirms instead of taking `--no-input` — pass `-y`. The - read commands (`list`, `show`, `params`, …) never prompt and don't take the flag. + instead. The destructive commands (`remove`, `preset delete`, `runner remove`) + confirm unless you pass `-y`; non-interactively, a missing `-y` is a clean exit-2 + refusal, never a hang. The read commands (`list`, `show`, `params`, …) never prompt + and don't take the flag. ## Discover entries @@ -200,7 +202,7 @@ Named value sets per entry, ideal for recurring jobs: skit run --set a=1 --set b=2 --save-preset nightly --dry-run --no-input # create without running skit preset list --json skit run -p nightly --no-input -skit preset delete nightly +skit preset delete nightly -y # confirms without -y ``` ## Prompts & runners @@ -249,7 +251,7 @@ skit params --no-interpolate # switch insertion off; --interpolate turns skit runner list --json # [{"name": …, "argv": […]}] skit runner add mycli -- mycli run {{prompt}} # each word = one argument, no shell skit runner add mycli --force -- mycli run --model opus {{prompt}} # --force replaces an existing runner (edit) -skit runner remove mycli -y # confirms without -y, like skit remove +skit runner remove mycli -y # confirms without -y, like skit remove and preset delete ``` - The agent's own per-run flags pass through after `--`: diff --git a/src/skit/store.py b/src/skit/store.py index 659fbc7..019665d 100644 --- a/src/skit/store.py +++ b/src/skit/store.py @@ -20,12 +20,7 @@ from typing import Any from . import argstate, paths, pep723 -from .atomic import ( - advisory_file_lock, - atomic_write_bytes_keep_mode, - atomic_write_toml, - try_advisory_file_lock, -) +from .atomic import advisory_file_lock, atomic_write_toml, try_advisory_file_lock from .i18n import gettext from .langs import registry from .langs.registry import stored_name @@ -41,7 +36,7 @@ ) from .params import ParamDecl, declared_from_meta from .paths import registry_path, scripts_dir -from .rewrite import detect_newline, restore_newline +from .rewrite import detect_newline, restore_newline, write_block_edit # Corruption/error types every meta.toml reader must treat the same way: valid-but-unreadable file, # invalid TOML, or valid TOML missing a required key are all "this entry is corrupt" — never a bare @@ -1385,13 +1380,11 @@ def _sync_python_block( block_deps = list(meta.dependencies or []) or [ str(d) for d in (block.get("dependencies") or []) ] - # Atomic, mode-preserving: a plain write can tear the stored copy on a crash, and a - # tmp+replace without the chmod would drop the bits copy2 preserved at add. - atomic_write_bytes_keep_mode( - script, - restore_newline( - pep723.set_dependencies(text, block_deps, requires_python=constraint), newline - ).encode("utf-8"), # pragma: no mutate — codec alias + # The shared write half (rewrite.write_block_edit): atomic + mode-preserving. The strict + # read above stays deliberately different from read_for_block_edit — a copy that doesn't + # decode bails out whole and meta stands in (the add_python encoding rule). + write_block_edit( + script, pep723.set_dependencies(text, block_deps, requires_python=constraint), newline ) diff --git a/src/skit/tui.py b/src/skit/tui.py index 330fe27..1b3c77a 100644 --- a/src/skit/tui.py +++ b/src/skit/tui.py @@ -235,6 +235,9 @@ class PendingRun: asm: flows.Assembly values: dict[str, str] extra: list[str] + # The tail's provenance (raw form text vs an already-processed replay), carried so + # the deferred save_after_run records the same bit an immediate save would have. + extra_raw: bool show_drift: bool runner: config.PromptRunner | None = None @@ -351,7 +354,11 @@ def __init__(self) -> None: self._entries: list[Entry] = [] self._visible: list[Entry] = [] self._ctrl_c_at: float = 0.0 - self._drift_cache: dict[str, tuple[float, bool]] = {} # slug -> (mtime, has_drift) + # slug -> (mtime, plan). The detail pane re-renders on every RowHighlighted, and + # a plan build is a full parse — or, for a reader kind like PowerShell, a + # synchronous subprocess with a cold-start runtime — so it must never run + # uncached inside a cursor-movement handler. + self._plan_cache: dict[str, tuple[float, flows.FormPlan]] = {} @override def get_default_screen(self) -> Screen[None]: @@ -488,19 +495,27 @@ def _refresh_footer(self) -> None: keys_local.update(tui_footer.bar(*local)) keys_global.update(tui_footer.bar(*globals_row)) + def _cached_plan(self, entry: Entry) -> flows.FormPlan: + """plan_for_entry for DISPLAY (detail pane, drift badge): lazy, mtime-cached. + Runs always build a fresh plan (action_run/action_rerun) — the cache serves the + cursor, never a launch.""" + try: + mtime = entry.script_path.stat().st_mtime + except OSError: + return flows.plan_for_entry(entry) + cached = self._plan_cache.get(entry.slug) + if cached is not None and cached[0] == mtime: + return cached[1] + plan = flows.plan_for_entry(entry) + self._plan_cache[entry.slug] = (mtime, plan) + return plan + def _has_drift(self, entry: Entry) -> bool: - """Drift is the expensive check (read + reconcile): lazy, per-selection, mtime-cached.""" + """Drift is the expensive check (read + reconcile): served off the plan cache.""" spec = spec_for(entry.meta.kind) if spec is None or spec.analyzer is None or not entry.script_path.exists(): return False - mtime = entry.script_path.stat().st_mtime - cached = self._drift_cache.get(entry.slug) - if cached is not None and cached[0] == mtime: - return cached[1] - plan = flows.plan_for_entry(entry) - drift = bool(plan.drift_lines) - self._drift_cache[entry.slug] = (mtime, drift) - return drift + return bool(self._cached_plan(entry).drift_lines) def _refresh_detail(self) -> None: # A queued RowHighlighted can reach this after the detail pane is gone — a @@ -558,7 +573,7 @@ def _detail_lines(self, entry: Entry) -> list[str]: def _detail_state_lines(self, entry: Entry) -> list[str]: lines: list[str] = [] state = argstate.load_state(entry.slug) - plan = flows.plan_for_entry(entry) + plan = self._cached_plan(entry) if plan.fields: shown: list[str] = [] for f in plan.fields[:6]: @@ -799,7 +814,11 @@ def action_rerun(self) -> None: # fall back to the form rather than assembling a broken command. self.action_run() return - self._execute(entry, plan, prefill, argstate.load_state(entry.slug)["extra_args"]) + # The replayed tail keeps its recorded provenance: a tail the CLI captured + # post-shell must not get a second token/glob pass just because the rerun + # happens from the TUI. + state = argstate.load_state(entry.slug) + self._execute(entry, plan, prefill, state["extra_args"], extra_raw=state["extra_args_raw"]) def _execute( self, @@ -808,14 +827,18 @@ def _execute( values: dict[str, str], extra: list[str], *, + extra_raw: bool = True, show_drift: bool = True, runner: config.PromptRunner | None = None, ) -> None: """Suspend, deliver (inject/flags/template), pass the terminal through, record. - show_drift=False when the form was just shown (its banner already said it).""" + show_drift=False when the form was just shown (its banner already said it). + extra_raw=True is the form path (the extra field is raw intent text — tokens and + globs expand); a rerun replaying a CLI-captured tail passes False so the tail + stays literal, matching how it originally launched.""" try: - asm = flows.assemble(plan, values, list(extra), cwd=Path.cwd()) + asm = flows.assemble(plan, values, list(extra), cwd=Path.cwd(), expand_extra=extra_raw) except flows.FormError as exc: self._refresh_status(gettext("Error: %(error)s") % {"error": escape(str(exc))}) return @@ -823,7 +846,11 @@ def _execute( # A launcher hands the terminal back: quit the TUI FIRST, run after # (_finish_run). Running under suspend() and exiting would repaint the # whole app for one frame on resume — a visible flash. - self.exit(PendingRun(entry, plan, asm, dict(values), list(extra), show_drift, runner)) + self.exit( + PendingRun( + entry, plan, asm, dict(values), list(extra), extra_raw, show_drift, runner + ) + ) return with self.suspend(): print(f"\n── {gettext('Run %(name)s') % {'name': entry.meta.name}} ──\n", flush=True) @@ -852,7 +879,9 @@ def _execute( gettext("Last: %(name)s ✗ couldn't launch") % {"name": escape(entry.meta.name)} ) return - flows.save_after_run(entry.slug, plan, values, list(extra), code, at=models.now_iso()) + flows.save_after_run( + entry.slug, plan, values, list(extra), code, at=models.now_iso(), extra_raw=extra_raw + ) self._reload() status = ( gettext("Last: %(name)s ✓ finished") @@ -892,7 +921,7 @@ def action_edit(self) -> None: editor.open_entry_in_editor(target, kind=entry.meta.kind) except (editor.EditorError, editor.EditedSourceError) as exc: edit_error = str(exc) - self._drift_cache.pop(entry.slug, None) + self._plan_cache.pop(entry.slug, None) self._reload() if edit_error is not None: self._refresh_status(gettext("Error: %(error)s") % {"error": escape(edit_error)}) @@ -983,7 +1012,7 @@ def action_settings(self, section: str = "") -> None: # pragma: no mutate from .tui_settings import ScriptSettingsScreen def _closed(_changed: bool | None) -> None: - self._drift_cache.pop(entry.slug, None) + self._plan_cache.pop(entry.slug, None) self._reload() self.push_screen(ScriptSettingsScreen(entry, initial_section=section), _closed) @@ -1093,6 +1122,7 @@ def _finish_run(pending: PendingRun) -> int: list(pending.extra), outcome.code, at=models.now_iso(), + extra_raw=pending.extra_raw, ) return outcome.code diff --git a/src/skit/tui_add.py b/src/skit/tui_add.py index 0e827d1..14c6259 100644 --- a/src/skit/tui_add.py +++ b/src/skit/tui_add.py @@ -33,6 +33,7 @@ config, editor, pep723, + rewrite, store, theme, tui_footer, @@ -740,6 +741,7 @@ def __init__( self._spec = spec_for(kind) self._text_error: str = "" + self._reader_modeled_memo: tuple[str, bool] | None = None try: self._text = path.read_text(encoding="utf-8", errors="replace") # pragma: no mutate — encoding None/utf-8/UTF-8 decode identically under skit's UTF-8-mode runtime (equivalent); the errors="replace" handler stays behaviourally pinned by test_add_review_screen_reads_invalid_utf8_with_replace # fmt: skip except OSError as exc: @@ -790,10 +792,18 @@ def _python_pin(self) -> str: def _reader_modeled(self) -> bool: """Whether the entry's own reader models a form from the current text — the shared trap predicate (flows.reader_fields) the tick list and its Space chip - both key on.""" + both key on. Memoized per text: for a reader kind (PowerShell) each read is a + synchronous subprocess, and this fires on every mode toggle — the panel must + not freeze for seconds because a radio button was clicked. Keyed on the text + itself so the Ctrl+E edit→rescan path recomputes naturally.""" + memo = self._reader_modeled_memo + if memo is not None and memo[0] == self._text: + return memo[1] from . import flows - return flows.reader_fields(self._spec, self._text) > 0 + modeled = flows.reader_fields(self._spec, self._text) > 0 + self._reader_modeled_memo = (self._text, modeled) + return modeled def _suggest_description(self) -> str: if self._kind == "python": @@ -1185,10 +1195,14 @@ def action_accept(self) -> None: if picked: specs = [ParamDecl.from_candidate(c) for c in picked] copy_path = entry.script_path - # This is a write-back path, so preserve arbitrary shell/fish bytes while - # inserting the comment-only metadata block. - current = copy_path.read_text(encoding="utf-8", errors="surrogateescape") # pragma: no mutate — utf-8 equivalence # fmt: skip - copy_path.write_text(self._spec.params_io.write(current, specs), encoding="utf-8", errors="surrogateescape") # pragma: no mutate — utf-8 equivalence # fmt: skip + # Byte-lossless write-back through the one shared pair (rewrite.py): + # read_text/write_text here silently rewrote every line ending of the + # just-stored copy — the exact corruption the CLI twin of this add + # (_onboard_script_params) documents and avoids. + current, newline = rewrite.read_for_block_edit(copy_path) + rewrite.write_block_edit( + copy_path, self._spec.params_io.write(current, specs), newline + ) self.dismiss(entry.slug) def action_cancel(self) -> None: @@ -1209,8 +1223,12 @@ class PromptReviewScreen(Screen[str | None]): Binding("ctrl+e", "edit_source", gettext("Edit prompt")), Binding("ctrl+s", "accept", gettext("Add"), priority=True), Binding("ctrl+n", "new_runner", gettext("New agent"), show=False, priority=True), + # Ctrl+L, not Ctrl+O: Ctrl+O is the grammar chord for "restore the default" + # (README documents it on the run form) and must not mean anything else on a + # sibling screen. Ctrl+L is no Input editing chord, so it fires from inside a + # field without priority; the chip stays the mouse path. Binding( - "ctrl+o", + "ctrl+l", "choose_prompt_candidates", gettext("Choose variables…"), show=False, @@ -1426,7 +1444,7 @@ def _compose_placeholders(self) -> ComposeResult: yield Static( tui_footer.chip( "screen.choose_prompt_candidates", - "Ctrl+O", + "Ctrl+L", gettext("Choose variables…"), ), id="pv-choose-candidates", diff --git a/src/skit/tui_prefs.py b/src/skit/tui_prefs.py index e610de8..881148e 100644 --- a/src/skit/tui_prefs.py +++ b/src/skit/tui_prefs.py @@ -119,12 +119,13 @@ def __init__(self) -> None: BINDINGS = [ Binding("escape", "close", gettext("Back")), Binding("ctrl+s", "save", gettext("Save"), priority=True), - # Ctrl+O/Ctrl+K, not Ctrl+N/Ctrl+T (those mean "New agent" / "insert value" - # elsewhere) — and NON-priority: Ctrl+K is every Input's delete-to-end-of-line, - # and a screen full of text fields must never answer an editing chord with a - # modal. The chips stay the mouse path; the chords fire from any non-Input focus. - Binding("ctrl+o", "manage_runners", gettext("Manage agents"), show=False), - Binding("ctrl+k", "install_skill", gettext("Teach an AI agent"), show=False), + # Ctrl+G/Ctrl+Y: chords with NO meaning anywhere else in skit — not the + # grammar chords (Ctrl+E/N/T/R/S, Ctrl+O = restore default, Ctrl+L = choose + # variables) and not an Input's own editing chords (Ctrl+A/E/K/U/W…), so they + # fire from inside a field without priority and never collide with muscle + # memory built on a sibling screen. The chips stay the mouse path. + Binding("ctrl+g", "manage_runners", gettext("Manage agents"), show=False), + Binding("ctrl+y", "install_skill", gettext("Teach an AI agent"), show=False), *tui_footer.FIELD_NAV_BINDINGS, ] # Boot on the language dropdown, not the "*" pick (the body scroll container). @@ -287,9 +288,9 @@ def compose(self) -> ComposeResult: yield Static("", id="pf-runner-count") yield Static( tui_footer.bar( - tui_footer.chip("screen.manage_runners", "Ctrl+O", gettext("Manage agents…")), + tui_footer.chip("screen.manage_runners", "Ctrl+G", gettext("Manage agents…")), tui_footer.chip( - "screen.install_skill", "Ctrl+K", gettext("Teach an AI agent skit…") + "screen.install_skill", "Ctrl+Y", gettext("Teach an AI agent skit…") ), ), id="pf-runner-manage", diff --git a/src/skit/tui_settings.py b/src/skit/tui_settings.py index d684bc3..24392fd 100644 --- a/src/skit/tui_settings.py +++ b/src/skit/tui_settings.py @@ -30,13 +30,12 @@ tui_prompt, tui_runner, ) -from .atomic import atomic_write_bytes_keep_mode from .i18n import gettext from .langs.prompt import text as prompt_text from .langs.registry import spec_for from .models import Entry from .params import ParamDecl -from .rewrite import detect_newline, restore_newline +from .rewrite import read_for_block_edit, write_block_edit class DiscardChangesModal(ModalScreen[bool]): @@ -268,8 +267,10 @@ class ScriptSettingsScreen(Screen[bool]): Binding("ctrl+s", "save", gettext("Save"), priority=True), Binding("ctrl+r", "resync", gettext("Resync"), priority=True), Binding("ctrl+n", "new_runner", gettext("New agent"), show=False, priority=True), + # Ctrl+L, same meaning as on the prompt review panel (one chord, one meaning); + # Ctrl+O stays reserved for the run form's "restore the default". Binding( - "ctrl+o", + "ctrl+l", "choose_prompt_candidates", gettext("Choose variables…"), show=False, @@ -305,20 +306,14 @@ def __init__(self, entry: Entry, initial_section: str = "") -> None: self._spec = spec_for(entry.meta.kind) self._is_prompt: bool = entry.meta.kind == "prompt" params_io = self._spec.params_io if self._spec is not None else None - # The newline style the copy actually uses, captured for the save below. Reading with - # universal newlines and errors="replace" (what this did) is fine for DISPLAY but - # cannot be written back: the save would flatten a CRLF script to LF on every - # checkbox tick and burn each non-UTF-8 byte down to U+FFFD. Same discipline as - # cli._edit_params — fold to LF for the LF-based block engine, restore on write. + # The newline style the copy actually uses, captured for the save below through the + # shared byte-lossless pair (rewrite.py): reading with universal newlines and + # errors="replace" is fine for DISPLAY but cannot be written back — the save would + # flatten a CRLF script to LF on every checkbox tick and burn each non-UTF-8 byte + # down to U+FFFD. self._newline: str = "\n" if params_io is not None and entry.script_path.exists(): - raw = entry.script_path.read_bytes() - self._newline = detect_newline(raw) - self._text = ( - raw.decode("utf-8", errors="surrogateescape") # pragma: no mutate — codec alias - .replace("\r\n", "\n") - .replace("\r", "\n") - ) + self._text, self._newline = read_for_block_edit(entry.script_path) # The ENTRY'S OWN params_io — never Python's. shell/fish carry their block in the same # '#' engine, but JS/TS carry it behind '//', so the Python reader would return [] for a # perfectly valid managed JS entry (TUI↔CLI parity: cli._edit_params already routes this way). @@ -708,7 +703,7 @@ def _compose_declared_fields(self, show_flag: bool) -> ComposeResult: yield Static( tui_footer.chip( "screen.choose_prompt_candidates", - "Ctrl+O", + "Ctrl+L", gettext("Choose variables…"), ), id="st-choose-candidates", @@ -1064,15 +1059,11 @@ def action_save(self) -> None: # noqa: PLR0911, PLR0912, PLR0915 — one valida box = self.query(f"#st-new-{i}") if box and box.first(Checkbox).value: new_specs.append(ParamDecl.from_candidate(c)) - # Atomic + mode-preserving: a torn write here could truncate the stored copy - # mid-save; tmp+replace leaves either the old script or the new one. Byte-lossless - # too — the copy's own line endings go back on, and surrogateescape carries any - # non-UTF-8 byte through untouched, so a comment-block edit stays a comment-block - # edit rather than rewriting the whole file. - written = restore_newline(spec.params_io.write(self._text, new_specs), self._newline) - atomic_write_bytes_keep_mode( - entry.script_path, - written.encode("utf-8", errors="surrogateescape"), # pragma: no mutate — alias + # The shared write half (rewrite.py): atomic + mode-preserving, the copy's own + # line endings back on, surrogateescape bytes untouched — a comment-block edit + # stays a comment-block edit rather than rewriting the whole file. + write_block_edit( + entry.script_path, spec.params_io.write(self._text, new_specs), self._newline ) purged = argstate.purge_secret(entry.slug, {s.name for s in new_specs if s.secret}) if purged: From 5a3652042513bec723a54f5e77629ff9a3d450f0 Mon Sep 17 00:00:00 2001 From: Yi-Ting Chiu Date: Sun, 26 Jul 2026 01:34:57 -0700 Subject: [PATCH 02/40] =?UTF-8?q?fix:=20design-audit=20round=202=20?= =?UTF-8?q?=E2=80=94=20jammed=20secret=20spellings,=20meta-aware=20plan=20?= =?UTF-8?q?cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions the round-2 review caught in round 1's own fixes: - Word-boundary secret matching dropped the jammed spellings the old substring rule caught — APIKEY/apikey/AUTHTOKEN stopped matching, a C3 regression in the dangerous direction (an unmarked literal is published into current_defaults, --json output, and plaintext state). Words ending in the long secret words (TOKEN/SECRET/PASSWORD/PASSWD) now match, and KEY-compounds match behind a credential-qualifier prefix list (APIKEY, SSHKEY — never MONKEY/TURKEY/HOTKEY). - The new display-plan cache keyed only on the script's mtime, but a plan is a function of meta.toml too (declared [[parameters]] rows, a prompt's managed list / interpolate switch): an agent running skit params --add beside an open TUI left the detail pane stale forever. The cache now keys on both mtimes. --- src/skit/params.py | 21 ++++++++++++++++++++- src/skit/tui.py | 31 ++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/skit/params.py b/src/skit/params.py index 3c9c699..68b25b0 100644 --- a/src/skit/params.py +++ b/src/skit/params.py @@ -55,15 +55,34 @@ # maxTokens do not. The substring match this replaces turned `--max-tokens` into a # password field — masked, never prefilled, never remembered — on the reader lane, # where no override exists to turn it off. +# +# Jammed spellings still count (the substring rule caught them, and a false NEGATIVE +# here publishes a literal into current_defaults/state — the dangerous direction): +# any word ENDING in the long secret words (AUTHTOKEN, MYSECRET, DBPASSWORD), and +# KEY-compounds whose prefix is a known credential qualifier (APIKEY, SSHKEY) — KEY +# alone is too short for a bare suffix rule (MONKEY, TURKEY, HOTKEY, WHISKEY). _SECRET_WORDS = frozenset({"KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD"}) +_SECRET_SUFFIXES = ("TOKEN", "SECRET", "PASSWORD", "PASSWD") +_KEY_PREFIXES = frozenset( + {"API", "AUTH", "ACCESS", "SECRET", "PRIVATE", "PASS", "SSH", "GPG", "AWS", "MASTER", + "SIGNING", "LICENSE", "ENCRYPTION"} +) # fmt: skip # lower/digit→Upper and WORDBreak boundaries; separators (_, -, spaces, punctuation) # are any non-letter run. _CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") +def _secret_word(w: str) -> bool: + if w in _SECRET_WORDS: + return True + if any(w.endswith(s) for s in _SECRET_SUFFIXES): + return True + return w.endswith("KEY") and w[:-3] in _KEY_PREFIXES + + def is_secret_name(text: str) -> bool: words = re.split(r"[^A-Za-z]+", _CAMEL_BOUNDARY.sub(" ", text)) - return any(w.upper() in _SECRET_WORDS for w in words if w) + return any(_secret_word(w.upper()) for w in words if w) _BINDINGS: tuple[Binding, ...] = ("const", "input", "envdefault", "none") diff --git a/src/skit/tui.py b/src/skit/tui.py index 1b3c77a..7a1ed35 100644 --- a/src/skit/tui.py +++ b/src/skit/tui.py @@ -354,11 +354,15 @@ def __init__(self) -> None: self._entries: list[Entry] = [] self._visible: list[Entry] = [] self._ctrl_c_at: float = 0.0 - # slug -> (mtime, plan). The detail pane re-renders on every RowHighlighted, and - # a plan build is a full parse — or, for a reader kind like PowerShell, a - # synchronous subprocess with a cold-start runtime — so it must never run - # uncached inside a cursor-movement handler. - self._plan_cache: dict[str, tuple[float, flows.FormPlan]] = {} + # slug -> ((script mtime, meta.toml mtime), plan). The detail pane re-renders on + # every RowHighlighted, and a plan build is a full parse — or, for a reader kind + # like PowerShell, a synchronous subprocess with a cold-start runtime — so it + # must never run uncached inside a cursor-movement handler. BOTH mtimes key the + # cache: a plan is a function of the script body AND of meta.toml (declared + # [[parameters]] rows, a prompt's managed list/interpolate switch), and an agent + # editing the library from the CLI while the TUI sits open is the product's own + # coexistence story — the pane must pick that up, not only the TUI's own writes. + self._plan_cache: dict[str, tuple[tuple[float, float], flows.FormPlan]] = {} @override def get_default_screen(self) -> Screen[None]: @@ -496,18 +500,23 @@ def _refresh_footer(self) -> None: keys_global.update(tui_footer.bar(*globals_row)) def _cached_plan(self, entry: Entry) -> flows.FormPlan: - """plan_for_entry for DISPLAY (detail pane, drift badge): lazy, mtime-cached. - Runs always build a fresh plan (action_run/action_rerun) — the cache serves the - cursor, never a launch.""" + """plan_for_entry for DISPLAY (detail pane, drift badge): lazy, mtime-cached on + the script AND its meta.toml (see _plan_cache). Runs always build a fresh plan + (action_run/action_rerun) — the cache serves the cursor, never a launch. An + entry whose script is gone/never-a-file (a command template, a removed target) + just builds fresh: those plans are cheap meta reads.""" try: - mtime = entry.script_path.stat().st_mtime + key = ( + entry.script_path.stat().st_mtime, + (entry.dir / "meta.toml").stat().st_mtime, + ) except OSError: return flows.plan_for_entry(entry) cached = self._plan_cache.get(entry.slug) - if cached is not None and cached[0] == mtime: + if cached is not None and cached[0] == key: return cached[1] plan = flows.plan_for_entry(entry) - self._plan_cache[entry.slug] = (mtime, plan) + self._plan_cache[entry.slug] = (key, plan) return plan def _has_drift(self, entry: Entry) -> bool: From 736bb6b8b2ab2065bb975c6d73f2751861b7649c Mon Sep 17 00:00:00 2001 From: Yi-Ting Chiu Date: Sun, 26 Jul 2026 02:42:42 -0700 Subject: [PATCH 03/40] test: cover the design-audit fixes (rounds 1 and 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repairs the suite for the two source commits and adds the behavior coverage that keeps each verified bug dead. Contract repairs — the API changes the fixes made: - flows.save_after_run's now-required extra_raw keyword threaded through its seven test call sites, each carrying the value its scenario really simulates (a TUI-form save is True, a CLI tail False). - tui.PendingRun's new positional extra_raw field at its five construction sites. - MenuApp._drift_cache -> _plan_cache: the seeded/inspected sentinels are now ((script mtime, meta.toml mtime), FormPlan) and still prove what they meant to — the cache hit, and the pop on edit / settings close. - test_store_mut's atomic-write spy repointed at rewrite.atomic_write_bytes_keep_mode, the seam _sync_python_block reaches through write_block_edit now; the two cli_design_cov write-back spies likewise, which also lets them pin the landing as atomic (neither Path.write_bytes nor write_text may touch a stored copy). - Ctrl+O -> Ctrl+L (prompt review, Script settings) and Ctrl+O/Ctrl+K -> Ctrl+G/Ctrl+Y (Preferences) in every pilot test that pressed them. - remove / preset delete need -y or a real terminal now, so their existing tests say so. New coverage (tests/test_design_audit_fixes.py, tests/test_design_audit_tui.py): - rewrite.read_for_block_edit / write_block_edit: CRLF, lone-CR and LF copies each round-trip with only the block changed and every other byte identical; non-UTF-8 bytes survive via surrogateescape; the executable bit survives the atomic write; cli._onboard_python degrades on a non-UTF-8 python file instead of raising. - The round-1 HIGH pinned at the surface it shipped from: an AddReviewScreen accept on a CRLF shell script leaves the stored copy CRLF and byte-exact outside the block. - is_secret_name's matrix in both directions, including one jammed spelling for every suffix and every KEY-prefix the rule recognizes (--max-tokens stays public). - The non-interactive contract for remove and preset delete (worded exit-2 refusal naming --yes, nothing removed), their -y and confirm paths, the unknown-preset error landing before any ask, and the same error when a preset vanishes mid-flight. - Extra-args provenance end to end: the argstate marker is written, cleared, and defaulted for legacy docs; save_after_run threads it; the CLI expands a replayed raw tail and replays an unmarked one literally; a fresh `-- args` clears the marker; --forget-args clears both; the TUI's `r` follows the record while the form's own tail saves marked; and the marker rides PendingRun into the deferred exit-mode save. - The display-plan cache: one build per (script mtime, meta.toml mtime), a rebuild when either moves, no caching at all when the script can't be stat'ed, _has_drift served off the same entry and short-circuiting before any build, and both pop sites. - AddReviewScreen._reader_modeled probes once per text and recomputes after an edit (a single-option getopts pins "modeled" at one field, not more than one). - Positive pilot tests for Ctrl+L / Ctrl+G / Ctrl+Y with a negative for each vacated chord, and Ctrl+O still restoring a run-form default. - params --manage on an exe names the --add door it does have — and the two degraded- spec refusals now assert they do NOT carry that hint; params --json rows carry "binding" beside the frozen "kind" without dropping a key an existing consumer reads. --- tests/test_argstate_mut.py | 9 +- tests/test_cli.py | 4 +- tests/test_cli_design_cov.py | 59 +- tests/test_cli_gaps_cov.py | 6 +- tests/test_cli_mut_part03.py | 17 +- tests/test_default_semantics_review_fixes.py | 10 +- tests/test_design_audit_fixes.py | 670 +++++++++++++++++++ tests/test_design_audit_tui.py | 659 ++++++++++++++++++ tests/test_flows.py | 19 +- tests/test_prompt_tui.py | 24 +- tests/test_source_default_semantics.py | 1 + tests/test_store_mut.py | 18 +- tests/test_tui_edit.py | 21 +- tests/test_tui_mut.py | 6 +- tests/test_tui_mut_part01.py | 6 +- tests/test_tui_mut_part05.py | 23 +- tests/test_tui_mut_part09.py | 41 +- tests/test_tui_prefs_agents_cov.py | 32 +- tests/test_tui_prefs_mut.py | 2 +- 19 files changed, 1519 insertions(+), 108 deletions(-) create mode 100644 tests/test_design_audit_fixes.py create mode 100644 tests/test_design_audit_tui.py diff --git a/tests/test_argstate_mut.py b/tests/test_argstate_mut.py index c5504fa..3d31de8 100644 --- a/tests/test_argstate_mut.py +++ b/tests/test_argstate_mut.py @@ -207,6 +207,7 @@ def test_a_missing_state_file_is_empty_not_an_error() -> None: assert argstate.load_state("absent") == { "values": {}, "extra_args": [], + "extra_args_raw": False, "presets": {}, "last_run": {}, } @@ -227,7 +228,13 @@ def _write_values_file(slug: str, body: str) -> None: (values_dir() / f"{slug}.toml").write_text(body, encoding="utf-8") -_EMPTY_STATE = {"values": {}, "extra_args": [], "presets": {}, "last_run": {}} +_EMPTY_STATE = { + "values": {}, + "extra_args": [], + "extra_args_raw": False, + "presets": {}, + "last_run": {}, +} @pytest.mark.parametrize( diff --git a/tests/test_cli.py b/tests/test_cli.py index 0eacd7f..d340852 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -771,7 +771,9 @@ def test_preset_list_not_found(): def test_preset_delete(tmp_path): ent = store.add_python(_py(tmp_path, "print(1)\n"), name="a") argstate.save_preset(ent.slug, "prod", {"CITY": "Taipei"}) - result = runner.invoke(cli.app, ["preset", "delete", "a", "prod"]) + # A preset is unrecoverable user data: deleting it non-interactively takes -y, the same + # ceremony `remove` and `runner remove` ask for. + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod", "--yes"]) assert result.exit_code == 0 assert argstate.load_state(ent.slug)["presets"] == {} diff --git a/tests/test_cli_design_cov.py b/tests/test_cli_design_cov.py index b8232e7..984c2d4 100644 --- a/tests/test_cli_design_cov.py +++ b/tests/test_cli_design_cov.py @@ -19,7 +19,7 @@ import pytest from typer.testing import CliRunner -from skit import argstate, cli, config, i18n, store +from skit import argstate, cli, config, i18n, rewrite, store from skit.langs.registry import spec_for runner = CliRunner() @@ -1020,22 +1020,27 @@ def test_onboard_script_params_reference_forwards_frameworks_to_notice(tmp_path, def test_onboard_script_params_write_back_is_byte_based_not_text_mode(tmp_path, monkeypatch): - """Onboarding's write-back must be BYTE-based (read_bytes + write_bytes), like _edit_params — - never text-mode write_text, which re-expands \\n to the host os.linesep. The only read_text on - the copy is the analyzer read (errors="replace"); the write-back re-reads raw bytes and writes - raw bytes, so a candidate tick neither bakes U+FFFD over raw bytes nor rewrites line endings.""" + """Onboarding's write-back must be BYTE-based, like _edit_params — never text-mode + write_text, which re-expands \\n to the host os.linesep. The only read_text on the copy is + the analyzer read (errors="replace"); the write-back re-reads raw bytes and lands raw bytes + through the shared rewrite pair, so a candidate tick neither bakes U+FFFD over raw bytes nor + rewrites line endings. The landing is atomic + mode-preserving now (write_block_edit → + atomic_write_bytes_keep_mode), so neither Path.write_bytes nor Path.write_text may touch the + copy: a torn plain write could truncate the file a user just handed skit.""" _two_const_shell(tmp_path) entry = store.resolve("d") # resolve before spying, so only the copy read/write are captured _fake_tty(monkeypatch) monkeypatch.setattr(cli.Prompt, "ask", staticmethod(lambda *a, **k: "1")) # pick one -> writes text_reads: list[dict[str, object]] = [] byte_reads: list[Path] = [] - byte_writes: list[bytes] = [] + atomic_writes: list[bytes] = [] + plain_byte_writes: list[Path] = [] # a plain (non-atomic) byte write is a regression too text_writes: list[Path] = [] # the regression: write_text must never touch the stored copy real_read_text = Path.read_text real_read_bytes = Path.read_bytes real_write_bytes = Path.write_bytes real_write_text = Path.write_text + real_atomic = rewrite.atomic_write_bytes_keep_mode def read_text_spy(self, encoding=None, errors=None, *a, **k): if self == entry.script_path: @@ -1047,9 +1052,14 @@ def read_bytes_spy(self): byte_reads.append(self) return real_read_bytes(self) + def atomic_spy(path, data): + if path == entry.script_path: + atomic_writes.append(data) + return real_atomic(path, data) + def write_bytes_spy(self, data): if self == entry.script_path: - byte_writes.append(data) + plain_byte_writes.append(self) return real_write_bytes(self, data) def write_text_spy(self, *a, **k): @@ -1059,13 +1069,15 @@ def write_text_spy(self, *a, **k): monkeypatch.setattr(Path, "read_text", read_text_spy) monkeypatch.setattr(Path, "read_bytes", read_bytes_spy) + monkeypatch.setattr(rewrite, "atomic_write_bytes_keep_mode", atomic_spy) monkeypatch.setattr(Path, "write_bytes", write_bytes_spy) monkeypatch.setattr(Path, "write_text", write_text_spy) assert cli._onboard_script_params(entry, _spec("shell"), no_input=False) == ["WIDTH"] assert [r["errors"] for r in text_reads] == ["replace"] # analyzer read only; no text re-read assert byte_reads # the write-back re-read raw bytes - assert len(byte_writes) == 1 # a single byte write-back - assert b"[tool.skit]" in byte_writes[0] # ...carrying the inserted block + assert len(atomic_writes) == 1 # a single atomic, mode-preserving byte write-back + assert b"[tool.skit]" in atomic_writes[0] # ...carrying the inserted block + assert plain_byte_writes == [] # never a plain write_bytes (non-atomic: can tear) assert text_writes == [] # write_text never touched the stored copy (no os.linesep expansion) @@ -1108,20 +1120,24 @@ def test_onboard_script_params_preserves_non_utf8_source_bytes(tmp_path, monkeyp def test_edit_params_write_back_is_byte_based_not_text_mode(tmp_path, monkeypatch): - """`params --manage`'s write-back must be BYTE-based (read_bytes + write_bytes), like - _normalize_params — never text-mode write_text, which re-expands \\n to os.linesep and would - CRLF-ify the whole stored copy on Windows. The only read_text on the copy is the analysis - read (errors="replace"); the write-back re-reads raw bytes and writes raw bytes, so a manage - edit can neither bake U+FFFD over raw bytes nor rewrite line endings to the host default.""" + """`params --manage`'s write-back must be BYTE-based, like _normalize_params — never + text-mode write_text, which re-expands \\n to os.linesep and would CRLF-ify the whole stored + copy on Windows. The only read_text on the copy is the analysis read (errors="replace"); the + write-back re-reads raw bytes and lands raw bytes through the shared rewrite pair, so a + manage edit can neither bake U+FFFD over raw bytes nor rewrite line endings to the host + default — and the landing is atomic + mode-preserving, so neither Path.write_bytes nor + Path.write_text may touch the copy.""" entry = _two_const_shell(tmp_path) text_reads: list[dict[str, object]] = [] byte_reads: list[Path] = [] - byte_writes: list[bytes] = [] + atomic_writes: list[bytes] = [] + plain_byte_writes: list[Path] = [] # a plain (non-atomic) byte write is a regression too text_writes: list[Path] = [] # the regression: write_text must never touch the stored copy real_read_text = Path.read_text real_read_bytes = Path.read_bytes real_write_bytes = Path.write_bytes real_write_text = Path.write_text + real_atomic = rewrite.atomic_write_bytes_keep_mode def read_text_spy(self, encoding=None, errors=None, *a, **k): if self == entry.script_path: @@ -1133,9 +1149,14 @@ def read_bytes_spy(self): byte_reads.append(self) return real_read_bytes(self) + def atomic_spy(path, data): + if path == entry.script_path: + atomic_writes.append(data) + return real_atomic(path, data) + def write_bytes_spy(self, data): if self == entry.script_path: - byte_writes.append(data) + plain_byte_writes.append(self) return real_write_bytes(self, data) def write_text_spy(self, *a, **k): @@ -1145,14 +1166,16 @@ def write_text_spy(self, *a, **k): monkeypatch.setattr(Path, "read_text", read_text_spy) monkeypatch.setattr(Path, "read_bytes", read_bytes_spy) + monkeypatch.setattr(rewrite, "atomic_write_bytes_keep_mode", atomic_spy) monkeypatch.setattr(Path, "write_bytes", write_bytes_spy) monkeypatch.setattr(Path, "write_text", write_text_spy) result = runner.invoke(cli.app, ["params", "d", "--manage", "WIDTH"]) assert result.exit_code == 0, result.output assert [r["errors"] for r in text_reads] == ["replace"] # analysis read only; no text re-read assert byte_reads # the write-back re-read raw bytes - assert len(byte_writes) == 1 # a single byte write-back - assert b"[tool.skit]" in byte_writes[0] # ...carrying the rewritten block + assert len(atomic_writes) == 1 # a single atomic, mode-preserving byte write-back + assert b"[tool.skit]" in atomic_writes[0] # ...carrying the rewritten block + assert plain_byte_writes == [] # never a plain write_bytes (non-atomic: can tear) assert text_writes == [] # write_text never touched the stored copy (no os.linesep expansion) diff --git a/tests/test_cli_gaps_cov.py b/tests/test_cli_gaps_cov.py index c9c602d..6cc494e 100644 --- a/tests/test_cli_gaps_cov.py +++ b/tests/test_cli_gaps_cov.py @@ -214,7 +214,11 @@ def test_add_stdin_store_error_surfaces_as_exit_1(): # -------------------------------------------------------------------------- -def test_remove_command_entry_confirmed(tmp_path): +def test_remove_command_entry_confirmed(tmp_path, monkeypatch): + # The confirm branch is now reachable only on a real terminal (in a pipe/CI a missing + # -y is a worded exit-2 refusal, never a prompt that eats piped stdin), so the tty + # answer has to be faked the way `runner remove`'s twin test does it. + monkeypatch.setattr(cli, "_is_interactive", lambda: True) store.add_command("echo hi", name="c") result = runner.invoke(cli.app, ["remove", "c"], input="y\n") assert result.exit_code == 0, result.output diff --git a/tests/test_cli_mut_part03.py b/tests/test_cli_mut_part03.py index e60c256..742524e 100644 --- a/tests/test_cli_mut_part03.py +++ b/tests/test_cli_mut_part03.py @@ -170,9 +170,12 @@ def test_edit_params_degraded_shell_spec_refuses_instead_of_crashing(tmp_path, m malformed=[], ) assert ei.value.exit_code == 1 - assert "has no managed parameters — its kind has no analyzer to read them from." in _norm( - capsys.readouterr().err - ) + err = _norm(capsys.readouterr().err) + assert "has no managed parameters — its kind has no analyzer to read them from." in err + # ...and NO --add hint: this kind's params_io IS its parameter home, so "declare one + # instead" would sell a lane it already has. The hint belongs to the kinds that carry no + # in-file block at all (exe/command/prompt). + assert "Declare one instead" not in err def test_edit_params_none_spec_refuses_before_attribute_access(tmp_path, monkeypatch, capsys): @@ -195,9 +198,11 @@ def test_edit_params_none_spec_refuses_before_attribute_access(tmp_path, monkeyp malformed=[], ) assert ei.value.exit_code == 1 - assert "has no managed parameters — its kind has no analyzer to read them from." in _norm( - capsys.readouterr().err - ) + err = _norm(capsys.readouterr().err) + assert "has no managed parameters — its kind has no analyzer to read them from." in err + # A kind skit doesn't know has no declared lane to point at either: the hint's guard + # short-circuits on the same None it just refused for. + assert "Declare one instead" not in err # -------------------------------------------------------------------------- diff --git a/tests/test_default_semantics_review_fixes.py b/tests/test_default_semantics_review_fixes.py index 90e1348..1efbc2c 100644 --- a/tests/test_default_semantics_review_fixes.py +++ b/tests/test_default_semantics_review_fixes.py @@ -342,7 +342,11 @@ def test_preset_from_last_saves_effective_values_after_an_all_defaults_run(tmp_p plan = flows.plan_for_entry(entry) values = flows.prefill(plan, entry.slug) assert values == {"GREETING": "bonjour"} - flows.save_after_run(entry.slug, plan, values, [], 0, at="2026-07-09T14:30:05+00:00") + # extra_raw=False: this stands in for a `skit run greet` — a CLI tail the user's shell + # already processed (there is none here at all). + flows.save_after_run( + entry.slug, plan, values, [], 0, at="2026-07-09T14:30:05+00:00", extra_raw=False + ) state = argstate.load_state(entry.slug) assert state["values"] == {} # nothing differed from the default assert state["last_run"]["exit"] == 0 # ... but the run really happened @@ -370,7 +374,9 @@ def test_preset_from_last_pins_the_default_that_actually_ran(tmp_path: Path): entry = _managed(tmp_path, body, specs, name="history") plan = flows.plan_for_entry(entry) values = flows.prefill(plan, entry.slug) - flows.save_after_run(entry.slug, plan, values, [], 0, at="2026-07-09T14:30:05+00:00") + flows.save_after_run( + entry.slug, plan, values, [], 0, at="2026-07-09T14:30:05+00:00", extra_raw=False + ) current = entry.script_path.read_text(encoding="utf-8") entry.script_path.write_text(current.replace('GREETING = "A"', 'GREETING = "B"')) diff --git a/tests/test_design_audit_fixes.py b/tests/test_design_audit_fixes.py new file mode 100644 index 0000000..ac3106b --- /dev/null +++ b/tests/test_design_audit_fixes.py @@ -0,0 +1,670 @@ +"""Behavior coverage for the design-audit fixes (rounds 1 and 2), headless + CLI half. + +Each section keeps one verified bug dead: + +A. ``rewrite.read_for_block_edit`` / ``write_block_edit`` — the ONE byte-lossless + comment-block write-back pair the six write sites now share (surrogateescape, LF fold, + newline restore, atomic + mode-preserving). The TUI half of A (the AddReviewScreen + corruption that actually shipped) lives in tests/test_design_audit_tui.py. +B. ``params.is_secret_name`` — whole-word matching with the jammed spellings kept. The + substring rule made ``--max-tokens`` a permanent password field on the reader lane; + round 2's repair must not swing the other way and let ``APIKEY`` through unmarked. +C. ``skit remove`` / ``skit preset delete`` — the non-interactive contract (worded exit-2 + refusal naming --yes, never a confirm that eats piped stdin) plus preset deletion's new + confirmation ceremony. +D. Extra-args provenance — a remembered tail records HOW it was captured, and every replay + in either face follows that record instead of the face doing the replaying. +H. ``params --manage`` on a kind with no analyzer names the ``--add`` door it does have. +I. ``params --json`` rows carry an additive ``binding`` key beside the frozen ``kind``. +""" + +from __future__ import annotations + +import json +import stat +import sys +import types +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from skit import argstate, cli, flows, launcher, params, rewrite, store +from skit.langs.python import metawriter +from skit.langs.registry import spec_for +from skit.params import ParamDecl + +runner = CliRunner() + + +@pytest.fixture(autouse=True) +def tmp_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("SKIT_DATA_DIR", str(tmp_path / "data")) + monkeypatch.setenv("SKIT_STATE_DIR", str(tmp_path / "state")) + monkeypatch.setenv("SKIT_CONFIG_DIR", str(tmp_path / "config")) + monkeypatch.setenv("SKIT_LANG", "en") + return tmp_path + + +@pytest.fixture +def run_entry_spy(monkeypatch: pytest.MonkeyPatch): + """Capture the delivery-ready material handed to launcher.run_entry (nothing runs).""" + calls: dict[str, object] = {} + + def fake( + entry, + extra_args=None, + *, + values=None, + invoke_cwd=None, + script_override=None, + env_overlay=None, + runner=None, + prepared=None, + ): + calls["entry"] = entry + calls["extra"] = list(extra_args or []) + calls["values"] = dict(values or {}) + return 0 + + monkeypatch.setattr(launcher, "run_entry", fake) + return calls + + +def _py(tmp_path: Path, body: str, name: str = "job.py") -> Path: + p = tmp_path / name + p.write_text(body, encoding="utf-8") + return p + + +def _fake_tty(monkeypatch: pytest.MonkeyPatch) -> None: + """A real terminal, as far as the CLI's interactivity gates can tell.""" + monkeypatch.setattr(sys, "stdin", types.SimpleNamespace(isatty=lambda: True, read=lambda: "")) + monkeypatch.setattr("sys.stdout.isatty", lambda: True, raising=False) + monkeypatch.setattr(cli, "_is_interactive", lambda: True) + + +# ========================================================================== +# A. rewrite.read_for_block_edit / write_block_edit +# ========================================================================== + +_SHELL_BODY = b'#!/usr/bin/env bash\nWIDTH=800\necho "$WIDTH"\n' +_BLOCK_OPEN = b"# /// script" +_BLOCK_CLOSE = b"# ///" + + +def _shell_block_edit(text: str) -> str: + """The real edit every caller of the pair performs: params_io.write inserts/rewrites the + comment block and touches nothing else.""" + shell = spec_for("shell") + assert shell is not None + assert shell.params_io is not None + return shell.params_io.write( + text, [ParamDecl(name="WIDTH", binding="const", type="int", default=800)] + ) + + +def _without_block(raw: bytes, newline: bytes) -> bytes: + """Drop the inserted comment block, keeping every other byte — terminators included — + exactly where it lies, so the comparison below is a real byte-for-byte claim about the + rest of the file rather than a normalized diff.""" + keep: list[bytes] = [] + inside = False + for chunk in raw.split(newline): + if chunk == _BLOCK_OPEN: + inside = True + continue + if inside: + inside = chunk != _BLOCK_CLOSE + continue + keep.append(chunk) + return newline.join(keep) + + +@pytest.mark.parametrize( + ("newline", "expected"), + [(b"\r\n", "\r\n"), (b"\r", "\r"), (b"\n", "\n")], + ids=["crlf", "lone-cr", "lf"], +) +def test_block_edit_pair_round_trips_every_line_ending_style( + tmp_path: Path, newline: bytes, expected: str +) -> None: + """A CRLF, a lone-CR and an LF copy each survive read → block edit → write with ONLY the + block changed: the style comes back verbatim, no foreign terminator is introduced, and + every non-block byte is identical to what went in. Path.write_text (the old TUI path) + re-expanded \\n to the host os.linesep and rewrote every line of the file.""" + path = tmp_path / "s.sh" + original = _SHELL_BODY.replace(b"\n", newline) + path.write_bytes(original) + + text, detected = rewrite.read_for_block_edit(path) + assert detected == expected + assert "\r" not in text # folded to LF for the LF-based block engine + rewrite.write_block_edit(path, _shell_block_edit(text), detected) + + after = path.read_bytes() + assert b"[tool.skit]" in after # the edit really landed + # The copy's own style, and nothing else: stripping every occurrence of the terminator + # must leave no stray \r or \n anywhere in the file. + stripped = after.replace(newline, b"") + assert b"\r" not in stripped + assert b"\n" not in stripped + # ...and every byte outside the block is exactly what was there before. + assert _without_block(after, newline) == original + + +def test_block_edit_pair_round_trips_non_utf8_bytes(tmp_path: Path) -> None: + """A shell/fish copy may legitimately hold arbitrary bytes. surrogateescape carries them + through a comment-only edit untouched; the strict/replace reads this pair replaced either + raised or baked U+FFFD over every one of them.""" + path = tmp_path / "raw.sh" + original = b"#!/usr/bin/env bash\nWIDTH=800\nprintf '\xff\xfe\\n'\n" + path.write_bytes(original) + + text, newline = rewrite.read_for_block_edit(path) + assert "�" not in text # not decoded lossily... + assert "\udcff" in text # ...but carried as surrogates + rewrite.write_block_edit(path, _shell_block_edit(text), newline) + + after = path.read_bytes() + assert b"[tool.skit]" in after + assert b"\xff\xfe" in after # the raw bytes round-tripped exactly + assert b"\xef\xbf\xbd" not in after # ...and none became U+FFFD + assert _without_block(after, b"\n") == original + + +def test_write_block_edit_keeps_the_executable_bit(tmp_path: Path) -> None: + """mkstemp's temp file is always 0600, so a tmp+replace without the mode carry would strip + the execute bit off a stored copy that copy2 preserved at add — and the entry's next launch + would fail with 'exists but isn't executable'.""" + path = tmp_path / "x.sh" + path.write_bytes(_SHELL_BODY) + path.chmod(0o755) + # What chmod actually produced — Windows has no POSIX mode bits and reports what it likes. + # The contract under test is PRESERVATION, not a particular value. + expected = stat.S_IMODE(path.stat().st_mode) + + text, newline = rewrite.read_for_block_edit(path) + rewrite.write_block_edit(path, _shell_block_edit(text), newline) + + assert b"[tool.skit]" in path.read_bytes() + assert stat.S_IMODE(path.stat().st_mode) == expected + + +def test_onboard_python_degrades_on_a_non_utf8_script_instead_of_crashing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The python onboarding lane read its just-stored copy with a STRICT decode, so a + non-UTF-8 python file escaped as a raw UnicodeDecodeError traceback — after the entry was + already in the store — where the shell lane degraded gracefully on the same input. Through + the shared pair it now round-trips the bytes and writes the block.""" + source = tmp_path / "raw.py" + original = b'CITY = "Taipei"\n# caf\xe9 (latin-1)\nprint(CITY)\n' + source.write_bytes(original) + text = source.read_text(encoding="utf-8", errors="replace") # what `add` hands in + _fake_tty(monkeypatch) + monkeypatch.setattr(cli.Prompt, "ask", staticmethod(lambda *a, **k: "1")) # pick candidate 1 + + entry, _deps, managed, _secrets = cli._onboard_python( + source, text, name="rawpy", description="d", no_input=False + ) + + assert managed == ["CITY"] + stored = entry.script_path.read_bytes() + assert b"[tool.skit]" in stored # the block landed + assert b"\xe9" in stored # ...and the latin-1 byte survived it + assert b"\xef\xbf\xbd" not in stored + + +# ========================================================================== +# B. params.is_secret_name — the whole-word rule and its jammed exceptions +# ========================================================================== + +_SECRET_TRUE = [ + "API_KEY", + "api_key", + "apiKey", + "APIKey", + "GITHUB_TOKEN", + "token", + "access-token", + "passwd", + "password", + "secret", + "Enter your API key:", + "sort_key", + "DB_PASSWORD", + # Jammed spellings: a false NEGATIVE here publishes a literal into current_defaults, + # --json output and plaintext state — the dangerous direction (round 2's repair). + "APIKEY", + "apikey", + "AUTHTOKEN", + "ACCESSTOKEN", + "SECRETKEY", + "sshkey", + "passkey", + "MYSECRET", + "DBPASSWORD", + "licensekey", + "privatekey", + "MYPASSWD", # the fourth long suffix, jammed like its three siblings above + # One jammed spelling per credential qualifier the KEY rule recognizes: each of these + # is a real thing people name a variable, and every one of them must stay masked. + "AUTHKEY", + "ACCESSKEY", + "GPGKEY", + "AWSKEY", + "MASTERKEY", + "SIGNINGKEY", + "ENCRYPTIONKEY", +] + +_SECRET_FALSE = [ + # The reported bug: --max-tokens became a masked, never-prefilled, never-remembered + # password field on the reader lane, where no override exists to turn it off. + "MAX_TOKENS", + "max-tokens", + "maxTokens", + "keyword", + "monkey", + "hotkey", + "tokens", + "PASSPHRASE", + "How many tokens?", + # KEY is too short for a bare suffix rule — these are why the prefix list exists. + "turkey", + "whiskey", + "donkey", + "jockey", + "keyfile", + "KEYBOARD", + "publickey", + "hostkey", + "primarykey", + "foreignkey", +] + + +@pytest.mark.parametrize("name", _SECRET_TRUE) +def test_is_secret_name_matches_real_credential_spellings(name: str) -> None: + assert params.is_secret_name(name) is True + + +@pytest.mark.parametrize("name", _SECRET_FALSE) +def test_is_secret_name_rejects_lookalikes(name: str) -> None: + assert params.is_secret_name(name) is False + + +def test_secret_heuristic_is_universal_across_lanes(tmp_path: Path) -> None: + """One rule, every source: a command template's placeholders run through the same + predicate the analyzers do, so "what counts as secret-looking" cannot fork per lane.""" + store.add_command("run --max-tokens {max_tokens} --key {api_key}", name="c") + plan = flows.plan_for_entry(store.resolve("c")) + by = {f.key: f.secret for f in plan.fields} + assert by["api_key"] is True + assert by["max_tokens"] is False # the false positive that had no override anywhere + + +# ========================================================================== +# C. remove / preset delete — the non-interactive contract +# ========================================================================== + + +def _entry(tmp_path: Path, name: str = "a") -> store.Entry: + return store.add_python(_py(tmp_path, "print(1)\n"), name=name) + + +def test_remove_refuses_without_yes_in_a_pipe(tmp_path: Path) -> None: + """`skit remove` used to typer.confirm inside pipes/CI — eating a line of piped stdin and + dying as click's bare 'Aborted.'. It now refuses the way its sibling `runner remove` does: + a worded exit-2 that names the flag, and nothing removed.""" + _entry(tmp_path) + result = runner.invoke(cli.app, ["remove", "a"]) + assert result.exit_code == 2 + assert "pass --yes" in result.output + assert store.resolve("a").meta.name == "a" # still there + + +def test_remove_refuses_under_no_input_even_on_a_terminal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """--no-input is the explicit half of the same contract: on a real terminal it still + refuses rather than asking, so a script that passes it is deterministic.""" + _entry(tmp_path) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["remove", "a", "--no-input"]) + assert result.exit_code == 2 + assert "pass --yes" in result.output + assert store.resolve("a").meta.name == "a" + + +def test_remove_with_yes_succeeds_non_interactively(tmp_path: Path) -> None: + _entry(tmp_path) + result = runner.invoke(cli.app, ["remove", "a", "--yes"]) + assert result.exit_code == 0, result.output + with pytest.raises(store.NotFoundError): + store.resolve("a") + + +def test_remove_still_confirms_on_a_terminal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """The interactive path survives the guard: a terminal still gets the ask, and "y" removes.""" + _entry(tmp_path) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["remove", "a"], input="y\n") + assert result.exit_code == 0, result.output + with pytest.raises(store.NotFoundError): + store.resolve("a") + + +def test_remove_abort_keeps_the_entry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _entry(tmp_path) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["remove", "a"], input="n\n") + assert result.exit_code == 1 # typer.confirm(abort=True) → Abort + assert store.resolve("a").meta.name == "a" + + +def test_preset_delete_refuses_without_yes_in_a_pipe(tmp_path: Path) -> None: + """A preset is unrecoverable user data that used to be deleted with no ask at all — the + trivially re-creatable config row was better guarded than the thing users typed in.""" + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod"]) + assert result.exit_code == 2 + assert "pass --yes" in result.output + assert argstate.load_state(entry.slug)["presets"] == {"prod": {"CITY": "Taipei"}} + + +def test_preset_delete_refuses_under_no_input_even_on_a_terminal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod", "--no-input"]) + assert result.exit_code == 2 + assert "pass --yes" in result.output + assert argstate.load_state(entry.slug)["presets"] == {"prod": {"CITY": "Taipei"}} + + +def test_preset_delete_with_yes_succeeds_non_interactively(tmp_path: Path) -> None: + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod", "--yes"]) + assert result.exit_code == 0, result.output + assert argstate.load_state(entry.slug)["presets"] == {} + + +def test_preset_delete_still_confirms_on_a_terminal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod"], input="y\n") + assert result.exit_code == 0, result.output + assert 'Delete preset "prod" from a?' in result.output + assert argstate.load_state(entry.slug)["presets"] == {} + + +def test_preset_delete_abort_keeps_the_preset( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + _fake_tty(monkeypatch) + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod"], input="n\n") + assert result.exit_code == 1 + assert argstate.load_state(entry.slug)["presets"] == {"prod": {"CITY": "Taipei"}} + + +def test_preset_delete_unknown_name_fails_before_any_ask( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Confirming a deletion that then turns out to target nothing is a wasted question: the + unknown-name feedback comes BEFORE the confirm, even on a terminal where one would be + asked. typer.confirm must never be reached.""" + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + _fake_tty(monkeypatch) + asked: list[object] = [] + monkeypatch.setattr(cli.typer, "confirm", lambda *a, **k: asked.append(a)) + + result = runner.invoke(cli.app, ["preset", "delete", "a", "ghost"]) + + assert result.exit_code == 1 + assert "Unknown preset" in result.output + assert "prod" in result.output # ...and says what IS available + assert asked == [] # never asked + assert argstate.load_state(entry.slug)["presets"] == {"prod": {"CITY": "Taipei"}} + + +def test_preset_delete_reports_the_same_error_when_it_vanishes_mid_flight( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The pre-check reads outside the lock, so a preset can still disappear between the two + reads (a concurrent agent, another window). delete_preset re-checks UNDER the lock and its + False lands in the very same message — one error for one condition, not two spellings of + it depending on who won the race.""" + entry = _entry(tmp_path) + argstate.save_preset(entry.slug, "prod", {"CITY": "Taipei"}) + monkeypatch.setattr(cli.argstate, "delete_preset", lambda slug, name: False) + + result = runner.invoke(cli.app, ["preset", "delete", "a", "prod", "--yes"]) + + assert result.exit_code == 1 + assert "Unknown preset" in result.output + + +# ========================================================================== +# D. extra-args provenance (argstate + flows + CLI) +# ========================================================================== + + +def test_save_last_persists_and_clears_the_raw_marker() -> None: + """The marker travels WITH the tail: written when a raw tail is saved, popped whenever the + tail is replaced by a processed one or cleared — so a marker can never describe a tail it + didn't come with.""" + argstate.save_last("prov", extra_args=["{today}.png"], extra_args_raw=True) + assert argstate.load_state("prov")["extra_args_raw"] is True + + argstate.save_last("prov", extra_args=["--literal"], extra_args_raw=False) + assert argstate.load_state("prov")["extra_args"] == ["--literal"] + assert argstate.load_state("prov")["extra_args_raw"] is False + + argstate.save_last("prov", extra_args=["{today}.png"], extra_args_raw=True) + assert argstate.load_state("prov")["extra_args_raw"] is True + argstate.save_last("prov", extra_args=[], extra_args_raw=True) # emptied field + assert argstate.load_state("prov")["extra_args"] == [] + assert argstate.load_state("prov")["extra_args_raw"] is False + + +def test_save_last_without_a_tail_leaves_the_marker_alone() -> None: + """A values-only save carries no tail, so it must not touch the tail's provenance.""" + argstate.save_last("keep", extra_args=["{today}.png"], extra_args_raw=True) + argstate.save_last("keep", values={"CITY": "Taipei"}) + state = argstate.load_state("keep") + assert state["extra_args"] == ["{today}.png"] + assert state["extra_args_raw"] is True + + +def test_load_state_defaults_the_marker_for_a_legacy_document() -> None: + """State written before the marker existed holds a tail and no key: it must read back as + False (already shell-processed → replays literally), never as a missing key the callers + would have to guess about.""" + argstate.save_last("legacy", extra_args=["*.png"]) + doc = argstate.load_state("legacy") + assert doc["extra_args"] == ["*.png"] + assert doc["extra_args_raw"] is False + + +def test_save_after_run_threads_the_provenance_to_argstate(tmp_path: Path) -> None: + entry = store.add_command("echo {msg}", name="c") + plan = flows.plan_for_entry(entry) + flows.save_after_run( + entry.slug, + plan, + {"msg": "hi"}, + ["{today}"], + 0, + at="2026-01-01T00:00:00+00:00", + extra_raw=True, + ) + assert argstate.load_state(entry.slug)["extra_args_raw"] is True + + flows.save_after_run( + entry.slug, + plan, + {"msg": "hi"}, + ["*.png"], + 0, + at="2026-01-01T00:00:01+00:00", + extra_raw=False, + ) + assert argstate.load_state(entry.slug)["extra_args_raw"] is False + + +def test_cli_run_expands_a_replayed_raw_tail(tmp_path: Path, run_entry_spy) -> None: + """A tail typed into the TUI form is raw intent text — it never met a shell. Replaying it + under `skit run` must expand its tokens exactly as `r` would: the two faces launch the same + argv from the same state. Before the marker, the CLI replayed it literally and the script + received the bare '{today}'.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["out_{today}.png"], extra_args_raw=True) + + result = runner.invoke(cli.app, ["run", "j", "--no-input"]) + + assert result.exit_code == 0, result.output + (tail,) = run_entry_spy["extra"] + assert tail != "out_{today}.png" # expanded, not passed through + assert tail.startswith("out_20") + assert tail.endswith(".png") + # Intent, never expansion, is what stays on disk — and it stays marked raw. + state = argstate.load_state(entry.slug) + assert state["extra_args"] == ["out_{today}.png"] + assert state["extra_args_raw"] is True + + +def test_cli_run_replays_an_unmarked_tail_literally(tmp_path: Path, run_entry_spy) -> None: + """The complement: a tail the user's shell already processed (or legacy state with no + marker) replays verbatim — a second token pass would rewrite what they deliberately + quoted.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["out_{today}.png"]) + + result = runner.invoke(cli.app, ["run", "j", "--no-input"]) + + assert result.exit_code == 0, result.output + assert run_entry_spy["extra"] == ["out_{today}.png"] + assert argstate.load_state(entry.slug)["extra_args_raw"] is False + + +def test_cli_fresh_tail_is_never_expanded_and_clears_the_marker( + tmp_path: Path, run_entry_spy +) -> None: + """This run's own `-- args` came through the user's shell: never re-expanded, and saved + UNMARKED — so a raw tail left over from a form run can't lend its expansion regime to the + literal one that replaced it.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["was_{today}"], extra_args_raw=True) + + result = runner.invoke(cli.app, ["run", "j", "--no-input", "--", "kept_{today}.png"]) + + assert result.exit_code == 0, result.output + assert run_entry_spy["extra"] == ["kept_{today}.png"] # untouched + state = argstate.load_state(entry.slug) + assert state["extra_args"] == ["kept_{today}.png"] + assert state["extra_args_raw"] is False # the stale marker is gone + + +def test_forget_args_clears_the_tail_and_its_marker(tmp_path: Path, run_entry_spy) -> None: + """--forget-args is the imperative clear: the tail AND the provenance that described it.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["out_{today}.png"], extra_args_raw=True) + + result = runner.invoke(cli.app, ["run", "j", "--no-input", "--forget-args"]) + + assert result.exit_code == 0, result.output + assert run_entry_spy["extra"] == [] + state = argstate.load_state(entry.slug) + assert state["extra_args"] == [] + assert state["extra_args_raw"] is False + + +# ========================================================================== +# H. params --manage on a kind with no params_io names the --add door +# ========================================================================== + + +def _exe(tmp_path: Path, name: str = "prog") -> store.Entry: + prog = tmp_path / ("t.exe" if sys.platform == "win32" else "t") + prog.write_text("#!/bin/sh\necho hi\n", encoding="utf-8") + prog.chmod(0o755) + return store.add_exe(prog, name=name) + + +def test_manage_on_an_exe_names_the_declared_lane_it_does_have(tmp_path: Path) -> None: + """The declared [[parameters]] lane IS an exe's parameter home. A refusal that named no way + forward hid the exact door built for it.""" + _exe(tmp_path) + result = runner.invoke(cli.app, ["params", "prog", "--manage", "WIDTH"]) + assert result.exit_code == 1 + out = " ".join(result.output.split()) + assert "prog has no managed parameters — its kind has no analyzer to read them from." in out + assert "Declare one instead: skit params prog --add PARAM" in out + + +def test_manage_on_a_python_entry_takes_the_analyzer_path_with_no_hint(tmp_path: Path) -> None: + """The complement: a kind that HAS an analyzer never sees the refusal — nor its hint. The + manage really happens.""" + entry = store.add_python(_py(tmp_path, 'CITY = "Taipei"\nprint(CITY)\n'), name="p") + result = runner.invoke(cli.app, ["params", "p", "--manage", "CITY"]) + assert result.exit_code == 0, result.output + assert "Declare one instead" not in result.output + assert "no managed parameters" not in result.output + io = spec_for("python") + assert io is not None + assert io.params_io is not None + stored = entry.script_path.read_text(encoding="utf-8") + assert [d.name for d in io.params_io.read(stored)] == ["CITY"] + + +# ========================================================================== +# I. params --json rows carry "binding" beside the frozen "kind" +# ========================================================================== + + +def test_params_json_rows_carry_both_kind_and_binding(tmp_path: Path) -> None: + """ "kind" is the FROZEN on-disk key and carries the BINDING (const/input) — while `show + --json`'s "kind" is the entry's LANGUAGE. The additive "binding" key lets an agent read one + axis by one unambiguous name; "kind" stays for the files already on disk.""" + text = metawriter.write_params( + 'CITY = "Taipei"\nNAME = input("Name: ")\nprint(CITY, NAME)\n', + [ + ParamDecl(name="CITY", binding="const", type="str", default="Taipei"), + ParamDecl(name="NAME", binding="input", type="str", prompt="Name: "), + ], + ) + store.add_python(_py(tmp_path, text), name="p") + + result = runner.invoke(cli.app, ["params", "p", "--json"]) + assert result.exit_code == 0, result.output + rows = json.loads(result.output)["params"] + + assert [r["name"] for r in rows] == ["CITY", "NAME"] + assert [r["binding"] for r in rows] == ["const", "input"] + for row in rows: + assert row["kind"] == row["binding"] # same axis, both spellings, no drift + + +def test_params_json_binding_is_additive_not_a_rename(tmp_path: Path) -> None: + """The frozen row is still emitted whole — "binding" is added ON TOP of to_block_dict, never + instead of a key an existing consumer reads.""" + text = metawriter.write_params( + 'TOKEN = "x"\nprint(TOKEN)\n', + [ParamDecl(name="TOKEN", binding="const", type="str", default="x", secret=True)], + ) + store.add_python(_py(tmp_path, text), name="s") + (row,) = json.loads(runner.invoke(cli.app, ["params", "s", "--json"]).output)["params"] + decl = ParamDecl(name="TOKEN", binding="const", type="str", default="x", secret=True) + assert row == {**decl.to_block_dict(), "binding": "const"} diff --git a/tests/test_design_audit_tui.py b/tests/test_design_audit_tui.py new file mode 100644 index 0000000..558ffcd --- /dev/null +++ b/tests/test_design_audit_tui.py @@ -0,0 +1,659 @@ +"""Behavior coverage for the design-audit fixes (rounds 1 and 2), TUI half. + +A. THE round-1 HIGH: the add-review panel wrote parameter blocks back with + read_text/write_text, silently rewriting every line ending of the just-stored copy. Pinned + at the SCREEN level — the helper-level round-trip lives in tests/test_design_audit_fixes.py, + but this is the surface where the corruption actually shipped. +D. Extra-args provenance, TUI face: `r` replays a CLI-captured tail literally, the form's own + tail expands, and the marker rides through the deferred (exit-mode) save on PendingRun. +E. The Library detail pane's plan cache: keyed on the script AND its meta.toml, popped by edit + and by the settings-close callback, and _has_drift served off the very same entry. +F. AddReviewScreen._reader_modeled memoizes its probe — for a reader kind each call is a + synchronous subprocess and the panel must not freeze because a radio button was clicked. +G. The chord grammar repair: Ctrl+O meant three things on three sibling screens while README + documents one. Choose variables → Ctrl+L (both screens), Preferences' squatters → Ctrl+G / + Ctrl+Y, and every moved chord gets a positive pilot test plus a negative for the vacated one. +""" + +from __future__ import annotations + +import contextlib +from pathlib import Path + +import pytest +from textual.widgets import Checkbox, Input, Select + +from skit import argstate, config, flows, launcher, store, tui +from skit.langs.python import metawriter +from skit.params import ParamDecl +from skit.tui_add import AddReviewScreen, PromptReviewScreen +from skit.tui_form import _EXTRA_KEY, FieldRow, RunFormScreen +from skit.tui_prefs import PreferencesScreen +from skit.tui_prompt import PromptCandidatePickerModal +from skit.tui_settings import ScriptSettingsScreen + + +@pytest.fixture(autouse=True) +def tmp_store(tmp_path, monkeypatch): + monkeypatch.setenv("SKIT_DATA_DIR", str(tmp_path / "data")) + monkeypatch.setenv("SKIT_STATE_DIR", str(tmp_path / "state")) + monkeypatch.setenv("SKIT_CONFIG_DIR", str(tmp_path / "config")) + monkeypatch.setenv("SKIT_LANG", "en") + + +@contextlib.contextmanager +def _noop_suspend(): + yield + + +@pytest.fixture +def quiet_run(monkeypatch): + """Neutralize the terminal-ownership pieces of _execute and capture the launch, keeping + the app alive afterwards (after_run=stay) so the test can inspect the Library.""" + config.save_after_run("stay") + calls: dict[str, object] = {} + + def fake_run( + entry, + extra_args=None, + *, + values=None, + invoke_cwd=None, + script_override=None, + env_overlay=None, + runner=None, + ): + calls["extra"] = list(extra_args or []) + calls["values"] = dict(values or {}) + return calls.get("code", 0) + + monkeypatch.setattr(launcher, "run_entry", fake_run) + monkeypatch.setattr(tui.MenuApp, "suspend", lambda self: _noop_suspend()) + return calls + + +def _py(tmp_path, body: str, name: str = "job.py") -> Path: + p = tmp_path / name + p.write_text(body, encoding="utf-8") + return p + + +def _plan_key(entry) -> tuple[float, float]: + return (entry.script_path.stat().st_mtime, (entry.dir / "meta.toml").stat().st_mtime) + + +def _detail_text(app) -> str: + return " ".join(str(s.render()) for s in app.query("#detail-body Static")) + + +# ========================================================================== +# A. the add-review panel's write-back keeps the copy's bytes +# ========================================================================== + +_CRLF_SHELL = b'#!/usr/bin/env bash\r\nWIDTH=800\r\necho "$WIDTH"\r\n' + + +def _without_block(raw: bytes, newline: bytes) -> bytes: + """Drop the inserted comment block, keeping every other byte — terminators included — + exactly where it lies, so the comparison is a real byte-for-byte claim about the rest of + the file rather than a normalized diff.""" + keep: list[bytes] = [] + inside = False + for chunk in raw.split(newline): + if chunk == b"# /// script": + inside = True + continue + if inside: + inside = chunk != b"# ///" + continue + keep.append(chunk) + return newline.join(keep) + + +async def test_add_review_accept_keeps_a_crlf_copy_byte_exact(tmp_path): + """THE round-1 HIGH regression, at the surface where it shipped: ticking a detected + candidate on a CRLF shell script and accepting must leave the STORED copy CRLF, with every + non-block byte exactly as it was. read_text/write_text (the old path here) re-expanded \\n + to the host os.linesep, rewriting every line of a file skit had just been handed.""" + src = tmp_path / "crlf.sh" + src.write_bytes(_CRLF_SHELL) + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + review = AddReviewScreen(src, kind="shell") + app.push_screen(review) + await pilot.pause() + review.query_one("#rv-name", Input).value = "crlfsh" + review.query_one("#rv-cand-0", Checkbox).value = True # tick WIDTH + review.action_accept() + await pilot.pause() + + stored = store.resolve("crlfsh").script_path.read_bytes() + assert b"[tool.skit]" in stored # the tick really was written + assert b'name = "WIDTH"' in stored + # CRLF end to end: stripping every CRLF pair must leave no bare terminator behind. + stripped = stored.replace(b"\r\n", b"") + assert b"\r" not in stripped + assert b"\n" not in stripped + # ...and every byte that isn't part of the inserted block is identical. + assert _without_block(stored, b"\r\n") == _CRLF_SHELL + + +async def test_add_review_accept_keeps_non_utf8_bytes(tmp_path): + """The same write-back carries arbitrary bytes through: the panel READS with + errors="replace" for display, so writing that text back would have baked U+FFFD over every + one of them.""" + src = tmp_path / "raw.sh" + original = b"#!/usr/bin/env bash\nWIDTH=800\nprintf '\xff\\n'\n" + src.write_bytes(original) + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + review = AddReviewScreen(src, kind="shell") + app.push_screen(review) + await pilot.pause() + review.query_one("#rv-name", Input).value = "rawsh" + review.query_one("#rv-cand-0", Checkbox).value = True + review.action_accept() + await pilot.pause() + + stored = store.resolve("rawsh").script_path.read_bytes() + assert b"[tool.skit]" in stored + assert b"\xff" in stored # round-tripped exactly + assert b"\xef\xbf\xbd" not in stored # ...never replaced + assert _without_block(stored, b"\n") == original + + +# ========================================================================== +# D. extra-args provenance, TUI face +# ========================================================================== + +MANAGED = 'CITY = "Taipei"\nprint(CITY)\n' + + +def _managed_entry(tmp_path, name: str = "j"): + text = metawriter.write_params( + MANAGED, [ParamDecl(name="CITY", binding="const", type="str", default="Taipei")] + ) + return store.add_python(_py(tmp_path, text), name=name) + + +async def test_rerun_replays_a_cli_captured_tail_literally(tmp_path, quiet_run): + """A tail the user's shell already processed must NOT get a second token/glob pass just + because the rerun happens from the TUI. Before the marker, `r` re-expanded every stored + tail — rewriting exactly what the user had deliberately quoted. `{today}` is the + discriminator here: a second pass would rewrite it, cwd or no cwd.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["out_{today}.txt", "*.png"]) # unmarked → literal + argstate.record_run(entry.slug, 0, at="2026-07-09T00:00:00+00:00") + + app = tui.MenuApp() + async with app.run_test() as pilot: + app.action_rerun() + await pilot.pause() + + assert quiet_run["extra"] == ["out_{today}.txt", "*.png"] # verbatim, both pieces + assert argstate.load_state(entry.slug)["extra_args_raw"] is False + + +async def test_rerun_expands_a_form_captured_tail(tmp_path, quiet_run): + """The complement: a tail the FORM captured is raw intent text, so `r` expands it — the + same argv the run form would have produced.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + argstate.save_last(entry.slug, extra_args=["out_{today}.txt"], extra_args_raw=True) + argstate.record_run(entry.slug, 0, at="2026-07-09T00:00:00+00:00") + + app = tui.MenuApp() + async with app.run_test() as pilot: + app.action_rerun() + await pilot.pause() + + (tail,) = quiet_run["extra"] + assert tail != "out_{today}.txt" + assert tail.startswith("out_20") + assert tail.endswith(".txt") + # Intent, not expansion, stays on disk — still marked raw for the next replay. + state = argstate.load_state(entry.slug) + assert state["extra_args"] == ["out_{today}.txt"] + assert state["extra_args_raw"] is True + + +async def test_form_submit_expands_its_tail_and_records_it_raw(tmp_path, quiet_run): + """The form's extra field has no shell behind it, so its tokens expand on the way out + (extra_raw defaults to True on _execute) and the tail is remembered MARKED, so `skit run` + replaying it later expands it the same way.""" + entry = _managed_entry(tmp_path) + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + await pilot.pause() + app.action_run() + await pilot.pause() + screen = app.screen + assert isinstance(screen, RunFormScreen) + extra_row = next(r for r in screen.query(FieldRow) if r.field.key == _EXTRA_KEY) + extra_row.set_value("out_{today}.txt") + await pilot.pause() + screen.action_submit() + await pilot.pause() + + (tail,) = quiet_run["extra"] + assert tail != "out_{today}.txt" # the form's own text expanded on the way out + assert tail.startswith("out_20") + assert tail.endswith(".txt") + state = argstate.load_state(entry.slug) + assert state["extra_args"] == ["out_{today}.txt"] # intent persisted, never expansion + assert state["extra_args_raw"] is True # ...marked, so the CLI replays it the same way + + +async def test_exit_mode_hands_the_forms_marker_to_the_pending_run(tmp_path, quiet_run): + """Out of the box skit is a launcher: the form's submit exits the TUI with a PendingRun and + the save happens afterwards, in _finish_run. The tail's provenance has to ride ALONG — a + run must not be remembered under a different expansion regime just because after_run is + "exit" rather than "stay". (quiet_run pins stay for the workbench tests; flip back here.)""" + config.save_after_run("exit") + entry = _managed_entry(tmp_path) + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + await pilot.pause() + app.action_run() + await pilot.pause() + screen = app.screen + assert isinstance(screen, RunFormScreen) + extra_row = next(r for r in screen.query(FieldRow) if r.field.key == _EXTRA_KEY) + extra_row.set_value("out_{today}.txt") + await pilot.pause() + screen.action_submit() + await pilot.pause() + + pending = app.return_value + assert isinstance(pending, tui.PendingRun) + assert pending.entry.slug == entry.slug + assert pending.extra == ["out_{today}.txt"] # intent, carried unexpanded + assert pending.extra_raw is True # ...and marked, so _finish_run records it raw + + +async def test_pending_run_carries_the_marker_into_the_deferred_save(tmp_path, monkeypatch): + """Exit mode defers the save to _finish_run, so PendingRun has to carry the bit an + immediate save would have recorded — otherwise the same run would be remembered under a + different expansion regime depending only on after_run.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="j") + plan = flows.plan_for_entry(entry) + asm = flows.assemble(plan, {}, ["out_{today}.txt"], cwd=tmp_path) + monkeypatch.setattr("builtins.print", lambda *a, **k: None) + monkeypatch.setattr(flows, "execute", lambda *a, **k: flows.RunOutcome(0, "", "")) + + tui._finish_run( + tui.PendingRun(entry, plan, asm, {}, ["out_{today}.txt"], extra_raw=True, show_drift=False) + ) + assert argstate.load_state(entry.slug)["extra_args_raw"] is True + + tui._finish_run( + tui.PendingRun(entry, plan, asm, {}, ["out_{today}.txt"], extra_raw=False, show_drift=False) + ) + assert argstate.load_state(entry.slug)["extra_args_raw"] is False + + +# ========================================================================== +# E. the Library detail pane's plan cache +# ========================================================================== + + +@pytest.fixture +def plan_builds(monkeypatch): + """Count real plan builds. The detail pane re-renders on every RowHighlighted, and for a + reader kind a build is a synchronous subprocess with a cold-start runtime.""" + calls: list[str] = [] + real = flows.plan_for_entry + + def counting(entry): + calls.append(entry.slug) + return real(entry) + + monkeypatch.setattr(tui.flows, "plan_for_entry", counting) + return calls + + +def test_cached_plan_serves_a_second_call_without_rebuilding(tmp_path, plan_builds): + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + app = tui.MenuApp() + first = app._cached_plan(entry) + assert plan_builds == [entry.slug] + assert app._cached_plan(entry) is first # the very same object, not a rebuild + assert plan_builds == [entry.slug] + + +def test_cached_plan_invalidates_when_the_script_changes(tmp_path, plan_builds): + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + app = tui.MenuApp() + app._cached_plan(entry) + import os + + stamp = entry.script_path.stat().st_mtime + 10 + os.utime(entry.script_path, (stamp, stamp)) + app._cached_plan(entry) + assert plan_builds == [entry.slug, entry.slug] # the body changed → rebuilt + + +def test_cached_plan_invalidates_when_meta_toml_changes(tmp_path, plan_builds): + """A plan is a function of meta.toml too (declared [[parameters]] rows, a prompt's managed + list / interpolate switch). Keying on the script alone left the detail pane stale FOREVER + when an agent ran `skit params --add` beside an open TUI — the product's own coexistence + story.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + app = tui.MenuApp() + app._cached_plan(entry) + import os + + meta = entry.dir / "meta.toml" + stamp = meta.stat().st_mtime + 10 + os.utime(meta, (stamp, stamp)) + app._cached_plan(entry) + assert plan_builds == [entry.slug, entry.slug] + + +def test_cached_plan_builds_fresh_when_the_script_cannot_be_stat_ed(tmp_path, plan_builds): + """An entry whose script is gone (a removed target) can't produce a key at all: it just + builds fresh — and caches nothing — rather than letting the OSError escape a cursor- + movement handler and crash the app.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="ghost") + entry.script_path.unlink() + app = tui.MenuApp() + app._cached_plan(entry) + app._cached_plan(entry) + assert plan_builds == [entry.slug, entry.slug] # never cached, never crashed + assert entry.slug not in app._plan_cache + + +def test_has_drift_is_served_off_the_same_cache(tmp_path, plan_builds): + """The drift badge and the detail pane are one read, not two: asking for drift after the + pane already built the plan must not build a second one.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + app = tui.MenuApp() + app._cached_plan(entry) + assert app._has_drift(entry) is False + assert plan_builds == [entry.slug] # ...still only the one build + + +def test_has_drift_short_circuits_before_building_any_plan(tmp_path, plan_builds): + """Drift is the EXPENSIVE check, and the guard exists so a kind that cannot produce drift + lines never pays for a plan to find that out. An exe has no analyzer, so the answer is + False without a single build — even though its stored copy is right there to be read.""" + prog = tmp_path / "t" + prog.write_text("#!/bin/sh\necho hi\n", encoding="utf-8") + prog.chmod(0o755) + entry = store.add_exe(prog, name="prog") + assert entry.script_path.exists() # the guard's other arms are NOT what answers here + app = tui.MenuApp() + + assert app._has_drift(entry) is False + assert plan_builds == [] # short-circuited: no plan was ever built + + +def test_has_drift_reports_the_cached_plans_drift(tmp_path): + """It really reads the plan's drift rather than always answering False.""" + drifted = metawriter.write_params( + "CITY = 'x'\nprint(CITY)\n", + [ + ParamDecl(name="CITY", binding="const", type="str"), + ParamDecl(name="GONE", binding="const", type="str"), + ], + ) + entry = store.add_python(_py(tmp_path, drifted, "drifty.py"), name="drifty") + app = tui.MenuApp() + assert app._has_drift(entry) is True + + +async def test_edit_pops_this_entrys_cache_key(tmp_path, monkeypatch): + """The stored copy may change under $EDITOR without its mtime helping (same-second + writes), so the edit drops this slug's key outright.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + monkeypatch.setattr(tui.editor, "open_in_editor", lambda p: None) + monkeypatch.setattr(tui.MenuApp, "suspend", lambda self: _noop_suspend()) + app = tui.MenuApp() + async with app.run_test() as pilot: + await pilot.pause() + planted = flows.FormPlan(source="none", drift_lines=["planted"]) + app._plan_cache[entry.slug] = (_plan_key(entry), planted) + app.action_edit() + await pilot.pause() + assert app._plan_cache.get(entry.slug, (None, None))[1] is not planted + + +async def test_settings_close_pops_this_entrys_cache_key(tmp_path): + """A Resync inside Script settings can change the definitions without touching the script, + so the close callback drops the key too.""" + entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") + app = tui.MenuApp() + async with app.run_test() as pilot: + await pilot.pause() + app.action_settings() + await pilot.pause() + planted = flows.FormPlan(source="none", drift_lines=["planted"]) + app._plan_cache[entry.slug] = (_plan_key(entry), planted) + app.screen.dismiss(False) + await pilot.pause() + assert app._plan_cache.get(entry.slug, (None, None))[1] is not planted + assert "The script changed" not in _detail_text(app) + + +# ========================================================================== +# F. AddReviewScreen._reader_modeled memoizes its probe +# ========================================================================== + + +_UNMODELED_SH = "#!/usr/bin/env bash\nCITY=Taipei\necho $CITY\n" +# Exactly ONE reader field: "modeled" is `> 0`, not "more than one" — a single-option script +# is as modeled as a ten-option one, and its form is the entry's real interface. +_MODELED_SH = "#!/usr/bin/env bash\nwhile getopts 'n:' o; do :; done\n" + + +@pytest.fixture +def reader_probes(monkeypatch): + """Every text flows.reader_fields is actually asked about. The screen is built but NOT + pushed, so compose can't warm the memo behind the count.""" + probes: list[str] = [] + real = flows.reader_fields + + def counting(spec, text): + probes.append(text) + return real(spec, text) + + monkeypatch.setattr(flows, "reader_fields", counting) + return probes + + +def test_reader_modeled_probes_once_per_text(tmp_path, reader_probes): + """The probe fires on every mode toggle and on every accept-gate check. For a reader kind + (PowerShell) each one is a synchronous subprocess, so the panel would freeze for seconds + because a radio button was clicked — three calls, exactly one probe.""" + src = tmp_path / "m.sh" + src.write_text(_UNMODELED_SH, encoding="utf-8") + review = AddReviewScreen(src, kind="shell") + + assert review._reader_modeled() is False + assert review._reader_modeled() is False + assert review._reader_modeled() is False + + assert reader_probes == [_UNMODELED_SH] # exactly one, then the memo answers + + +def test_reader_modeled_recomputes_after_the_text_changes(tmp_path, reader_probes): + """The memo is keyed on the TEXT, so the Ctrl+E edit→rescan path recomputes naturally — a + memo keyed on nothing would pin the panel to the pre-edit verdict forever.""" + src = tmp_path / "m.sh" + src.write_text(_UNMODELED_SH, encoding="utf-8") + review = AddReviewScreen(src, kind="shell") + assert review._reader_modeled() is False + + # What the edit→rescan path does: new text in, verdict re-derived from it. + review._text = _MODELED_SH + assert review._reader_modeled() is True + assert review._reader_modeled() is True # ...and the new verdict memoizes too + + assert reader_probes == [_UNMODELED_SH, _MODELED_SH] + + +# ========================================================================== +# G. the chord grammar repair — one chord, one meaning +# ========================================================================== + + +def _prompt_entry(tmp_path, text="{{a}} {{b}} {{c}}\n", name="p"): + src = tmp_path / f"{name}.prompt.md" + src.write_text(text, encoding="utf-8") + return store.add_prompt(src, name=name) + + +async def _open_settings(app, pilot): + app.action_settings() + await pilot.pause() + assert isinstance(app.screen, ScriptSettingsScreen) + await pilot.pause() + return app.screen + + +def _many_names(count: int) -> list[str]: + return [f"u{i}" for i in range(count)] + + +async def test_prompt_review_ctrl_l_opens_choose_variables(tmp_path): + """Positive pilot test for the moved chord (AGENTS.md: every key a footer advertises + must have one).""" + from skit.langs.prompt.analyzer import AUTO_MANAGE_LIMIT + + names = _many_names(AUTO_MANAGE_LIMIT + 2) + src = tmp_path / "big.prompt.md" + src.write_text(" ".join(f"{{{{{n}}}}}" for n in names), encoding="utf-8") + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + await pilot.pause() + app.push_screen(PromptReviewScreen(src)) + await pilot.pause() + await pilot.press("ctrl+l") + await pilot.pause() + assert isinstance(app.screen, PromptCandidatePickerModal) + await pilot.press("escape") + await pilot.pause() + + +async def test_prompt_review_ctrl_o_no_longer_opens_choose_variables(tmp_path): + """The negative twin: Ctrl+O is the grammar chord for "restore the default" and must not + mean anything else on a sibling screen.""" + from skit.langs.prompt.analyzer import AUTO_MANAGE_LIMIT + + names = _many_names(AUTO_MANAGE_LIMIT + 2) + src = tmp_path / "big.prompt.md" + src.write_text(" ".join(f"{{{{{n}}}}}" for n in names), encoding="utf-8") + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + await pilot.pause() + review = PromptReviewScreen(src) + app.push_screen(review) + await pilot.pause() + await pilot.press("ctrl+o") + await pilot.pause() + assert app.screen is review # nothing opened + await pilot.press("escape") + await pilot.pause() + + +async def test_settings_ctrl_l_opens_choose_variables(tmp_path): + from skit.langs.prompt.analyzer import LIST_PREVIEW_LIMIT + + names = _many_names(LIST_PREVIEW_LIMIT + 3) + entry = _prompt_entry(tmp_path, text=" ".join(f"{{{{{n}}}}}" for n in names)) + store.write_prompt_managed(entry.slug, []) + app = tui.MenuApp() + async with app.run_test(size=(110, 40)) as pilot: + await pilot.pause() + await _open_settings(app, pilot) + await pilot.press("ctrl+l") + await pilot.pause() + assert isinstance(app.screen, PromptCandidatePickerModal) + await pilot.press("escape", "escape") + await pilot.pause() + + +async def test_settings_ctrl_o_no_longer_opens_choose_variables(tmp_path): + from skit.langs.prompt.analyzer import LIST_PREVIEW_LIMIT + + names = _many_names(LIST_PREVIEW_LIMIT + 3) + entry = _prompt_entry(tmp_path, text=" ".join(f"{{{{{n}}}}}" for n in names)) + store.write_prompt_managed(entry.slug, []) + app = tui.MenuApp() + async with app.run_test(size=(110, 40)) as pilot: + await pilot.pause() + screen = await _open_settings(app, pilot) + await pilot.press("ctrl+o") + await pilot.pause() + assert app.screen is screen # Ctrl+O stays reserved for "restore the default" + await pilot.press("escape") + await pilot.pause() + + +async def test_prefs_ctrl_g_opens_manage_agents_and_ctrl_o_does_not(tmp_path): + """Preferences' two squatters moved off the grammar chords. Both halves in one test so the + positive and the negative can never drift apart.""" + from skit.tui_runner import RunnerManageScreen + + app = tui.MenuApp() + async with app.run_test() as pilot: + app.push_screen(PreferencesScreen()) + await pilot.pause() + screen = app.screen + assert isinstance(screen, PreferencesScreen) + screen.query_one("#pf-lang", Select).focus() + await pilot.pause() + + await pilot.press("ctrl+o") # the vacated chord + await pilot.pause() + assert app.screen is screen # ...opens nothing + + await pilot.press("ctrl+g") + await pilot.pause() + assert isinstance(app.screen, RunnerManageScreen) + + +async def test_prefs_ctrl_y_installs_the_skill_and_ctrl_k_does_not(tmp_path, monkeypatch): + """Ctrl+K is every Input's delete-to-end-of-line; on a screen full of text fields it must + open nothing. Ctrl+Y — a chord with no meaning anywhere else in skit — carries the action.""" + from skit import agentskill + from skit.tui_prefs import SkillInstallModal + + monkeypatch.setattr(agentskill, "detect_targets", lambda *, home, cwd: []) + app = tui.MenuApp() + async with app.run_test() as pilot: + app.push_screen(PreferencesScreen()) + await pilot.pause() + screen = app.screen + assert isinstance(screen, PreferencesScreen) + screen.query_one("#pf-lang", Select).focus() + await pilot.pause() + + await pilot.press("ctrl+k") # the vacated chord + await pilot.pause() + assert app.screen is screen + + await pilot.press("ctrl+y") + await pilot.pause() + assert isinstance(app.screen, SkillInstallModal) + + +async def test_run_form_ctrl_o_still_restores_the_default(tmp_path): + """The chord the others were cleared FOR: Ctrl+O on the run form restores the field's + definition default over a remembered value — the one meaning README documents.""" + entry = _managed_entry(tmp_path, name="reset") + argstate.save_last(entry.slug, values={"CITY": "Kaohsiung"}) + app = tui.MenuApp() + async with app.run_test(size=(100, 40)) as pilot: + await pilot.pause() + app.action_run() + await pilot.pause() + screen = app.screen + assert isinstance(screen, RunFormScreen) + row = next(r for r in screen.query(FieldRow) if r.field.key == "CITY") + box = row.query_one(Input) + assert box.value == "Kaohsiung" + box.focus() + await pilot.pause() + await pilot.press("ctrl+o") + await pilot.pause() + assert box.value == "Taipei" # the script's own default came back diff --git a/tests/test_flows.py b/tests/test_flows.py index a59519d..709bb62 100644 --- a/tests/test_flows.py +++ b/tests/test_flows.py @@ -519,11 +519,15 @@ def test_save_after_run_persists_intent_and_stamps_run(tmp_path): ["--fast"], 0, at="2026-07-09T14:30:05+00:00", + extra_raw=True, # the TUI form path: the extra field is raw intent text ) state = argstate.load_state("s") assert state["values"]["OUTPUT"] == "long_{today}.jpg" # raw token text, not expansion assert "API_KEY" not in state["values"] # C3 assert state["extra_args"] == ["--fast"] + # ... and the tail's PROVENANCE travels with it, so a later replay from either face + # re-expands exactly the tails that were captured raw. + assert state["extra_args_raw"] is True assert state["last_run"] == { "at": "2026-07-09T14:30:05+00:00", "exit": 0, @@ -939,13 +943,18 @@ def test_save_after_run_clears_cleared_extra_args(tmp_path): entry = _python_entry(tmp_path, MANAGED_SCRIPT, slug="clr") plan = flows.plan_for_entry(entry) flows.save_after_run( - "clr", plan, {"OUTPUT": "a"}, ["--fast"], 0, at="2026-01-01T00:00:00+00:00" + "clr", plan, {"OUTPUT": "a"}, ["--fast"], 0, at="2026-01-01T00:00:00+00:00", extra_raw=True ) assert argstate.load_state("clr")["extra_args"] == ["--fast"] + assert argstate.load_state("clr")["extra_args_raw"] is True # the form's raw intent text # The user emptied the extra-args field: the cleared state must PERSIST (the old - # falsy-merge resurrected it forever). - flows.save_after_run("clr", plan, {"OUTPUT": "a"}, [], 0, at="2026-01-01T00:00:01+00:00") + # falsy-merge resurrected it forever) — and the provenance marker goes with it, so a + # later CLI tail can never inherit the old form tail's expansion regime. + flows.save_after_run( + "clr", plan, {"OUTPUT": "a"}, [], 0, at="2026-01-01T00:00:01+00:00", extra_raw=True + ) assert argstate.load_state("clr")["extra_args"] == [] + assert argstate.load_state("clr")["extra_args_raw"] is False def test_save_after_run_purges_secret_placeholder_from_presets(tmp_path): @@ -957,7 +966,9 @@ def test_save_after_run_purges_secret_placeholder_from_presets(tmp_path): argstate.save_preset("c3", "old", {"api_key": "sk-123"}) argstate.save_last("c3", values={"api_key": "sk-123"}) plan = flows.plan_for_entry(entry) - flows.save_after_run("c3", plan, {"api_key": "sk-456"}, [], 0, at="2026-01-01T00:00:00+00:00") + flows.save_after_run( + "c3", plan, {"api_key": "sk-456"}, [], 0, at="2026-01-01T00:00:00+00:00", extra_raw=False + ) state = argstate.load_state("c3") assert "api_key" not in state["values"] assert all("api_key" not in p for p in state["presets"].values()) diff --git a/tests/test_prompt_tui.py b/tests/test_prompt_tui.py index 4fe1bb3..8eaed7a 100644 --- a/tests/test_prompt_tui.py +++ b/tests/test_prompt_tui.py @@ -1010,7 +1010,7 @@ async def test_settings_candidate_picker_reaches_a_hidden_name_and_waits_for_out async with app.run_test(size=(110, 40)) as pilot: await pilot.pause() screen = await _open_settings(app, pilot) - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1072,13 +1072,13 @@ async def test_settings_candidate_picker_cancel_and_unchanged_done_are_noops(tmp await pilot.pause() screen = await _open_settings(app, pilot) - await pilot.press("ctrl+o", "escape") + await pilot.press("ctrl+l", "escape") await pilot.pause() assert app.screen is screen assert screen._pending_prompt_candidates == set() assert screen._dirty is False - await pilot.press("ctrl+o", "ctrl+s") + await pilot.press("ctrl+l", "ctrl+s") await pilot.pause() assert app.screen is screen assert screen._pending_prompt_candidates == set() @@ -1088,7 +1088,7 @@ async def test_settings_candidate_picker_cancel_and_unchanged_done_are_noops(tmp async def test_settings_candidate_picker_tolerates_preview_recompose(tmp_path): - """A queued Ctrl+O/Done can straddle a responsive recompose. Missing old preview + """A queued Ctrl+L/Done can straddle a responsive recompose. Missing old preview widgets are skipped by name while the full modal selection still survives.""" from skit.langs.prompt.analyzer import LIST_PREVIEW_LIMIT @@ -1100,7 +1100,7 @@ async def test_settings_candidate_picker_tolerates_preview_recompose(tmp_path): await pilot.pause() screen = await _open_settings(app, pilot) await screen.query_one("#st-prompt-new-0", Checkbox).remove() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1123,13 +1123,13 @@ async def test_settings_choose_variables_key_is_harmless_when_off_or_short(tmp_p insertion = screen.query_one("#st-interpolate", Checkbox) insertion.value = False await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() assert app.screen is screen insertion.value = True await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() assert app.screen is screen await pilot.press("escape") @@ -1191,7 +1191,7 @@ async def test_review_candidate_picker_keyboard_reaches_a_hidden_name(tmp_path): await pilot.pause() app.push_screen(PromptReviewScreen(src)) await pilot.pause() - await pilot.press("ctrl+o") # the advertised full-list keyboard path + await pilot.press("ctrl+l") # the advertised full-list keyboard path await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1257,7 +1257,7 @@ async def test_review_candidate_picker_keeps_search_and_footer_usable_on_tiny_sc await pilot.pause() app.push_screen(PromptReviewScreen(src)) await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1289,7 +1289,7 @@ async def test_review_candidate_picker_empty_search_and_cancel_are_keyboard_oper await pilot.pause() review = app.screen assert isinstance(review, PromptReviewScreen) - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1325,7 +1325,7 @@ async def test_review_candidate_picker_tolerates_preview_recompose(tmp_path): review = PromptReviewScreen(src) app.push_screen(review) await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() modal = app.screen assert isinstance(modal, PromptCandidatePickerModal) @@ -1349,7 +1349,7 @@ async def test_review_choose_variables_key_is_harmless_for_a_short_prompt(tmp_pa review = PromptReviewScreen(src) app.push_screen(review) await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+l") await pilot.pause() assert app.screen is review await pilot.press("escape") diff --git a/tests/test_source_default_semantics.py b/tests/test_source_default_semantics.py index ab298de..1f2f7c0 100644 --- a/tests/test_source_default_semantics.py +++ b/tests/test_source_default_semantics.py @@ -410,6 +410,7 @@ def test_save_after_run_persists_via_the_remembered_rule(tmp_path): [], 0, at="2026-07-09T14:30:05+00:00", + extra_raw=False, # a CLI-style run: no tail at all, nothing to re-expand ) state = argstate.load_state("rem") assert state["values"] == {"WIDTH": "900"} # GREETING (== default) dropped; WIDTH kept diff --git a/tests/test_store_mut.py b/tests/test_store_mut.py index 3478ce1..792606a 100644 --- a/tests/test_store_mut.py +++ b/tests/test_store_mut.py @@ -22,7 +22,7 @@ import pytest -from skit import argstate, config, store +from skit import argstate, config, rewrite, store from skit.atomic import atomic_write_bytes from skit.models import ScriptMeta, now_iso, slugify from skit.params import ParamDecl @@ -619,11 +619,13 @@ def test_update_dependencies_reference_mode_never_touches_disk(sample_script): def test_update_dependencies_reads_and_writes_script_py_as_utf8(sample_script, monkeypatch): """The copy-mode PEP 723 sync reads the stored copy as BYTES (strict utf-8 decode, so a - non-utf-8 byte is preserved instead of replaced) and writes it back through - atomic_write_bytes_keep_mode (atomic + permission-preserving, and byte-exact so the - copy's own line endings survive) — never a plain read_text/write_text. Its - mutation-killing purpose is retained: the stored copy still round-trips as UTF-8 with - the edit applied.""" + non-utf-8 byte is preserved instead of replaced) and writes it back through the shared + rewrite.write_block_edit pair, whose write half is atomic_write_bytes_keep_mode (atomic + + permission-preserving, and byte-exact so the copy's own line endings survive) — never a + plain read_text/write_text. The spy sits on rewrite's own reference to the atomic writer, + so the assertion still reaches the real bytes the sync lands even though store.py now + calls it through write_block_edit. Its mutation-killing purpose is retained: the stored + copy still round-trips as UTF-8 with the edit applied.""" entry = store.add_python(sample_script) read_bytes_paths: list[Path] = [] @@ -634,14 +636,14 @@ def spy_read_bytes(self): return real_read_bytes(self) keep_mode_writes: list[tuple[Path, bytes]] = [] - real_keep_mode = store.atomic_write_bytes_keep_mode + real_keep_mode = rewrite.atomic_write_bytes_keep_mode def spy_keep_mode(path, data): keep_mode_writes.append((path, data)) return real_keep_mode(path, data) monkeypatch.setattr(Path, "read_bytes", spy_read_bytes) - monkeypatch.setattr(store, "atomic_write_bytes_keep_mode", spy_keep_mode) + monkeypatch.setattr(rewrite, "atomic_write_bytes_keep_mode", spy_keep_mode) store.update_dependencies(entry.slug, ["httpx"], ">=3.11") assert any(p.name == "script.py" for p in read_bytes_paths), ( diff --git a/tests/test_tui_edit.py b/tests/test_tui_edit.py index 1bf82bb..e5f1adc 100644 --- a/tests/test_tui_edit.py +++ b/tests/test_tui_edit.py @@ -8,7 +8,7 @@ import pytest from textual.widgets import Static -from skit import store, tui +from skit import flows, store, tui @pytest.fixture(autouse=True) @@ -74,16 +74,23 @@ async def test_edit_command_entry_reports_no_source(tmp_path, monkeypatch): assert "no editable source" in str(app.query_one("#status", Static).render()) -async def test_edit_invalidates_the_drift_cache(tmp_path, monkeypatch): +async def test_edit_invalidates_the_plan_cache(tmp_path, monkeypatch): entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") monkeypatch.setattr(tui.editor, "open_in_editor", lambda p: None) monkeypatch.setattr(tui.MenuApp, "suspend", lambda self: _noop_suspend()) app = tui.MenuApp() async with app.run_test() as pilot: - app._drift_cache[entry.slug] = (0.0, True) + # A stale sentinel under an impossible mtime key, carrying drift the file doesn't + # have: only the post-edit pop + re-derivation can replace it. + stale = flows.FormPlan(source="none", drift_lines=["stale sentinel"]) + app._plan_cache[entry.slug] = ((0.0, 0.0), stale) app.action_edit() await pilot.pause() - # The stale sentinel is gone: the reload re-derived the truth from the file. - mtime, drift = app._drift_cache[entry.slug] - assert mtime != 0.0 - assert drift is False + # The stale sentinel is gone: the reload re-derived the truth from the files. + key, plan = app._plan_cache[entry.slug] + assert key == ( + entry.script_path.stat().st_mtime, + (entry.dir / "meta.toml").stat().st_mtime, + ) + assert plan is not stale + assert plan.drift_lines == [] diff --git a/tests/test_tui_mut.py b/tests/test_tui_mut.py index c45580c..b4ed3f8 100644 --- a/tests/test_tui_mut.py +++ b/tests/test_tui_mut.py @@ -863,7 +863,7 @@ def test_finish_run_executes_records_and_passes_the_code_through(tmp_path, quiet quiet_run["code"] = 7 printed: list[str] = [] monkeypatch.setattr("builtins.print", lambda *a, **k: printed.append(" ".join(map(str, a)))) - pending = tui.PendingRun(entry, plan, asm, {}, [], show_drift=True) + pending = tui.PendingRun(entry, plan, asm, {}, [], extra_raw=True, show_drift=True) monkeypatch.setattr(tui.MenuApp, "run", lambda self: pending) assert tui.run_menu() == 7 assert "values" in quiet_run @@ -881,7 +881,9 @@ def test_finish_run_launch_failure_uses_docker_codes_and_records_nothing(tmp_pat printed: list[str] = [] monkeypatch.setattr("builtins.print", lambda *a, **k: printed.append(" ".join(map(str, a)))) monkeypatch.setattr( - tui.MenuApp, "run", lambda self: tui.PendingRun(entry, plan, asm, {}, [], False) + tui.MenuApp, + "run", + lambda self: tui.PendingRun(entry, plan, asm, {}, [], extra_raw=True, show_drift=False), ) assert tui.run_menu() == 127 assert argstate.load_state(entry.slug)["last_run"] == {} diff --git a/tests/test_tui_mut_part01.py b/tests/test_tui_mut_part01.py index 6bc3f0e..0abb1a6 100644 --- a/tests/test_tui_mut_part01.py +++ b/tests/test_tui_mut_part01.py @@ -116,7 +116,7 @@ def test_finish_run_prints_banner_drift_and_transparency_all_flushed( calls: list[tuple[tuple[object, ...], dict[str, object]]] = [] monkeypatch.setattr("builtins.print", lambda *a, **k: calls.append((a, k))) - pending = tui.PendingRun(entry, plan, asm, {}, [], show_drift=True) + pending = tui.PendingRun(entry, plan, asm, {}, [], extra_raw=True, show_drift=True) assert tui._finish_run(pending) == 0 # the script's own code passes through # The run banner: exact framing + msgid, flushed. @@ -152,7 +152,7 @@ def test_finish_run_launch_failure_prints_error_and_uses_docker_code(tmp_path, m calls: list[tuple[tuple[object, ...], dict[str, object]]] = [] monkeypatch.setattr("builtins.print", lambda *a, **k: calls.append((a, k))) - pending = tui.PendingRun(entry, plan, asm, {}, [], show_drift=False) + pending = tui.PendingRun(entry, plan, asm, {}, [], extra_raw=True, show_drift=False) assert tui._finish_run(pending) == 127 # docker code for a missing target err = next(((a, k) for a, k in calls if a and str(a[0]).startswith("Error:")), None) @@ -175,5 +175,5 @@ def test_finish_run_unmapped_failure_falls_back_to_generic_skit_error_code(tmp_p "execute", lambda *a, **k: flows.RunOutcome(None, "totally-unknown-failure", "boom"), ) - pending = tui.PendingRun(entry, plan, asm, {}, [], show_drift=False) + pending = tui.PendingRun(entry, plan, asm, {}, [], extra_raw=True, show_drift=False) assert tui._finish_run(pending) == 125 diff --git a/tests/test_tui_mut_part05.py b/tests/test_tui_mut_part05.py index 57eb282..19a03ec 100644 --- a/tests/test_tui_mut_part05.py +++ b/tests/test_tui_mut_part05.py @@ -8,8 +8,8 @@ so an unflushed banner would interleave with the script's own output); a launch that never starts prints an error line and lands a "couldn't launch" status; a nonzero exit records a "failed (code N)" status; EOF at the "press Enter" prompt is swallowed rather -than crashing the workbench; and the lazy, mtime-keyed drift cache is trusted only while -fresh and reflects the plan's real drift. +than crashing the workbench; and the lazy, mtime-keyed PLAN cache _has_drift is served +off is trusted only while fresh and reflects the plan's real drift. """ from __future__ import annotations @@ -180,7 +180,7 @@ async def test_launch_failure_prints_flushed_error_and_couldnt_launch_status( # --------------------------------------------------------------------------- -# _has_drift: the guard and the lazy mtime-keyed cache +# _has_drift: the guard and the lazy mtime-keyed plan cache it reads # --------------------------------------------------------------------------- @@ -198,15 +198,18 @@ def test_has_drift_is_false_and_never_stats_a_missing_script(tmp_path): def test_has_drift_trusts_a_fresh_mtime_matching_cache(tmp_path): - """A cache entry whose stored mtime equals the file's current mtime is trusted verbatim — - the expensive plan/reconcile is skipped and the cached verdict returned. Planting True for - a script whose real drift is False proves the cache is honored: mutmut_9 (mtime=None), - mutmut_10 (cached=None), mutmut_11 (get(None)), mutmut_14 (cached[1]) and mutmut_15 (!=) - all miss the plant and recompute the real False.""" + """A cache entry whose stored key equals the CURRENT (script mtime, meta.toml mtime) pair + is trusted verbatim — the expensive plan/reconcile is skipped and the cached plan's drift + returned. Planting a drifting plan for a script whose real drift is empty proves the cache + is honored: any mutant that drops the key, the lookup or the equality test misses the plant + and recomputes the real False.""" entry = store.add_python(_py(tmp_path, "print(1)\n"), name="clean") - mtime = entry.script_path.stat().st_mtime + key = ( + entry.script_path.stat().st_mtime, + (entry.dir / "meta.toml").stat().st_mtime, + ) app = tui.MenuApp() - app._drift_cache[entry.slug] = (mtime, True) + app._plan_cache[entry.slug] = (key, flows.FormPlan(source="none", drift_lines=["planted"])) assert app._has_drift(entry) is True diff --git a/tests/test_tui_mut_part09.py b/tests/test_tui_mut_part09.py index 545c8ae..e137558 100644 --- a/tests/test_tui_mut_part09.py +++ b/tests/test_tui_mut_part09.py @@ -1,7 +1,7 @@ """Mutation-kill tests for src/skit/tui.py — chunk 9/10. Covers the Library action paths: Ctrl+C double-tap quit + toast, edit (no-source -message, editor-error print/prompt/EOF suppression, drift-cache invalidation), the +message, editor-error print/prompt/EOF suppression, plan-cache invalidation), the Script-settings deep-link + close callback, run/rerun guards and value plumbing. Each test pins real, observable behaviour through the public TUI surface (Textual @@ -16,7 +16,7 @@ from textual.containers import VerticalScroll from textual.widgets import DataTable, Input, Static -from skit import argstate, config, launcher, store, tui +from skit import argstate, config, flows, launcher, store, tui from skit.langs.python import metawriter from skit.params import ParamDecl from skit.tui_form import FieldRow, RunFormScreen @@ -176,8 +176,13 @@ async def test_edit_editor_error_lands_on_the_status_line(tmp_path, monkeypatch) assert "Edited" not in status -async def test_edit_invalidates_fresh_drift_cache_entry(tmp_path, monkeypatch): - """The stored copy may change under the editor, so edit drops this slug's drift +def _plan_key(entry) -> tuple[float, float]: + """The cache key _cached_plan builds: (script mtime, meta.toml mtime).""" + return (entry.script_path.stat().st_mtime, (entry.dir / "meta.toml").stat().st_mtime) + + +async def test_edit_invalidates_fresh_plan_cache_entry(tmp_path, monkeypatch): + """The stored copy may change under the editor, so edit drops this slug's plan cache entry — even a fresh (mtime-matching) one — forcing a re-derivation. A stale 'has drift' sentinel that survives would light a false 'script changed' warning.""" entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") @@ -186,25 +191,27 @@ async def test_edit_invalidates_fresh_drift_cache_entry(tmp_path, monkeypatch): app = tui.MenuApp() async with app.run_test() as pilot: await pilot.pause() - # A cache entry whose mtime matches the file on disk: only the pop can clear it + # A cache entry whose key matches the files on disk: only the pop can clear it # (an mtime mismatch would recompute regardless, masking the bug). - mtime = entry.script_path.stat().st_mtime - app._drift_cache[entry.slug] = (mtime, True) + app._plan_cache[entry.slug] = ( + _plan_key(entry), + flows.FormPlan(source="none", drift_lines=["planted"]), + ) app.action_edit() await pilot.pause() assert "The script changed" not in _detail_text(app) # re-derived: no real drift -async def test_edit_drift_cache_pop_tolerates_an_absent_slug(tmp_path, monkeypatch): +async def test_edit_plan_cache_pop_tolerates_an_absent_slug(tmp_path, monkeypatch): """The post-edit pop must not assume the slug is cached — dropping pop's default - would raise KeyError and crash the edit whenever drift was never computed.""" + would raise KeyError and crash the edit whenever no plan was ever cached.""" store.add_python(_py(tmp_path, "print(1)\n"), name="j") monkeypatch.setattr(tui.editor, "open_in_editor", lambda p: None) monkeypatch.setattr(tui.MenuApp, "suspend", lambda self: _noop_suspend()) app = tui.MenuApp() async with app.run_test() as pilot: await pilot.pause() - app._drift_cache.clear() # slug is absent when the pop runs + app._plan_cache.clear() # slug is absent when the pop runs app.action_edit() # a KeyError here would crash the edit await pilot.pause() assert "Edited j." in _status_text(app) @@ -261,8 +268,8 @@ async def test_settings_close_reloads_the_library(tmp_path): assert app.query_one(DataTable).row_count == 2 # _closed reloaded the table -async def test_settings_close_invalidates_fresh_drift_cache_entry(tmp_path): - """The _closed callback pops this slug's drift cache (a Resync could have changed +async def test_settings_close_invalidates_fresh_plan_cache_entry(tmp_path): + """The _closed callback pops this slug's plan cache (a Resync could have changed the definitions), even a fresh mtime-matching entry. A stale sentinel would show a bogus 'script changed' warning after closing.""" entry = store.add_python(_py(tmp_path, "print(1)\n"), name="a") @@ -271,14 +278,16 @@ async def test_settings_close_invalidates_fresh_drift_cache_entry(tmp_path): await pilot.pause() app.action_settings() await pilot.pause() - mtime = entry.script_path.stat().st_mtime - app._drift_cache[entry.slug] = (mtime, True) + app._plan_cache[entry.slug] = ( + _plan_key(entry), + flows.FormPlan(source="none", drift_lines=["planted"]), + ) app.screen.dismiss(False) await pilot.pause() assert "The script changed" not in _detail_text(app) -async def test_settings_close_drift_pop_tolerates_an_absent_slug(tmp_path): +async def test_settings_close_plan_pop_tolerates_an_absent_slug(tmp_path): """The _closed pop must survive an uncached slug: dropping pop's default raises KeyError inside the dismiss callback, which surfaces as an app crash.""" store.add_python(_py(tmp_path, "print(1)\n"), name="a") @@ -290,7 +299,7 @@ async def test_settings_close_drift_pop_tolerates_an_absent_slug(tmp_path): from skit.tui_settings import ScriptSettingsScreen assert isinstance(app.screen, ScriptSettingsScreen) - app._drift_cache.clear() # slug absent when _closed's pop runs + app._plan_cache.clear() # slug absent when _closed's pop runs app.screen.dismiss(False) await pilot.pause() # The screen closed cleanly; a KeyError in _closed would fault the app on exit. diff --git a/tests/test_tui_prefs_agents_cov.py b/tests/test_tui_prefs_agents_cov.py index 7e9afc3..ddad513 100644 --- a/tests/test_tui_prefs_agents_cov.py +++ b/tests/test_tui_prefs_agents_cov.py @@ -222,13 +222,13 @@ async def test_prefs_dirty_esc_discard_closes(tmp_path): assert config.load_js_runner() == "" # the unsaved edit never persisted -# ------------------------------------------------- chord grammar: Ctrl+O / Ctrl+K +# ------------------------------------------------- chord grammar: Ctrl+G / Ctrl+Y async def test_prefs_ctrl_k_in_an_input_edits_the_field_not_a_modal(tmp_path): - """Ctrl+O/Ctrl+K are NON-priority: with focus in a prefs Input, Ctrl+K - is the Input's own delete-to-end-of-line — the screen must NOT answer a text-editing - chord with a modal on a screen full of text fields.""" + """Ctrl+K stays the Input's own delete-to-end-of-line on Preferences: the screen binds + NOTHING to it (its two squatters moved to Ctrl+G/Ctrl+Y), so a screen full of text fields + can never answer a text-editing chord with a modal.""" app = tui.MenuApp() async with app.run_test() as pilot: app.push_screen(PreferencesScreen()) @@ -246,8 +246,8 @@ async def test_prefs_ctrl_k_in_an_input_edits_the_field_not_a_modal(tmp_path): assert editor_box.value == "micro" # delete-to-end ran in the Input -async def test_prefs_ctrl_k_from_non_input_focus_opens_the_skill_modal(tmp_path, monkeypatch): - """The twin: from a non-Input focus (the language dropdown boots there) Ctrl+K still +async def test_prefs_ctrl_y_from_non_input_focus_opens_the_skill_modal(tmp_path, monkeypatch): + """The twin: from a non-Input focus (the language dropdown boots there) Ctrl+Y opens the Teach-an-AI-agent modal — the chord fires, just never over an Input.""" monkeypatch.setattr(agentskill, "detect_targets", lambda *, home, cwd: []) app = tui.MenuApp() @@ -257,13 +257,13 @@ async def test_prefs_ctrl_k_from_non_input_focus_opens_the_skill_modal(tmp_path, screen = _as(app.screen, PreferencesScreen) screen.query_one("#pf-lang", Select).focus() # a non-Input widget await pilot.pause() - await pilot.press("ctrl+k") + await pilot.press("ctrl+y") await pilot.pause() assert isinstance(app.screen, SkillInstallModal) -async def test_prefs_ctrl_o_from_non_input_focus_opens_manage_runners(tmp_path): - """Ctrl+O from a non-Input focus opens the Manage agents screen.""" +async def test_prefs_ctrl_g_from_non_input_focus_opens_manage_runners(tmp_path): + """Ctrl+G from a non-Input focus opens the Manage agents screen.""" from skit.tui_runner import RunnerManageScreen app = tui.MenuApp() @@ -273,7 +273,7 @@ async def test_prefs_ctrl_o_from_non_input_focus_opens_manage_runners(tmp_path): screen = _as(app.screen, PreferencesScreen) screen.query_one("#pf-lang", Select).focus() await pilot.pause() - await pilot.press("ctrl+o") + await pilot.press("ctrl+g") await pilot.pause() assert isinstance(app.screen, RunnerManageScreen) @@ -288,8 +288,8 @@ async def test_prefs_agents_count_updates_after_managing(tmp_path): await pilot.pause() screen = _as(app.screen, PreferencesScreen) assert "8 agents configured" in str(screen.query_one("#pf-runner-count", Static).render()) - # Ctrl+O opens the manage screen; remove one runner (confirm) and come back. - await pilot.press("ctrl+o") + # Ctrl+G opens the manage screen; remove one runner (confirm) and come back. + await pilot.press("ctrl+g") await pilot.pause() manage = app.screen assert isinstance(manage, RunnerManageScreen) @@ -322,7 +322,7 @@ async def test_prefs_agents_count_empty_state(tmp_path, monkeypatch): ) -async def test_prefs_ctrl_t_opens_then_cancel_notifies_nothing(tmp_path, monkeypatch): +async def test_prefs_ctrl_y_opens_then_cancel_notifies_nothing(tmp_path, monkeypatch): monkeypatch.setattr(agentskill, "detect_targets", lambda *, home, cwd: []) notes: list[str] = [] monkeypatch.setattr( @@ -332,7 +332,7 @@ async def test_prefs_ctrl_t_opens_then_cancel_notifies_nothing(tmp_path, monkeyp async with app.run_test() as pilot: app.push_screen(PreferencesScreen()) await pilot.pause() - await pilot.press("ctrl+k") + await pilot.press("ctrl+y") await pilot.pause() assert isinstance(app.screen, SkillInstallModal) _as(app.screen, SkillInstallModal).action_cancel() # dismiss(None): no notify @@ -396,7 +396,7 @@ async def test_skill_modal_empty_targets_shows_hint(tmp_path, monkeypatch): assert not app.screen.query(OptionList) -async def test_prefs_ctrl_t_install_notifies_with_the_written_path(tmp_path, monkeypatch): +async def test_prefs_ctrl_y_install_notifies_with_the_written_path(tmp_path, monkeypatch): monkeypatch.setattr(agentskill, "detect_targets", lambda *, home, cwd: [_fake_target(tmp_path)]) notes: list[str] = [] monkeypatch.setattr( @@ -406,7 +406,7 @@ async def test_prefs_ctrl_t_install_notifies_with_the_written_path(tmp_path, mon async with app.run_test() as pilot: app.push_screen(PreferencesScreen()) await pilot.pause() - await pilot.press("ctrl+k") + await pilot.press("ctrl+y") await pilot.pause() options = app.screen.query_one(OptionList) options.highlighted = 0 diff --git a/tests/test_tui_prefs_mut.py b/tests/test_tui_prefs_mut.py index 1737643..e07f961 100644 --- a/tests/test_tui_prefs_mut.py +++ b/tests/test_tui_prefs_mut.py @@ -496,7 +496,7 @@ async def test_install_skill_notify_is_letter_exact(tmp_path, monkeypatch): async with app.run_test() as pilot: await _open_prefs(app) await pilot.pause() - await pilot.press("ctrl+k") + await pilot.press("ctrl+y") await pilot.pause() options = app.screen.query_one(OptionList) options.highlighted = 0 From 9ed215264a8229832df587dd3f47dcbe9f9ae3c7 Mon Sep 17 00:00:00 2001 From: Yi-Ting Chiu Date: Sun, 26 Jul 2026 05:00:59 -0700 Subject: [PATCH 04/40] =?UTF-8?q?fix:=20design-audit=20round=204=20?= =?UTF-8?q?=E2=80=94=20post-#33=20merge=20review=20follow-ups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-4 review of merged PR #33 came back clean at medium-and-above and verified all four interaction seams with the audit branch. Two low notes fixed: - argstate reads the extra_args_raw marker with `is True` instead of bool(), matching the house rule for hand-editable bools (models.interpolate, config.enabled): a hand-edited scalar must degrade to the safe literal-replay default, never coerce truthy toward re-expansion. Pinned by a test. - AGENTS.md records #33's one sanctioned bend of the read-command contract (a listing may self-heal skit's own registry index) next to the rules it bends, the way the A5 exception is recorded. --- AGENTS.md | 5 ++++- src/skit/argstate.py | 6 +++++- tests/test_design_audit_fixes.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da8d172..f89d3ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,10 @@ principles. What this means in practice: untouched; skit errors are 125/126/127), `--no-input`, `--dry-run`, and dynamic completion. The non-interactive contract is absolute: in a pipe, in CI, or under `--no-input`, never guess, never prompt, never silently assemble a broken command. When choosing between designs, -prefer the one an agent can drive deterministically. +prefer the one an agent can drive deterministically. One sanctioned bend, recorded like the +A5 exception: read commands never prompt and never touch user data, but a listing may +self-heal **skit's own registry index** (`store._repair_rows`) — atomic, lock-protected, +skipped under any contention, and convergent, so reads stay safe to run concurrently. **5. Verification gate:** 100% test coverage floor, ruff, ty (strictest mode), mutation testing with mutmut (zero surviving mutants), and the i18n coverage gate are all hard CI diff --git a/src/skit/argstate.py b/src/skit/argstate.py index 4be7e11..ade4e8a 100644 --- a/src/skit/argstate.py +++ b/src/skit/argstate.py @@ -93,7 +93,11 @@ def load_state(slug: str) -> dict[str, Any]: return { "values": dict(doc.get("values", {})), "extra_args": list(doc.get("extra_args", [])), - "extra_args_raw": bool(doc.get("extra_args_raw", False)), + # `is True`, not bool(): the house rule for hand-editable bools (models.py's + # interpolate, config.py's enabled). A hand-edited `extra_args_raw = "no"` + # must degrade to the safe literal-replay default, never coerce truthy toward + # re-expansion — the exact direction the provenance marker exists to prevent. + "extra_args_raw": doc.get("extra_args_raw") is True, "presets": {k: dict(v) for k, v in doc.get("presets", {}).items()}, "last_run": dict(doc.get("last_run", {})), } diff --git a/tests/test_design_audit_fixes.py b/tests/test_design_audit_fixes.py index ac3106b..f86c9c9 100644 --- a/tests/test_design_audit_fixes.py +++ b/tests/test_design_audit_fixes.py @@ -498,6 +498,22 @@ def test_load_state_defaults_the_marker_for_a_legacy_document() -> None: assert doc["extra_args_raw"] is False +def test_a_hand_edited_marker_degrades_to_literal_replay() -> None: + """The house rule for hand-editable bools (`is True`, models.interpolate's discipline): + a values file is TOML a person can edit, and `extra_args_raw = "no"` must land on the + safe literal-replay default — a truthy-string coercion would flip the tail toward + re-expansion, the exact surprise the marker exists to prevent.""" + from skit.paths import values_dir + + values_dir().mkdir(parents=True, exist_ok=True) + (values_dir() / "edited.toml").write_text( + 'extra_args = ["*.png"]\nextra_args_raw = "no"\n', encoding="utf-8" + ) + doc = argstate.load_state("edited") + assert doc["extra_args"] == ["*.png"] + assert doc["extra_args_raw"] is False + + def test_save_after_run_threads_the_provenance_to_argstate(tmp_path: Path) -> None: entry = store.add_command("echo {msg}", name="c") plan = flows.plan_for_entry(entry) From 67d7ee12dc7efa7e77ebbcd5cdb0eada85345089 Mon Sep 17 00:00:00 2001 From: Yi-Ting Chiu Date: Sun, 26 Jul 2026 06:16:33 -0700 Subject: [PATCH 05/40] =?UTF-8?q?fix:=20design-audit=20round=205=20?= =?UTF-8?q?=E2=80=94=20max-review=20findings=20on=20the=20audit=20branch?= =?UTF-8?q?=20itself?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow review (max effort) confirmed 15 findings against PR #34's own fixes; the seven correctness ones, worst first: - is_secret_name regressed plurals (API_KEYS, SECRETS, GITHUB_TOKENS) and acronym+lowercase jams (APIkey, SSHkey) to non-secret — false negatives that publish live literals into --json and state files. The heuristic is now segment-based (no camel regex to mangle nonstandard casing; one trailing S folds away) with an explicit TOKEN count-context rule: count qualifiers (max_tokens, token_limit, nTokens) suppress, credential qualifiers (github_tokens, session_token) mask, bare plural 'tokens' reads as a count. - The launch menu was a third face of the provenance rule: a CLI-captured literal tail prefilled into the form and submitted untouched was re-expanded and its stored marker flipped to raw for every later replay. An untouched tail now keeps its recorded provenance; only text the user actually edited is re-captured as form text. - Replaying a marker-less tail that carries token/glob syntax now says so on stderr (legacy pre-provenance state replays literally by design — silently was the bug), and the docs explain the one-time re-capture. - _cached_plan re-resolves the entry from the store on a cache miss: the Library's in-memory row can predate the very meta.toml change that invalidated the key, and building from it pinned a stale plan under the fresh key past every reload. The key also tightens to (mtime_ns, size) per file, narrowing the coarse-mtime blind spot to same-tick same-size writes. - The add panel's reader-modeled memo keys on the reader's runtime fingerprint (new CliReader field, wired for PowerShell) so installing pwsh mid-session is seen instead of serving the tool-less verdict forever. - Preferences action docstrings caught up with the chord move (Ctrl+G/Ctrl+Y). Cleanups: the strict block-edit read lanes (--normalize, deps sync) now go through read_for_block_edit(errors="strict") — the fold/detect discipline lives in exactly one place; the destructive-trio confirmation guard is one helper (_require_yes); the --add hint is one msgid instead of two sentences spliced with a hard-coded ASCII space; _SECRET_WORDS/_SECRET_SUFFIXES no longer shadow each other; analysis.py's header lists its third resident (ArgSpec). i18n at 100% for the new msgids. Tests follow in the next commit. --- docs/content/docs/parameters.mdx | 6 +- src/skit/analysis.py | 5 +- src/skit/cli.py | 87 +- src/skit/langs/base.py | 6 + src/skit/langs/powershell/cli_reader.py | 7 + src/skit/langs/registry.py | 9 +- src/skit/locales/skit.pot | 1140 +++++++++---------- src/skit/locales/zh_CN/LC_MESSAGES/skit.mo | Bin 100610 -> 101030 bytes src/skit/locales/zh_CN/LC_MESSAGES/skit.po | 1147 ++++++++++---------- src/skit/locales/zh_TW/LC_MESSAGES/skit.mo | Bin 100642 -> 101065 bytes src/skit/locales/zh_TW/LC_MESSAGES/skit.po | 1147 ++++++++++---------- src/skit/params.py | 76 +- src/skit/rewrite.py | 15 +- src/skit/store.py | 28 +- src/skit/tui.py | 58 +- src/skit/tui_add.py | 20 +- src/skit/tui_prefs.py | 4 +- 17 files changed, 1935 insertions(+), 1820 deletions(-) diff --git a/docs/content/docs/parameters.mdx b/docs/content/docs/parameters.mdx index dbacf84..884caec 100644 --- a/docs/content/docs/parameters.mdx +++ b/docs/content/docs/parameters.mdx @@ -48,7 +48,11 @@ per-field `delivers_empty` in `skit show --json`. clears it; `--raw` never replays it. A replayed tail keeps the semantics it was captured with: a tail typed into the launch menu's extra field re-expands its tokens and globs on each run, while a tail your shell already processed - replays literally — in both the CLI and the TUI. + replays literally — in both the CLI and the TUI, and submitting the launch + menu with the prefilled tail untouched keeps whichever it was. Tails + remembered by versions before this record existed replay literally (skit + notes it on stderr when they contain tokens or globs); re-enter the tail in + the launch menu once to re-capture it as expanding form text. - **Run history**: each entry's state file records the last run's time, exit code, and exact values. - All of this lives in skit's state directory (one TOML per entry) — see diff --git a/src/skit/analysis.py b/src/skit/analysis.py index e4c1f76..07786dc 100644 --- a/src/skit/analysis.py +++ b/src/skit/analysis.py @@ -1,6 +1,9 @@ """Language-neutral analysis model + drift reconciliation (docs/design/multilang.md). -Two things live here, shared by every analyzable language (python today, shell next): +Three things live here, shared by every analyzable language (python today, shell next): + +- **The CLI-surface model** — `ArgSpec`, the result type every language's `cli_reader` + returns (argparse/click/typer, parseArgs, getopts, fish argparse, PowerShell param()). - **The candidate model** — `Candidate` (one detected parameter) and `Analysis` (the whole detection result). Every language's `analyze(text) -> Analysis` returns these; they are diff --git a/src/skit/cli.py b/src/skit/cli.py index 1553cf2..45418ba 100644 --- a/src/skit/cli.py +++ b/src/skit/cli.py @@ -42,7 +42,7 @@ from .i18n import gettext, ngettext from .langs.registry import KNOWN_KINDS, spec_for from .params import ParamDecl, declared_from_meta, edit_declared, is_secret_name -from .rewrite import detect_newline, read_for_block_edit, write_block_edit +from .rewrite import read_for_block_edit, write_block_edit if TYPE_CHECKING: from collections.abc import Callable @@ -80,6 +80,15 @@ def _fail(message: str, code: int) -> typer.Exit: return typer.Exit(code) +def _require_yes(yes: bool, no_input: bool, message: str) -> None: + """The destructive-command half of the non-interactive contract, in ONE place + (remove, preset delete, runner remove): in a pipe/CI or under --no-input, a missing + -y is a worded exit-2 refusal — never a typer.confirm that eats a line of piped + stdin or dies as click's bare "Aborted.".""" + if not yes and (no_input or not _is_interactive()): + raise _fail(message, EXIT_USAGE) + + def _is_interactive() -> bool: return sys.stdin.isatty() and sys.stdout.isatty() @@ -2530,13 +2539,9 @@ def remove( entry = store.resolve(name) except store.NotFoundError as exc: raise _fail(str(exc), 1) from exc - # The non-interactive contract, same guard as runner_remove: in a pipe/CI or under - # --no-input, a missing -y is a worded exit-2 refusal — never a typer.confirm that - # eats a line of piped stdin or dies as click's bare "Aborted.". - if not yes and (no_input or not _is_interactive()): - raise _fail( - gettext("Confirmation is required; pass --yes to remove the entry."), EXIT_USAGE - ) + _require_yes( + yes, no_input, gettext("Confirmation is required; pass --yes to remove the entry.") + ) if not yes: entry_spec = spec_for(entry.meta.kind) if entry_spec is not None and not entry_spec.has_original_file: @@ -3158,6 +3163,13 @@ def run( err_console.print( f"[dim]{gettext('Reusing your last arguments: %(args)s') % {'args': ' '.join(escape(a) for a in extra)}}[/dim]" ) + if not extra_raw and any(ch in piece for piece in extra for ch in "{*?["): + # De-silence the literal replay exactly where it could surprise: a tail + # with token/glob syntax but no raw marker (captured via the CLI — or + # remembered before provenance existed) is passed through untouched. + err_console.print( + f"[dim]{escape(gettext('(passed as-is — a remembered tail only expands {tokens} and globs when it was typed into the launch menu)'))}[/dim]" + ) try: asm = flows.assemble(plan, values, extra, cwd=Path.cwd(), expand_extra=extra_raw) except flows.FormError as exc: @@ -3525,10 +3537,9 @@ def runner_remove( % {"count": pinned_count} + "[/yellow]" ) - if not yes and (no_input or not _is_interactive()): - raise _fail( - gettext("Confirmation is required; pass --yes to remove the runner."), EXIT_USAGE - ) + _require_yes( + yes, no_input, gettext("Confirmation is required; pass --yes to remove the runner.") + ) if not yes: # The same ask its two siblings give: skit remove confirms unless -y, and the # TUI's agent removal confirms — deleting config rows is not a one-keystroke act. @@ -3692,10 +3703,9 @@ def preset_delete( # A preset is unrecoverable user data; its deletion gets the same ask (and the same # non-interactive refusal) as removing an entry or a runner — the trivially # re-creatable config row must not be better guarded than the thing users typed in. - if not yes and (no_input or not _is_interactive()): - raise _fail( - gettext("Confirmation is required; pass --yes to delete the preset."), EXIT_USAGE - ) + _require_yes( + yes, no_input, gettext("Confirmation is required; pass --yes to delete the preset.") + ) if not yes: typer.confirm( gettext('Delete preset "%(preset)s" from %(name)s?') @@ -4523,17 +4533,27 @@ def _edit_params( console = _maybe_quiet(quiet) entry_spec = spec_for(entry.meta.kind) if entry_spec is None or entry_spec.params_io is None or entry_spec.analyzer is None: - message = gettext( - "%(name)s has no managed parameters — its kind has no analyzer to read them from." - ) % {"name": entry.meta.name} if entry_spec is not None and entry_spec.params_io is None: # The declared [[parameters]] lane IS this kind's parameter home (exe, command, # prompt, the Tier-0 interpreted kinds) — a refusal that names no way forward - # would hide the exact door that was built for it. - message += " " + gettext("Declare one instead: skit params %(name)s --add PARAM") % { - "name": entry.meta.name - } - raise _fail(message, 1) + # would hide the exact door that was built for it. ONE msgid, not two sentences + # spliced with a hard-coded space: translators own the whole pair, including + # its punctuation and order. + raise _fail( + gettext( + "%(name)s has no managed parameters — its kind has no analyzer to read " + "them from. Declare one instead: skit params %(name)s --add PARAM" + ) + % {"name": entry.meta.name}, + 1, + ) + raise _fail( + gettext( + "%(name)s has no managed parameters — its kind has no analyzer to read them from." + ) + % {"name": entry.meta.name}, + 1, + ) if entry.meta.mode == "reference": raise _fail( gettext( @@ -4678,13 +4698,12 @@ def _normalize_params( copy_path = entry.script_path if not copy_path.exists(): raise _fail(gettext("%(name)s has no stored copy to edit.") % {"name": entry.meta.name}, 1) - # Bytes in, bytes out: --normalize rewrites the script's own text, and the whole - # parse-and-splice pipeline is strict UTF-8 end to end — a lossy read (errors="replace") - # would bake U+FFFD over every non-UTF-8 byte on write-back. A script that doesn't decode - # is refused whole instead, leaving the stored copy byte-for-byte untouched. - raw = copy_path.read_bytes() + # The shared read half, strict: --normalize rewrites the script's own text, and the + # parse-and-splice pipeline is strict UTF-8 end to end — a script that doesn't decode + # is refused whole (this lane's OWN policy; the comment-only edits keep their + # surrogateescape default), leaving the stored copy byte-for-byte untouched. try: - text = raw.decode("utf-8") # pragma: no mutate — codec alias + text, newline = read_for_block_edit(copy_path, errors="strict") except UnicodeDecodeError: raise _fail( gettext( @@ -4694,14 +4713,6 @@ def _normalize_params( % {"name": entry.meta.name}, 1, ) from None - # The comment-block engine below is LF-based (its block regex never matches "# ///\r"), so - # fold every terminator to LF — \r\n AND lone \r (classic-Mac) -> \n, or a CRLF/CR copy - # leaves "# ///\r" unmatched and normalizes nothing. The copy's own style is captured first - # and restored at write-back, so a CRLF script stays CRLF and skit's edit stays confined to - # the one constant it rewrote (write_text, the old path, re-expanded \n to the HOST os.linesep - # instead, CRLF-ifying the whole copy on Windows and skipping the next re-anchor silently). - newline = detect_newline(raw) - text = text.replace("\r\n", "\n").replace("\r", "\n") result = entry_spec.normalizer.normalize(text, list(names)) for warning in result.refused: err_console.print(f"[yellow]{escape(_render_normalize_warning(warning))}[/yellow]") diff --git a/src/skit/langs/base.py b/src/skit/langs/base.py index aa0366e..5c5483d 100644 --- a/src/skit/langs/base.py +++ b/src/skit/langs/base.py @@ -249,6 +249,12 @@ class CliReader: """Static reader for the script's OWN argument parser (argparse tier).""" read_cli: Callable[[str], ArgSpec | None] + # Non-None only when read_cli's answer depends on an external tool (PowerShell's + # parser subprocess): returns the resolved tool identity (path, or None when absent) + # so a caller that MEMOIZES read_cli results can key on it — otherwise a pwsh + # installed mid-session would keep serving the tool-less verdict from the memo. + # Purely-static readers leave it None and memoize on text alone. + runtime_fingerprint: Callable[[], str | None] | None = None @dataclass(frozen=True) diff --git a/src/skit/langs/powershell/cli_reader.py b/src/skit/langs/powershell/cli_reader.py index dd9f9fd..eaca26d 100644 --- a/src/skit/langs/powershell/cli_reader.py +++ b/src/skit/langs/powershell/cli_reader.py @@ -153,6 +153,13 @@ def read_cli(text: str) -> ArgSpec | None: tmp.unlink(missing_ok=True) # pragma: no mutate +def runtime_fingerprint() -> str | None: + """The resolved tool identity for memo keys (see base.CliReader.runtime_fingerprint): + read_cli's verdict is a function of the text AND of which PowerShell answers, so a + cached verdict must be invalidated when the tool appears, disappears, or moves.""" + return _find_powershell() + + def _find_powershell() -> str | None: """`pwsh` anywhere on PATH; on Windows, `powershell.exe` as the fallback (Windows PowerShell exposes the identical Parser API). None otherwise — the reader degrades.""" diff --git a/src/skit/langs/registry.py b/src/skit/langs/registry.py index 332b0e1..040d408 100644 --- a/src/skit/langs/registry.py +++ b/src/skit/langs/registry.py @@ -215,7 +215,14 @@ def _ts_spec() -> LangSpec: def _powershell_caps() -> Capabilities: from .powershell import cli_reader - return Capabilities(cli_reader=CliReader(read_cli=cli_reader.read_cli)) + return Capabilities( + cli_reader=CliReader( + read_cli=cli_reader.read_cli, + # The read shells out to pwsh, so a memoized verdict is stale the moment + # the tool appears/disappears — the fingerprint lets memo keys see that. + runtime_fingerprint=cli_reader.runtime_fingerprint, + ) + ) def _powershell_spec() -> LangSpec: diff --git a/src/skit/locales/skit.pot b/src/skit/locales/skit.pot index 6d21ccf..972b41f 100644 --- a/src/skit/locales/skit.pot +++ b/src/skit/locales/skit.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: skit 0.4.1.dev0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-07-26 01:20-0700\n" +"POT-Creation-Date: 2026-07-26 06:15-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,70 +17,70 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: src/skit/analysis.py:162 +#: src/skit/analysis.py:165 #, python-format msgid "The parameter definitions for %(name)s have drifted from the script:" msgstr "" -#: src/skit/analysis.py:171 +#: src/skit/analysis.py:174 #, python-brace-format, python-format msgid "%(name)s is no longer read from the environment (its ${...:-default} was removed or overridden by a plain assignment) — your value would be silently ignored. Re-add or resync." msgstr "" -#: src/skit/analysis.py:181 +#: src/skit/analysis.py:184 #, python-format msgid "%(name)s: injection target no longer exists (dropped from this run's form)" msgstr "" -#: src/skit/analysis.py:189 +#: src/skit/analysis.py:192 #, python-format msgid "%(name)s: type changed from %(old)s to %(new)s in the source (still injected — double-check the value)" msgstr "" -#: src/skit/analysis.py:197 +#: src/skit/analysis.py:200 #, python-format msgid "%(name)s: its prompt no longer matches a unique input/read call; falling back to position (still injected — double-check this lands on the right question, especially if it's a secret)" msgstr "" -#: src/skit/analysis.py:205 +#: src/skit/analysis.py:208 #, python-format msgid "To refresh the definitions, run: skit params %(name)s --resync" msgstr "" -#: src/skit/analysis.py:219 +#: src/skit/analysis.py:222 #, python-format msgid "%(name)s isn't a managed parameter; skipped." msgstr "" -#: src/skit/analysis.py:220 +#: src/skit/analysis.py:223 #, python-format msgid "Dropped %(name)s: it no longer exists in the script." msgstr "" -#: src/skit/analysis.py:221 +#: src/skit/analysis.py:224 #, python-format msgid "%(name)s is already managed; skipped." msgstr "" -#: src/skit/analysis.py:223 +#: src/skit/analysis.py:226 #, python-format msgid "%(name)s isn't a detectable parameter in the current script; skipped." msgstr "" -#: src/skit/analysis.py:226 +#: src/skit/analysis.py:229 msgid "Could not parse the script (syntax error); resync skipped. Parameter definitions are unchanged." msgstr "" -#: src/skit/analysis.py:230 +#: src/skit/analysis.py:233 #, python-format msgid "%(name)s: re-anchored to its current position after its prompt stopped matching uniquely; double-check the prompt/secret assignment is still correct." msgstr "" -#: src/skit/cli.py:67 +#: src/skit/cli.py:56 msgid "skit — a launcher and parameter manager for scripts, prompts, programs, and commands. Run it without a subcommand to open the main menu." msgstr "" -#: src/skit/cli.py:112 +#: src/skit/cli.py:110 msgid "Cancelled — nothing was added." msgstr "" @@ -92,1855 +92,1860 @@ msgstr "" msgid "Show version" msgstr "" -#: src/skit/cli.py:194 +#: src/skit/cli.py:201 #, python-format msgid "The script declares its own dependencies (PEP 723): %(deps)s" msgstr "" -#: src/skit/cli.py:228 src/skit/cli.py:329 +#: src/skit/cli.py:235 src/skit/cli.py:338 msgid "Dependencies to install (Enter to accept, edit the list, or '-' for none)" msgstr "" -#: src/skit/cli.py:252 +#: src/skit/cli.py:259 msgid "Python version (Enter accepts the #! pin, '-' for automatic)" msgstr "" -#: src/skit/cli.py:254 +#: src/skit/cli.py:261 msgid "Python version (leave empty for automatic)" msgstr "" -#: src/skit/cli.py:294 +#: src/skit/cli.py:303 #, python-format msgid "The #! line pins a python version — recording requires-python %(pin)s (change it with --python)." msgstr "" -#: src/skit/cli.py:359 +#: src/skit/cli.py:368 msgid "Reference-mode entries take no managed dependencies — they run from their own project. Add it as a copy, or drop --dep." msgstr "" -#: src/skit/cli.py:362 +#: src/skit/cli.py:371 #, python-format msgid "%(kind)s entries don't take package dependencies — drop --dep." msgstr "" -#: src/skit/cli.py:369 src/skit/store.py:960 +#: src/skit/cli.py:378 src/skit/store.py:1256 #, python-format msgid "A Python constraint doesn't apply to %(kind)s scripts." msgstr "" -#: src/skit/cli.py:390 src/skit/cli.py:763 src/skit/cli.py:1201 -#: src/skit/cli.py:2000 +#: src/skit/cli.py:399 src/skit/cli.py:781 src/skit/cli.py:1219 +#: src/skit/cli.py:2018 msgid "Name in skit" msgstr "" -#: src/skit/cli.py:396 src/skit/cli.py:1471 src/skit/cli.py:2006 +#: src/skit/cli.py:405 src/skit/cli.py:1489 src/skit/cli.py:2024 #: src/skit/tui_add.py:246 msgid "Description (optional)" msgstr "" -#: src/skit/cli.py:420 src/skit/cli.py:428 src/skit/store.py:224 -#: src/skit/store.py:355 src/skit/store.py:427 src/skit/store.py:639 +#: src/skit/cli.py:429 src/skit/cli.py:437 src/skit/store.py:326 +#: src/skit/store.py:457 src/skit/store.py:529 src/skit/store.py:741 #: src/skit/tui_add.py:357 #, python-format msgid "File not found: %(path)s" msgstr "" -#: src/skit/cli.py:422 +#: src/skit/cli.py:431 #, python-format msgid "Not a file: %(path)s" msgstr "" -#: src/skit/cli.py:457 src/skit/cli.py:1119 src/skit/cli.py:2640 -#: src/skit/tui_add.py:1431 +#: src/skit/cli.py:466 src/skit/cli.py:1137 src/skit/cli.py:2661 +#: src/skit/tui_add.py:1439 msgid " (secret)" msgstr "" -#: src/skit/cli.py:461 +#: src/skit/cli.py:470 #, python-format msgid "%(num)s. %(name)s (%(type)s) = %(value)s%(secret)s" msgstr "" -#: src/skit/cli.py:473 +#: src/skit/cli.py:482 #, python-format msgid "%(num)s. input() #%(ordinal)s: %(prompt)s%(secret)s" msgstr "" -#: src/skit/cli.py:478 +#: src/skit/cli.py:487 msgid "⚠ looks like a loop accumulator — probably not a parameter" msgstr "" -#: src/skit/cli.py:491 src/skit/tui_add.py:1049 +#: src/skit/cli.py:500 src/skit/tui_add.py:1055 msgid "This script reads command-line arguments; the run form has an extra-arguments field for them." msgstr "" -#: src/skit/cli.py:500 +#: src/skit/cli.py:509 #, python-format msgid "💡 %(names)s are written directly inside the code, so skit can't turn them into form fields. To manage one, first give it a name at the top of the script, e.g. OUTPUT = '…' (skit edit %(script)s)." msgstr "" -#: src/skit/cli.py:521 src/skit/tui_add.py:991 +#: src/skit/cli.py:530 src/skit/tui_add.py:997 #, python-format msgid "✓ skit read this script's own arguments (%(count)s field). Running it opens a form — nothing to memorize." msgid_plural "✓ skit read this script's own arguments (%(count)s fields). Running it opens a form — nothing to memorize." msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:531 src/skit/tui_add.py:1006 +#: src/skit/cli.py:540 src/skit/tui_add.py:1012 #, python-format msgid "This script parses its own arguments (%(names)s); skit couldn't model them statically, so the run form offers an extra-arguments field." msgstr "" -#: src/skit/cli.py:557 src/skit/cli.py:615 +#: src/skit/cli.py:569 src/skit/cli.py:631 #, python-format msgid "Found %(count)s parameter candidate (constants / input() calls):" msgid_plural "Found %(count)s parameter candidates (constants / input() calls):" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:566 src/skit/cli.py:624 +#: src/skit/cli.py:578 src/skit/cli.py:640 msgid "Which ones should skit manage? (e.g. 1,3 / all / none)" msgstr "" -#: src/skit/cli.py:585 +#: src/skit/cli.py:599 msgid "Reference mode never touches the original file, so parameter setup was skipped." msgstr "" -#: src/skit/cli.py:707 +#: src/skit/cli.py:725 #, python-format msgid "The draft's #! names no interpreter skit knows — add it with: skit add %(path)s --kind " msgstr "" -#: src/skit/cli.py:720 +#: src/skit/cli.py:738 #, python-format msgid "--dep/--python are python flags, but the draft's shebang names %(kind)s — drop them, or keep the python shebang." msgstr "" -#: src/skit/cli.py:748 +#: src/skit/cli.py:766 msgid "--edit opens your editor, which --no-input forbids — pipe the script in instead: skit add - -n NAME" msgstr "" -#: src/skit/cli.py:756 +#: src/skit/cli.py:774 msgid "Writing a new script in an editor needs an interactive terminal." msgstr "" -#: src/skit/cli.py:765 src/skit/cli.py:1203 src/skit/cli.py:3388 -#: src/skit/cli.py:3432 src/skit/config.py:620 src/skit/store.py:1133 +#: src/skit/cli.py:783 src/skit/cli.py:1221 src/skit/cli.py:3422 +#: src/skit/cli.py:3466 src/skit/config.py:620 src/skit/store.py:1419 #: src/skit/tui_add.py:447 src/skit/tui_runner.py:160 msgid "A name is required." msgstr "" -#: src/skit/cli.py:776 src/skit/cli.py:1212 src/skit/store.py:707 +#: src/skit/cli.py:794 src/skit/cli.py:1230 src/skit/store.py:809 #, python-format msgid "The name %(name)s is already taken — pick another name." msgstr "" -#: src/skit/cli.py:788 src/skit/cli.py:1227 +#: src/skit/cli.py:806 src/skit/cli.py:1245 msgid "Opening your editor…" msgstr "" -#: src/skit/cli.py:796 src/skit/cli.py:1097 src/skit/cli.py:1233 -#: src/skit/cli.py:2140 src/skit/editor.py:105 src/skit/langs/launch.py:712 -#: src/skit/store.py:445 src/skit/tui_add.py:532 src/skit/tui_add.py:749 -#: src/skit/tui_add.py:1089 src/skit/tui_add.py:1281 src/skit/tui_add.py:1559 +#: src/skit/cli.py:814 src/skit/cli.py:1115 src/skit/cli.py:1251 +#: src/skit/cli.py:2158 src/skit/editor.py:105 src/skit/langs/launch.py:720 +#: src/skit/store.py:547 src/skit/tui_add.py:532 src/skit/tui_add.py:749 +#: src/skit/tui_add.py:1095 src/skit/tui_add.py:1289 src/skit/tui_add.py:1567 #: src/skit/tui_settings.py:359 #, python-format msgid "Can't read %(path)s: %(error)s" msgstr "" -#: src/skit/cli.py:801 +#: src/skit/cli.py:819 msgid "Nothing was written, so no script was added." msgstr "" -#: src/skit/cli.py:871 src/skit/cli.py:924 src/skit/cli.py:1273 +#: src/skit/cli.py:889 src/skit/cli.py:942 src/skit/cli.py:1291 msgid "Reading the script from stdin needs an explicit --name." msgstr "" -#: src/skit/cli.py:881 src/skit/cli.py:931 src/skit/cli.py:1293 +#: src/skit/cli.py:899 src/skit/cli.py:949 src/skit/cli.py:1311 msgid "Nothing arrived on stdin, so there is nothing to add." msgstr "" -#: src/skit/cli.py:979 +#: src/skit/cli.py:997 msgid "# New prompt" msgstr "" -#: src/skit/cli.py:995 +#: src/skit/cli.py:1013 msgid "Custom agents: skit runner add mycli -- mycli run {{prompt}} (see skit runner --help)" msgstr "" -#: src/skit/cli.py:1019 src/skit/cli.py:3485 +#: src/skit/cli.py:1037 src/skit/cli.py:3519 #, python-format msgid "Unknown runner: %(runner)s. Configured runners: %(names)s" msgstr "" -#: src/skit/cli.py:1034 +#: src/skit/cli.py:1052 #, python-format msgid "The draft is no longer at %(path)s, so nothing was added." msgstr "" -#: src/skit/cli.py:1038 +#: src/skit/cli.py:1056 #, python-format msgid "Your draft was kept at %(path)s — fix the problem and add it with: skit add %(path)s" msgstr "" -#: src/skit/cli.py:1042 src/skit/tui_add.py:546 src/skit/tui_add.py:572 +#: src/skit/cli.py:1060 src/skit/tui_add.py:546 src/skit/tui_add.py:572 #, python-format msgid "Your draft was kept at %(path)s" msgstr "" -#: src/skit/cli.py:1059 +#: src/skit/cli.py:1077 msgid "- = no pin (the run form asks each time)" msgstr "" -#: src/skit/cli.py:1062 src/skit/cli.py:2881 +#: src/skit/cli.py:1080 src/skit/cli.py:2903 msgid "Run this prompt with which agent?" msgstr "" -#: src/skit/cli.py:1117 src/skit/cli.py:2638 +#: src/skit/cli.py:1135 src/skit/cli.py:2659 msgid "Detected placeholders (each becomes a form field):" msgstr "" -#: src/skit/cli.py:1124 src/skit/cli.py:2645 src/skit/tui_add.py:296 -#: src/skit/tui_add.py:1439 src/skit/tui_settings.py:699 +#: src/skit/cli.py:1142 src/skit/cli.py:2666 src/skit/tui_add.py:296 +#: src/skit/tui_add.py:1447 src/skit/tui_settings.py:699 #, python-format msgid "…and %(count)s more" msgstr "" -#: src/skit/cli.py:1128 +#: src/skit/cli.py:1146 msgid "Manage which? (all / none / off / numbers like 1,3)" msgstr "" -#: src/skit/cli.py:1157 +#: src/skit/cli.py:1175 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written (turn it on with: skit params %(name)s --interpolate)" msgstr "" -#: src/skit/cli.py:1162 +#: src/skit/cli.py:1180 #, python-format msgid "Detected %(count)s placeholders — too many to manage automatically, so none were. Manage the ones you need with: skit params %(name)s --add NAME, or turn insertion off with --no-interpolate." msgstr "" -#: src/skit/cli.py:1166 +#: src/skit/cli.py:1184 #, python-format msgid "Runs with %(runner)s (change it with: skit params %(name)s --runner NAME)" msgstr "" -#: src/skit/cli.py:1194 +#: src/skit/cli.py:1212 msgid "--prompt with no path opens your editor, which --no-input forbids — pipe the body in instead: skit add - --prompt -n NAME" msgstr "" -#: src/skit/cli.py:1238 +#: src/skit/cli.py:1256 msgid "Nothing was written, so no prompt was added." msgstr "" -#: src/skit/cli.py:1344 +#: src/skit/cli.py:1362 #, python-format msgid "Unknown kind: %(kind)s. Choose from: %(kinds)s" msgstr "" -#: src/skit/cli.py:1364 src/skit/tui_add.py:121 +#: src/skit/cli.py:1382 src/skit/tui_add.py:121 #, python-format msgid "The #! in %(file)s names no interpreter skit knows. What is it?" msgstr "" -#: src/skit/cli.py:1366 src/skit/tui_add.py:123 +#: src/skit/cli.py:1384 src/skit/tui_add.py:123 #, python-format msgid "What is %(file)s? skit can't tell from the name." msgstr "" -#: src/skit/cli.py:1371 +#: src/skit/cli.py:1389 msgid "- = cancel" msgstr "" -#: src/skit/cli.py:1373 src/skit/cli.py:1453 +#: src/skit/cli.py:1391 src/skit/cli.py:1471 msgid "Which one?" msgstr "" -#: src/skit/cli.py:1387 +#: src/skit/cli.py:1405 #, python-format msgid "Detected parameters: %(names)s (the run form asks for them; your last values are remembered)" msgstr "" -#: src/skit/cli.py:1447 +#: src/skit/cli.py:1465 msgid "What would you like to add?" msgstr "" -#: src/skit/cli.py:1448 +#: src/skit/cli.py:1466 msgid "A file you already have — a script, program, or prompt" msgstr "" -#: src/skit/cli.py:1449 +#: src/skit/cli.py:1467 msgid "A new script, written in your editor" msgstr "" -#: src/skit/cli.py:1450 +#: src/skit/cli.py:1468 msgid "A new AI-agent prompt, written in your editor" msgstr "" -#: src/skit/cli.py:1451 +#: src/skit/cli.py:1469 #, python-brace-format msgid "A command template (e.g. ffmpeg -i {input})" msgstr "" -#: src/skit/cli.py:1464 +#: src/skit/cli.py:1482 msgid "Command template" msgstr "" -#: src/skit/cli.py:1467 src/skit/tui_add.py:243 +#: src/skit/cli.py:1485 src/skit/tui_add.py:243 msgid "Name for the command" msgstr "" -#: src/skit/cli.py:1477 +#: src/skit/cli.py:1495 msgid "Path to the file" msgstr "" -#: src/skit/cli.py:1484 +#: src/skit/cli.py:1502 msgid "Add a script, executable, prompt, or command to skit." msgstr "" -#: src/skit/cli.py:1486 +#: src/skit/cli.py:1504 #, python-brace-format msgid "Examples: skit add tools/resize.py · skit add --cmd 'ffmpeg -i {input}' -n convert · pbpaste | skit add - -n clip" msgstr "" -#: src/skit/cli.py:1493 +#: src/skit/cli.py:1511 msgid "Path to a script, executable, or prompt, or '-' to read one from stdin (omit it in a terminal and skit asks what to add)" msgstr "" -#: src/skit/cli.py:1498 +#: src/skit/cli.py:1516 msgid "Name / alias (defaults to the file name)" msgstr "" -#: src/skit/cli.py:1504 +#: src/skit/cli.py:1522 msgid "Description (inferred from the source when possible)" msgstr "" -#: src/skit/cli.py:1507 +#: src/skit/cli.py:1525 msgid "Write a brand-new script in your editor, then add it" msgstr "" -#: src/skit/cli.py:1512 +#: src/skit/cli.py:1530 msgid "Reference mode: link to the original file instead of copying it" msgstr "" -#: src/skit/cli.py:1517 +#: src/skit/cli.py:1535 msgid "Force the executable kind (normally inferred from the file itself)" msgstr "" -#: src/skit/cli.py:1522 +#: src/skit/cli.py:1540 msgid "Force the language kind (e.g. shell, js) for an extensionless file" msgstr "" -#: src/skit/cli.py:1525 +#: src/skit/cli.py:1543 #, python-brace-format msgid "Register a command template, e.g. --cmd 'ffmpeg -i {input}'" msgstr "" -#: src/skit/cli.py:1531 +#: src/skit/cli.py:1549 msgid "Add the file as a prompt for an AI agent (with no path: draft one in your editor)" msgstr "" -#: src/skit/cli.py:1537 +#: src/skit/cli.py:1555 msgid "Pin the agent a prompt entry runs with (see skit runner list)" msgstr "" -#: src/skit/cli.py:1543 +#: src/skit/cli.py:1561 msgid "Prompt only: no variable insertion at all — the body travels exactly as written" msgstr "" -#: src/skit/cli.py:1549 +#: src/skit/cli.py:1567 msgid "A dependency (repeat for more; skips the interactive question)" msgstr "" -#: src/skit/cli.py:1552 src/skit/cli.py:4912 +#: src/skit/cli.py:1570 src/skit/cli.py:4951 msgid "Python version constraint, e.g. \">=3.11\"" msgstr "" -#: src/skit/cli.py:1555 +#: src/skit/cli.py:1573 msgid "Never prompt; accept the detected suggestions" msgstr "" -#: src/skit/cli.py:1564 +#: src/skit/cli.py:1582 msgid "--prompt names the kind outright — drop --edit/--exe/--kind/--cmd." msgstr "" -#: src/skit/cli.py:1577 +#: src/skit/cli.py:1595 msgid "stdin ('-')" msgstr "" -#: src/skit/cli.py:1578 +#: src/skit/cli.py:1596 msgid "a file path" msgstr "" -#: src/skit/cli.py:1586 +#: src/skit/cli.py:1604 #, python-format msgid "%(flags)s each pick a different way to add — use exactly one (nothing was added)." msgstr "" -#: src/skit/cli.py:1627 +#: src/skit/cli.py:1645 msgid "a --cmd template takes only --name/--description" msgstr "" -#: src/skit/cli.py:1630 +#: src/skit/cli.py:1648 msgid "stdin authors a brand-new copy, and --ref/--exe need an existing file" msgstr "" -#: src/skit/cli.py:1632 +#: src/skit/cli.py:1650 msgid "--edit drafts a fresh script: its kind comes from the shebang you write (e.g. #!/usr/bin/env bash), --ref/--exe need an existing file, and a prompt is drafted with skit add --prompt" msgstr "" -#: src/skit/cli.py:1637 +#: src/skit/cli.py:1655 msgid "a drafted prompt takes only --name/--description/--runner/--no-interpolate" msgstr "" -#: src/skit/cli.py:1645 +#: src/skit/cli.py:1663 #, python-format msgid "%(flags)s can't apply here — %(hint)s (nothing was added)." msgstr "" -#: src/skit/cli.py:1658 src/skit/cli.py:1976 +#: src/skit/cli.py:1676 src/skit/cli.py:1994 msgid "--no-interpolate only applies to prompt entries — add one with --prompt." msgstr "" -#: src/skit/cli.py:1663 src/skit/cli.py:1971 +#: src/skit/cli.py:1681 src/skit/cli.py:1989 msgid "--runner only applies to prompt entries — add one with --prompt." msgstr "" -#: src/skit/cli.py:1678 +#: src/skit/cli.py:1696 msgid "--exe needs an existing program on disk — stdin and the editor author scripts." msgstr "" -#: src/skit/cli.py:1687 +#: src/skit/cli.py:1705 msgid "--dep/--python are script flags — a prompt has no dependencies." msgstr "" -#: src/skit/cli.py:1696 +#: src/skit/cli.py:1714 msgid "--runner/--no-interpolate only apply to prompt entries — pipe one with --prompt." msgstr "" -#: src/skit/cli.py:1714 +#: src/skit/cli.py:1732 msgid "The piped text's #! names no interpreter skit knows — pass --kind to choose one." msgstr "" -#: src/skit/cli.py:1727 +#: src/skit/cli.py:1745 #, python-format msgid "--dep/--python don't apply to a %(kind)s entry (piped text's kind)." msgstr "" -#: src/skit/cli.py:1747 +#: src/skit/cli.py:1765 msgid "A --cmd entry needs a --name" msgstr "" -#: src/skit/cli.py:1767 +#: src/skit/cli.py:1785 msgid "Provide a source path — or pipe the text in (skit add - -n NAME; add --prompt for an AI-agent prompt), or register a command template with --cmd." msgstr "" -#: src/skit/cli.py:1799 +#: src/skit/cli.py:1817 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …), or pick a lane outright with %(lanes)s (nothing was added)." msgstr "" -#: src/skit/cli.py:1806 +#: src/skit/cli.py:1824 #, python-format msgid "%(flags)s need a source — pass the path in the same command (skit add PATH …) (nothing was added)." msgstr "" -#: src/skit/cli.py:1828 +#: src/skit/cli.py:1846 msgid "Use --kind or --exe, not both." msgstr "" -#: src/skit/cli.py:1856 +#: src/skit/cli.py:1874 #, python-format msgid "%(file)s is a directory — pass --exe to add it as a program that runs directly." msgstr "" -#: src/skit/cli.py:1864 +#: src/skit/cli.py:1882 #, python-format msgid "%(file)s is a directory. Add it as a program that runs directly?" msgstr "" -#: src/skit/cli.py:1898 +#: src/skit/cli.py:1916 #, python-format msgid "%(file)s is one of skit's own kept drafts — a resumed draft is always added as a copy (and consumed on success), which a reference or program entry can't be. Drop %(flags)s." msgstr "" -#: src/skit/cli.py:1905 +#: src/skit/cli.py:1923 #, python-format msgid "%(file)s is one of skit's own kept drafts, and a draft is always added as a script or prompt copy — pass --kind to name its language." msgstr "" -#: src/skit/cli.py:1929 +#: src/skit/cli.py:1947 #, python-format msgid "%(file)s looks like a prompt. Add it as one?" msgstr "" -#: src/skit/cli.py:2022 +#: src/skit/cli.py:2040 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one." msgstr "" -#: src/skit/cli.py:2027 +#: src/skit/cli.py:2045 #, python-format msgid "The #! in %(file)s names no interpreter skit knows — pass --kind to choose one, or --exe to run it directly." msgstr "" -#: src/skit/cli.py:2039 +#: src/skit/cli.py:2057 #, python-format msgid "%(file)s is a kept draft skit can't classify — pass --kind to add it as a script, or --prompt for an AI-agent prompt." msgstr "" -#: src/skit/cli.py:2048 +#: src/skit/cli.py:2066 #, python-format msgid "%(file)s isn't a script or an executable — pass --kind for an extensionless script, --prompt for an AI-agent prompt, --exe for a program, or --cmd for a command template." msgstr "" -#: src/skit/cli.py:2198 +#: src/skit/cli.py:2216 #, python-format msgid "(%(mode)s mode)" msgstr "" -#: src/skit/cli.py:2203 +#: src/skit/cli.py:2221 #, python-format msgid "Added: %(name)s" msgstr "" -#: src/skit/cli.py:2207 +#: src/skit/cli.py:2225 #, python-format msgid "Description: %(desc)s" msgstr "" -#: src/skit/cli.py:2211 src/skit/cli.py:2369 +#: src/skit/cli.py:2229 src/skit/cli.py:2390 #, python-format msgid "Dependencies: %(deps)s" msgstr "" -#: src/skit/cli.py:2215 +#: src/skit/cli.py:2233 #, python-format msgid "Managed parameters: %(names)s" msgstr "" -#: src/skit/cli.py:2217 src/skit/cli.py:2410 +#: src/skit/cli.py:2235 src/skit/cli.py:2431 #, python-format msgid "Run it: skit run %(name)s" msgstr "" -#: src/skit/cli.py:2220 +#: src/skit/cli.py:2238 #, python-format msgid "Secret parameter values are never saved by skit: %(names)s" msgstr "" -#: src/skit/cli.py:2226 +#: src/skit/cli.py:2244 msgid "When this prompt runs, the selected agent receives those values as plaintext and may log or sync them." msgstr "" -#: src/skit/cli.py:2238 +#: src/skit/cli.py:2256 msgid "List every registered entry." msgstr "" -#: src/skit/cli.py:2240 src/skit/cli.py:2451 src/skit/cli.py:3250 -#: src/skit/cli.py:3627 src/skit/cli.py:4924 src/skit/cli.py:5151 -#: src/skit/cli.py:5495 +#: src/skit/cli.py:2258 src/skit/cli.py:2472 src/skit/cli.py:3284 +#: src/skit/cli.py:3662 src/skit/cli.py:4963 src/skit/cli.py:5194 +#: src/skit/cli.py:5539 msgid "Output as JSON" msgstr "" -#: src/skit/cli.py:2263 +#: src/skit/cli.py:2284 msgid "No entries yet. Add one with: skit add " msgstr "" -#: src/skit/cli.py:2268 src/skit/tui.py:377 src/skit/tui.py:1088 -#: src/skit/tui_add.py:639 src/skit/tui_add.py:865 src/skit/tui_add.py:1344 +#: src/skit/cli.py:2289 src/skit/tui.py:384 src/skit/tui.py:1121 +#: src/skit/tui_add.py:639 src/skit/tui_add.py:871 src/skit/tui_add.py:1352 msgid "Name" msgstr "" -#: src/skit/cli.py:2269 src/skit/cli.py:3974 src/skit/tui.py:377 -#: src/skit/tui.py:1088 +#: src/skit/cli.py:2290 src/skit/cli.py:4010 src/skit/tui.py:384 +#: src/skit/tui.py:1121 msgid "Kind" msgstr "" -#: src/skit/cli.py:2270 src/skit/tui_add.py:641 src/skit/tui_add.py:867 -#: src/skit/tui_add.py:1349 +#: src/skit/cli.py:2291 src/skit/tui_add.py:641 src/skit/tui_add.py:873 +#: src/skit/tui_add.py:1357 msgid "Description" msgstr "" -#: src/skit/cli.py:2295 src/skit/cli.py:2296 src/skit/cli.py:2436 -#: src/skit/cli.py:3716 src/skit/cli.py:3717 +#: src/skit/cli.py:2316 src/skit/cli.py:2317 src/skit/cli.py:2457 +#: src/skit/cli.py:3750 src/skit/cli.py:3751 msgid "yes" msgstr "" -#: src/skit/cli.py:2351 +#: src/skit/cli.py:2372 #, python-format msgid "Source: %(path)s" msgstr "" -#: src/skit/cli.py:2354 +#: src/skit/cli.py:2375 #, python-format msgid "Working directory: %(dir)s" msgstr "" -#: src/skit/cli.py:2358 +#: src/skit/cli.py:2379 #, python-format msgid "Interpreter: %(program)s" msgstr "" -#: src/skit/cli.py:2373 src/skit/cli.py:4890 +#: src/skit/cli.py:2394 src/skit/cli.py:4929 #, python-format msgid "Python constraint: %(python)s" msgstr "" -#: src/skit/cli.py:2377 +#: src/skit/cli.py:2398 #, python-format msgid "Needs: %(needs)s" msgstr "" -#: src/skit/cli.py:2381 +#: src/skit/cli.py:2402 #, python-format msgid "Command template: %(template)s" msgstr "" -#: src/skit/cli.py:2385 +#: src/skit/cli.py:2406 #, python-format msgid "Runner: %(runner)s" msgstr "" -#: src/skit/cli.py:2385 +#: src/skit/cli.py:2406 msgid "(asks at run time)" msgstr "" -#: src/skit/cli.py:2388 +#: src/skit/cli.py:2409 msgid "Variable insertion: off (the body travels as written)" msgstr "" -#: src/skit/cli.py:2392 src/skit/cli.py:3061 +#: src/skit/cli.py:2413 src/skit/cli.py:3085 msgid "skit could not model this script's own arguments; pass them after -- instead." msgstr "" -#: src/skit/cli.py:2398 +#: src/skit/cli.py:2419 msgid "No form fields — arguments after -- go to the selected agent." msgstr "" -#: src/skit/cli.py:2400 +#: src/skit/cli.py:2421 msgid "No form fields — arguments after -- are appended to the command." msgstr "" -#: src/skit/cli.py:2403 +#: src/skit/cli.py:2424 msgid "No form fields — arguments after -- pass straight through to the script." msgstr "" -#: src/skit/cli.py:2408 +#: src/skit/cli.py:2429 #, python-format msgid "Presets: %(names)s" msgstr "" -#: src/skit/cli.py:2417 src/skit/cli.py:3755 src/skit/cli.py:3973 +#: src/skit/cli.py:2438 src/skit/cli.py:3789 src/skit/cli.py:4009 msgid "Parameter" msgstr "" -#: src/skit/cli.py:2418 src/skit/cli.py:3757 src/skit/cli.py:3975 +#: src/skit/cli.py:2439 src/skit/cli.py:3791 src/skit/cli.py:4011 msgid "Type" msgstr "" -#: src/skit/cli.py:2419 +#: src/skit/cli.py:2440 msgid "Required" msgstr "" -#: src/skit/cli.py:2420 src/skit/cli.py:3758 src/skit/cli.py:3976 +#: src/skit/cli.py:2441 src/skit/cli.py:3792 src/skit/cli.py:4012 msgid "Default" msgstr "" -#: src/skit/cli.py:2421 +#: src/skit/cli.py:2442 msgid "Choices" msgstr "" -#: src/skit/cli.py:2422 src/skit/cli.py:3759 src/skit/cli.py:3977 +#: src/skit/cli.py:2443 src/skit/cli.py:3793 src/skit/cli.py:4013 msgid "Secret" msgstr "" -#: src/skit/cli.py:2423 src/skit/tui.py:343 src/skit/tui.py:493 +#: src/skit/cli.py:2444 src/skit/tui.py:343 src/skit/tui.py:500 msgid "Help" msgstr "" -#: src/skit/cli.py:2428 src/skit/cli.py:3723 src/skit/cli.py:3731 -#: src/skit/cli.py:3741 src/skit/cli.py:3981 src/skit/cli.py:3986 +#: src/skit/cli.py:2449 src/skit/cli.py:3757 src/skit/cli.py:3765 +#: src/skit/cli.py:3775 src/skit/cli.py:4017 src/skit/cli.py:4022 msgid "•••" msgstr "" -#: src/skit/cli.py:2446 +#: src/skit/cli.py:2467 msgid "Show everything about one entry: metadata, dependencies, parameters, presets." msgstr "" -#: src/skit/cli.py:2447 +#: src/skit/cli.py:2468 msgid "Examples: skit show resize · skit show resize --json" msgstr "" -#: src/skit/cli.py:2508 +#: src/skit/cli.py:2531 msgid "Remove a registered entry (an original source file is left untouched)." msgstr "" -#: src/skit/cli.py:2511 src/skit/cli.py:3424 src/skit/cli.py:3655 +#: src/skit/cli.py:2534 src/skit/cli.py:3458 src/skit/cli.py:3690 msgid "Skip confirmation" msgstr "" -#: src/skit/cli.py:2512 src/skit/cli.py:3425 src/skit/cli.py:3656 +#: src/skit/cli.py:2535 src/skit/cli.py:3459 src/skit/cli.py:3691 msgid "Never prompt" msgstr "" -#: src/skit/cli.py:2523 +#: src/skit/cli.py:2543 msgid "Confirmation is required; pass --yes to remove the entry." msgstr "" -#: src/skit/cli.py:2527 src/skit/tui.py:149 +#: src/skit/cli.py:2548 src/skit/tui.py:149 #, python-format msgid "Remove \"%(name)s\"?" msgstr "" -#: src/skit/cli.py:2529 +#: src/skit/cli.py:2550 #, python-format msgid "Remove \"%(name)s\"? Your original file will not be deleted." msgstr "" -#: src/skit/cli.py:2540 +#: src/skit/cli.py:2561 #, python-format msgid "Removed: %(name)s" msgstr "" -#: src/skit/cli.py:2543 +#: src/skit/cli.py:2564 msgid "Rename an entry (presets, remembered values and history survive)." msgstr "" -#: src/skit/cli.py:2546 +#: src/skit/cli.py:2567 msgid "The new name" msgstr "" -#: src/skit/cli.py:2555 +#: src/skit/cli.py:2576 #, python-format msgid "Renamed to %(name)s." msgstr "" -#: src/skit/cli.py:2559 +#: src/skit/cli.py:2580 msgid "Set an entry's description (shown in the Library and skit list)." msgstr "" -#: src/skit/cli.py:2562 +#: src/skit/cli.py:2583 msgid "The description (empty text clears it)" msgstr "" -#: src/skit/cli.py:2572 +#: src/skit/cli.py:2593 #, python-format msgid "Description updated for %(name)s." msgstr "" -#: src/skit/cli.py:2576 +#: src/skit/cli.py:2597 #, python-format msgid "Description cleared for %(name)s." msgstr "" -#: src/skit/cli.py:2584 +#: src/skit/cli.py:2605 #, python-format msgid "No editable entry named %(name)s." msgstr "" -#: src/skit/cli.py:2588 +#: src/skit/cli.py:2609 #, python-format msgid "No editable entry named \"%(name)s\". Create a script now?" msgstr "" -#: src/skit/cli.py:2616 src/skit/cli.py:3839 +#: src/skit/cli.py:2637 src/skit/cli.py:3873 #, python-format msgid "Detected but not yet managed: %(names)s … and %(count)d more candidate (use --add to manage them)" msgid_plural "Detected but not yet managed: %(names)s … and %(count)d more candidates (use --add to manage them)" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:2624 src/skit/cli.py:3847 +#: src/skit/cli.py:2645 src/skit/cli.py:3881 #, python-format msgid "Detected but not yet managed: %(names)s (use --add to manage them)" msgstr "" -#: src/skit/cli.py:2649 +#: src/skit/cli.py:2670 msgid "Manage which? (all / none / numbers like 1,3)" msgstr "" -#: src/skit/cli.py:2665 src/skit/tui.py:959 +#: src/skit/cli.py:2686 src/skit/tui.py:992 #, python-format msgid "Now managed: %(names)s" msgstr "" -#: src/skit/cli.py:2671 +#: src/skit/cli.py:2692 msgid "Open a script or prompt source in your editor (offers to create a script if the name is new)." msgstr "" -#: src/skit/cli.py:2673 +#: src/skit/cli.py:2694 msgid "Example: skit edit resize" msgstr "" -#: src/skit/cli.py:2685 src/skit/tui.py:913 +#: src/skit/cli.py:2706 src/skit/tui.py:946 #, python-format msgid "%(name)s has no editable source (programs and command templates run as-is)." msgstr "" -#: src/skit/cli.py:2693 +#: src/skit/cli.py:2714 #, python-format msgid "%(name)s: the referenced source file is gone: %(path)s" msgstr "" -#: src/skit/cli.py:2698 +#: src/skit/cli.py:2719 #, python-format msgid "Editing the original file (reference mode): %(path)s" msgstr "" -#: src/skit/cli.py:2705 src/skit/cli.py:4520 src/skit/cli.py:4652 +#: src/skit/cli.py:2726 src/skit/cli.py:4568 src/skit/cli.py:4700 #, python-format msgid "%(name)s has no stored copy to edit." msgstr "" -#: src/skit/cli.py:2712 +#: src/skit/cli.py:2733 #, python-format msgid "Saved %(name)s." msgstr "" -#: src/skit/cli.py:2720 +#: src/skit/cli.py:2741 #, python-format msgid "skit reconciles parameter drift at run time; review managed parameters with: skit params %(name)s" msgstr "" -#: src/skit/cli.py:2736 src/skit/cli.py:3693 +#: src/skit/cli.py:2757 src/skit/cli.py:3727 #, python-format msgid "Unknown preset \"%(preset)s\". Available: %(presets)s" msgstr "" -#: src/skit/cli.py:2766 +#: src/skit/cli.py:2789 #, python-format msgid "Malformed --set (expected NAME=VALUE): %(items)s" msgstr "" -#: src/skit/cli.py:2776 +#: src/skit/cli.py:2799 #, python-format msgid "Unknown parameter for --set: %(names)s. This entry's parameters: %(valid)s" msgstr "" -#: src/skit/cli.py:2827 +#: src/skit/cli.py:2852 #, python-format msgid "Parameters for %(name)s (press Enter to keep the value shown):" msgstr "" -#: src/skit/cli.py:2870 src/skit/cli.py:4410 +#: src/skit/cli.py:2892 src/skit/cli.py:4446 msgid "--runner only applies to prompt entries." msgstr "" -#: src/skit/cli.py:2890 src/skit/cli.py:3283 src/skit/cli.py:3315 -#: src/skit/langs/launch.py:663 +#: src/skit/cli.py:2912 src/skit/cli.py:3317 src/skit/cli.py:3349 +#: src/skit/langs/launch.py:671 msgid "No agents are configured. Add one with: skit runner add mycli -- mycli run {{prompt}}" msgstr "" -#: src/skit/cli.py:2898 src/skit/langs/launch.py:669 +#: src/skit/cli.py:2920 src/skit/langs/launch.py:677 #, python-format msgid "No runner selected for %(name)s. Pass --runner NAME, or pin one with: skit params %(name)s --runner NAME" msgstr "" -#: src/skit/cli.py:2908 src/skit/cli.py:3100 src/skit/cli.py:4417 -#: src/skit/langs/launch.py:678 +#: src/skit/cli.py:2930 src/skit/cli.py:3124 src/skit/cli.py:4453 +#: src/skit/langs/launch.py:686 #, python-format msgid "The runner %(runner)s isn't configured (known: %(names)s). Manage runners with: skit runner list" msgstr "" -#: src/skit/cli.py:2920 +#: src/skit/cli.py:2942 msgid "Run a registered entry in the terminal." msgstr "" -#: src/skit/cli.py:2922 +#: src/skit/cli.py:2944 msgid "Examples: skit run stitch · skit run stitch -p web -- extra.png · skit run stitch --set width=800 --no-input · skit run stitch --dry-run" msgstr "" -#: src/skit/cli.py:2927 +#: src/skit/cli.py:2949 msgid "Arguments passed through after --" msgstr "" -#: src/skit/cli.py:2929 +#: src/skit/cli.py:2951 msgid "Never prompt; reuse last values and defaults" msgstr "" -#: src/skit/cli.py:2935 +#: src/skit/cli.py:2957 msgid "Named preset of parameter values to prefill the form with" msgstr "" -#: src/skit/cli.py:2942 +#: src/skit/cli.py:2964 #, python-brace-format msgid "Set a parameter value by name, as NAME=VALUE (repeatable; values may use tokens like {cwd} or {env:VAR}; the form no longer asks for a field you set)" msgstr "" -#: src/skit/cli.py:2946 +#: src/skit/cli.py:2968 msgid "Save this run's values as a named preset" msgstr "" -#: src/skit/cli.py:2949 +#: src/skit/cli.py:2971 msgid "Line-by-line prompts instead of the inline form" msgstr "" -#: src/skit/cli.py:2955 +#: src/skit/cli.py:2977 msgid "Skip the parameter form and injection and run the script as-is (escape hatch)" msgstr "" -#: src/skit/cli.py:2962 +#: src/skit/cli.py:2984 msgid "Print the exact command that would run (tokens and globs expanded), then exit" msgstr "" -#: src/skit/cli.py:2968 +#: src/skit/cli.py:2990 msgid "Run a prompt entry with this agent (overrides its pin for one run)" msgstr "" -#: src/skit/cli.py:2975 +#: src/skit/cli.py:2997 msgid "Forget the remembered extra arguments before this run (they are otherwise reused when you pass none)" msgstr "" -#: src/skit/cli.py:2992 +#: src/skit/cli.py:3016 msgid "--raw runs the script as-is; --set, --preset, and --save-preset do not apply." msgstr "" -#: src/skit/cli.py:3007 +#: src/skit/cli.py:3031 msgid "--raw doesn't apply to a prompt entry: its {{placeholders}} are the artifact itself — there is no as-is without them." msgstr "" -#: src/skit/cli.py:3012 +#: src/skit/cli.py:3036 #, python-brace-format, python-format msgid "--raw doesn't apply to a %(kind)s entry: its {placeholders} are the artifact itself — there is no as-is without them." msgstr "" -#: src/skit/cli.py:3039 +#: src/skit/cli.py:3063 msgid "Raw mode: skipping the parameter form and injection." msgstr "" -#: src/skit/cli.py:3053 src/skit/cli.py:3565 src/skit/tui_form.py:935 +#: src/skit/cli.py:3077 src/skit/cli.py:3600 src/skit/tui_form.py:935 #, python-format msgid "%(name)s has no form fields, so there's nothing to save." msgstr "" -#: src/skit/cli.py:3140 +#: src/skit/cli.py:3164 #, python-format msgid "Reusing your last arguments: %(args)s" msgstr "" -#: src/skit/cli.py:3167 src/skit/cli.py:3620 +#: src/skit/cli.py:3171 +#, python-brace-format +msgid "(passed as-is — a remembered tail only expands {tokens} and globs when it was typed into the launch menu)" +msgstr "" + +#: src/skit/cli.py:3198 src/skit/cli.py:3655 #, python-format msgid "Preset \"%(preset)s\" saved for %(name)s." msgstr "" -#: src/skit/cli.py:3230 +#: src/skit/cli.py:3264 #, python-format msgid "Run exited with code %(code)s" msgstr "" -#: src/skit/cli.py:3240 +#: src/skit/cli.py:3274 msgid "Manage the agents (runners) that prompt entries run with." msgstr "" -#: src/skit/cli.py:3247 +#: src/skit/cli.py:3281 msgid "List the configured runners (seeds them into config on first use)." msgstr "" -#: src/skit/cli.py:3254 +#: src/skit/cli.py:3288 msgid "Include malformed raw rows and their repair indexes" msgstr "" -#: src/skit/cli.py:3291 +#: src/skit/cli.py:3325 msgid "Row" msgstr "" -#: src/skit/cli.py:3292 src/skit/cli.py:3323 +#: src/skit/cli.py:3326 src/skit/cli.py:3357 msgid "Runner" msgstr "" -#: src/skit/cli.py:3293 src/skit/cli.py:3324 src/skit/kindnames.py:29 +#: src/skit/cli.py:3327 src/skit/cli.py:3358 src/skit/kindnames.py:29 msgid "Command" msgstr "" -#: src/skit/cli.py:3294 +#: src/skit/cli.py:3328 msgid "Status" msgstr "" -#: src/skit/cli.py:3297 +#: src/skit/cli.py:3331 msgid "container" msgstr "" -#: src/skit/cli.py:3331 +#: src/skit/cli.py:3365 msgid "The built-in amp preset uses amp -x and runs the prompt once; it does not open an interactive session." msgstr "" -#: src/skit/cli.py:3348 +#: src/skit/cli.py:3382 msgid "A runner needs a command — e.g. skit runner add mycli mycli run {{prompt}}" msgstr "" -#: src/skit/cli.py:3351 +#: src/skit/cli.py:3385 msgid "A runner command must contain the {{prompt}} slot exactly once — that's where the rendered prompt lands." msgstr "" -#: src/skit/cli.py:3355 src/skit/config.py:614 src/skit/tui_runner.py:42 +#: src/skit/cli.py:3389 src/skit/config.py:614 src/skit/tui_runner.py:42 msgid "{{prompt}} can't be the command itself — the first word must be the program to run." msgstr "" -#: src/skit/cli.py:3358 src/skit/config.py:617 src/skit/tui_runner.py:45 +#: src/skit/cli.py:3392 src/skit/config.py:617 src/skit/tui_runner.py:45 msgid "Runner commands take only the {{prompt}} slot — single-brace text is literal, and other {{holes}} aren't supported." msgstr "" -#: src/skit/cli.py:3367 +#: src/skit/cli.py:3401 msgid "Register a runner: skit runner add NAME COMMAND… ({{prompt}} marks where the rendered prompt goes; each shell word becomes one argument, no shell involved)" msgstr "" -#: src/skit/cli.py:3376 +#: src/skit/cli.py:3410 msgid "Runner name (e.g. claude)" msgstr "" -#: src/skit/cli.py:3378 +#: src/skit/cli.py:3412 msgid "The command, word by word, with one {{prompt}} slot" msgstr "" -#: src/skit/cli.py:3383 +#: src/skit/cli.py:3417 msgid "Replace the runner if the name already exists (the edit path)" msgstr "" -#: src/skit/cli.py:3398 +#: src/skit/cli.py:3432 #, python-format msgid "The runner %(name)s already exists — pass --force to replace its command." msgstr "" -#: src/skit/cli.py:3406 +#: src/skit/cli.py:3440 #, python-format msgid "Runner %(name)s updated: %(command)s" msgstr "" -#: src/skit/cli.py:3410 +#: src/skit/cli.py:3444 #, python-format msgid "Runner %(name)s added: %(command)s" msgstr "" -#: src/skit/cli.py:3414 +#: src/skit/cli.py:3448 msgid "Remove a configured runner." msgstr "" -#: src/skit/cli.py:3417 +#: src/skit/cli.py:3451 msgid "Runner name" msgstr "" -#: src/skit/cli.py:3422 +#: src/skit/cli.py:3456 msgid "Remove one raw row index from 'runner list --all' (or 'container')" msgstr "" -#: src/skit/cli.py:3428 +#: src/skit/cli.py:3462 msgid "Pass exactly one runner name or --row INDEX." msgstr "" -#: src/skit/cli.py:3448 src/skit/cli.py:3452 +#: src/skit/cli.py:3482 src/skit/cli.py:3486 msgid "--row must be a non-negative index or 'container'." msgstr "" -#: src/skit/cli.py:3457 +#: src/skit/cli.py:3491 #, python-format msgid "Unknown runner row: %(row)s. Inspect with: skit runner list --all" msgstr "" -#: src/skit/cli.py:3465 +#: src/skit/cli.py:3499 #, python-format msgid "Runner row %(row)s is valid. Remove the agent by name instead: skit runner remove \"%(name)s\"" msgstr "" -#: src/skit/cli.py:3474 src/skit/tui_runner.py:315 +#: src/skit/cli.py:3508 src/skit/tui_runner.py:315 msgid "Remove the malformed prompt runner container?" msgstr "" -#: src/skit/cli.py:3476 +#: src/skit/cli.py:3510 #, python-format msgid "Remove runner row %(row)d (\"%(name)s\")?" msgstr "" -#: src/skit/cli.py:3489 src/skit/tui_runner.py:321 +#: src/skit/cli.py:3523 src/skit/tui_runner.py:321 #, python-format msgid "Remove the agent \"%(name)s\"?" msgstr "" -#: src/skit/cli.py:3499 src/skit/tui_runner.py:326 +#: src/skit/cli.py:3533 src/skit/tui_runner.py:326 #, python-format msgid "%(count)d prompt pins this runner and will need another runner before it can run again." msgid_plural "%(count)d prompts pin this runner and will need another runner before they can run again." msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:3508 +#: src/skit/cli.py:3541 msgid "Confirmation is required; pass --yes to remove the runner." msgstr "" -#: src/skit/cli.py:3523 +#: src/skit/cli.py:3556 msgid "The runner row changed before it could be removed; inspect again." msgstr "" -#: src/skit/cli.py:3526 +#: src/skit/cli.py:3559 #, python-format msgid "Runner %(name)s removed." msgstr "" -#: src/skit/cli.py:3529 +#: src/skit/cli.py:3562 msgid "Malformed prompt runner container removed." msgstr "" -#: src/skit/cli.py:3532 +#: src/skit/cli.py:3565 #, python-format msgid "Malformed runner row %(row)d removed." msgstr "" -#: src/skit/cli.py:3541 +#: src/skit/cli.py:3574 msgid "Manage named parameter presets for an entry." msgstr "" -#: src/skit/cli.py:3546 +#: src/skit/cli.py:3579 msgid "Save a set of parameter values as a named preset." msgstr "" -#: src/skit/cli.py:3549 src/skit/cli.py:3654 src/skit/tui_form.py:343 +#: src/skit/cli.py:3582 src/skit/cli.py:3689 src/skit/tui_form.py:343 msgid "Preset name" msgstr "" -#: src/skit/cli.py:3553 +#: src/skit/cli.py:3586 msgid "Save the last run's values without asking (automation-friendly)" msgstr "" -#: src/skit/cli.py:3573 src/skit/cli.py:3589 +#: src/skit/cli.py:3608 src/skit/cli.py:3624 #, python-format msgid "%(name)s has no remembered values yet — run it once first." msgstr "" -#: src/skit/cli.py:3607 +#: src/skit/cli.py:3642 #, python-format msgid "Secret values are never stored in presets; skipped: %(names)s" msgstr "" -#: src/skit/cli.py:3624 +#: src/skit/cli.py:3659 msgid "List an entry's saved presets." msgstr "" -#: src/skit/cli.py:3641 +#: src/skit/cli.py:3676 #, python-format msgid "No presets for %(name)s yet. Create one with: skit run %(name)s --save-preset " msgstr "" -#: src/skit/cli.py:3651 +#: src/skit/cli.py:3686 msgid "Delete a named preset from an entry." msgstr "" -#: src/skit/cli.py:3673 +#: src/skit/cli.py:3707 msgid "Confirmation is required; pass --yes to delete the preset." msgstr "" -#: src/skit/cli.py:3677 +#: src/skit/cli.py:3711 #, python-format msgid "Delete preset \"%(preset)s\" from %(name)s?" msgstr "" -#: src/skit/cli.py:3683 +#: src/skit/cli.py:3717 #, python-format msgid "Preset \"%(preset)s\" deleted from %(name)s." msgstr "" -#: src/skit/cli.py:3742 +#: src/skit/cli.py:3776 #, python-format msgid "default %(value)s" msgstr "" -#: src/skit/cli.py:3744 +#: src/skit/cli.py:3778 msgid "optional" msgstr "" -#: src/skit/cli.py:3746 +#: src/skit/cli.py:3780 msgid "secret" msgstr "" -#: src/skit/cli.py:3756 +#: src/skit/cli.py:3790 msgid "Delivery" msgstr "" -#: src/skit/cli.py:3760 src/skit/cli.py:3978 +#: src/skit/cli.py:3794 src/skit/cli.py:4014 msgid "Last value" msgstr "" -#: src/skit/cli.py:3804 +#: src/skit/cli.py:3838 #, python-format msgid "Variable insertion is off — the body travels to the agent exactly as written. Turn it on with: skit params %(name)s --interpolate" msgstr "" -#: src/skit/cli.py:3815 src/skit/cli.py:3949 src/skit/cli.py:3958 +#: src/skit/cli.py:3849 src/skit/cli.py:3985 src/skit/cli.py:3994 #, python-format msgid "%(name)s has no managed parameters." msgstr "" -#: src/skit/cli.py:3820 +#: src/skit/cli.py:3854 msgid "Prompt placeholders (the run form asks for them):" msgstr "" -#: src/skit/cli.py:3822 +#: src/skit/cli.py:3856 msgid "Command template placeholders (the run form asks for them):" msgstr "" -#: src/skit/cli.py:3829 +#: src/skit/cli.py:3863 msgid "Declared environment variables (set on the run):" msgstr "" -#: src/skit/cli.py:3852 +#: src/skit/cli.py:3886 #, python-format msgid "No longer in the prompt (the value would be ignored): %(names)s — remove with --rm, or edit the body." msgstr "" -#: src/skit/cli.py:3964 +#: src/skit/cli.py:4000 #, python-format msgid "%(name)s has no managed parameters. Use --manage to bring a detected candidate under management." msgstr "" -#: src/skit/cli.py:4002 +#: src/skit/cli.py:4038 #, python-format msgid "Detected but not yet managed: %(names)s" msgstr "" -#: src/skit/cli.py:4009 +#: src/skit/cli.py:4045 msgid "Reference mode: skit never writes the original file — manage parameters by editing its [tool.skit] block in the source directly." msgstr "" -#: src/skit/cli.py:4017 +#: src/skit/cli.py:4053 #, python-format msgid "Detected but not yet managed: %(names)s (use --manage to manage them)" msgstr "" -#: src/skit/cli.py:4022 +#: src/skit/cli.py:4058 #, python-brace-format, python-format msgid "This script locates itself ($0 / BASH_SOURCE). Injecting a constant runs it from a temporary copy, so it would see that copy path instead. Rewriting the constant as NAME=\"${NAME:-value}\" delivers the value through the environment with no copy at all — `skit params %(name)s --normalize NAME` does the rewrite for you on the stored copy." msgstr "" -#: src/skit/cli.py:4041 +#: src/skit/cli.py:4077 msgid "Show or edit an entry's managed or declared parameters." msgstr "" -#: src/skit/cli.py:4043 +#: src/skit/cli.py:4079 msgid "Examples: skit params resize --manage WIDTH · skit params conv --add size --type size=int --deliver size=flag --flag size=--size" msgstr "" -#: src/skit/cli.py:4051 +#: src/skit/cli.py:4087 msgid "Prune definitions that no longer match the script and refresh changed types" msgstr "" -#: src/skit/cli.py:4056 +#: src/skit/cli.py:4092 msgid "Bring a currently detected candidate under management (repeatable)" msgstr "" -#: src/skit/cli.py:4059 +#: src/skit/cli.py:4095 msgid "Drop a managed parameter (repeatable)" msgstr "" -#: src/skit/cli.py:4064 +#: src/skit/cli.py:4100 msgid "Declare a new parameter on an exe/command entry, by name (repeatable)" msgstr "" -#: src/skit/cli.py:4067 +#: src/skit/cli.py:4103 msgid "Remove a declared parameter, by name (repeatable)" msgstr "" -#: src/skit/cli.py:4072 +#: src/skit/cli.py:4108 msgid "Set a declared parameter's type, as NAME=str|int|float|bool|choice|path" msgstr "" -#: src/skit/cli.py:4075 +#: src/skit/cli.py:4111 msgid "Set a declared parameter's default, as NAME=VALUE" msgstr "" -#: src/skit/cli.py:4080 +#: src/skit/cli.py:4116 msgid "Set a declared parameter's choices, as NAME=a,b,c (comma separated)" msgstr "" -#: src/skit/cli.py:4086 +#: src/skit/cli.py:4122 msgid "Set how a declared parameter reaches the program, as NAME=env|flag|placeholder" msgstr "" -#: src/skit/cli.py:4092 +#: src/skit/cli.py:4128 msgid "Set a declared flag parameter's option, as NAME=--out (empty = positional)" msgstr "" -#: src/skit/cli.py:4095 +#: src/skit/cli.py:4131 msgid "Mark a declared parameter as required (repeatable)" msgstr "" -#: src/skit/cli.py:4098 +#: src/skit/cli.py:4134 msgid "Mark a declared parameter as optional (repeatable)" msgstr "" -#: src/skit/cli.py:4101 +#: src/skit/cli.py:4137 msgid "Set a declared parameter's help text, as NAME=text" msgstr "" -#: src/skit/cli.py:4104 +#: src/skit/cli.py:4140 msgid "Mark a managed parameter as secret (repeatable)" msgstr "" -#: src/skit/cli.py:4109 +#: src/skit/cli.py:4145 msgid "Remove the secret mark from a managed parameter (repeatable)" msgstr "" -#: src/skit/cli.py:4112 +#: src/skit/cli.py:4148 msgid "Set a parameter's form prompt, as NAME=text (repeatable)" msgstr "" -#: src/skit/cli.py:4118 +#: src/skit/cli.py:4154 msgid "Read a secret parameter from an environment variable at run time, as NAME=ENVVAR (empty ENVVAR clears it; repeatable)" msgstr "" -#: src/skit/cli.py:4125 +#: src/skit/cli.py:4161 #, python-brace-format msgid "Shell only: rewrite a constant into the ${NAME:-default} idiom in the stored copy, so its value is delivered as an environment variable instead of a rewritten temporary copy (repeatable)" msgstr "" -#: src/skit/cli.py:4132 +#: src/skit/cli.py:4168 msgid "Prompt only: pin the agent this prompt runs with (empty value clears the pin)" msgstr "" -#: src/skit/cli.py:4140 +#: src/skit/cli.py:4176 msgid "Prompt only: turn variable insertion on/off for this prompt (off = the body travels exactly as written)" msgstr "" -#: src/skit/cli.py:4148 +#: src/skit/cli.py:4184 msgid "Set where the entry runs: origin (its own folder), store, invoke (where you run skit from), or an absolute path" msgstr "" -#: src/skit/cli.py:4155 +#: src/skit/cli.py:4191 #, python-brace-format msgid "Command only: rewrite the template ({placeholders} are re-read from it)" msgstr "" -#: src/skit/cli.py:4161 +#: src/skit/cli.py:4197 msgid "Pin the interpreter/runtime an interpreted entry runs with (e.g. zsh, bun; empty value returns to automatic)" msgstr "" -#: src/skit/cli.py:4165 +#: src/skit/cli.py:4201 msgid "Output the read view as JSON" msgstr "" -#: src/skit/cli.py:4227 +#: src/skit/cli.py:4263 #, python-format msgid "%(op)s is its own operation — run it in a separate skit params call (nothing was changed)." msgstr "" -#: src/skit/cli.py:4297 +#: src/skit/cli.py:4333 #, python-format msgid "%(name)s manages its parameters from the script itself — use --manage / --unmanage, or edit the [tool.skit] block." msgstr "" -#: src/skit/cli.py:4385 +#: src/skit/cli.py:4421 #, python-format msgid "Template updated. Placeholders: %(names)s" msgstr "" -#: src/skit/cli.py:4389 +#: src/skit/cli.py:4425 #, python-format msgid "%(name)s now runs in: %(dir)s" msgstr "" -#: src/skit/cli.py:4394 +#: src/skit/cli.py:4430 #, python-format msgid "%(name)s now runs with: %(program)s" msgstr "" -#: src/skit/cli.py:4398 +#: src/skit/cli.py:4434 #, python-format msgid "%(name)s is back to automatic interpreter detection." msgstr "" -#: src/skit/cli.py:4429 +#: src/skit/cli.py:4465 #, python-format msgid "%(name)s now runs with %(runner)s." msgstr "" -#: src/skit/cli.py:4433 +#: src/skit/cli.py:4469 #, python-format msgid "Cleared the runner pin — %(name)s asks at run time." msgstr "" -#: src/skit/cli.py:4442 +#: src/skit/cli.py:4478 msgid "--interpolate only applies to prompt entries." msgstr "" -#: src/skit/cli.py:4449 +#: src/skit/cli.py:4485 #, python-format msgid "Variable insertion is on — %(name)s fills its managed placeholders again." msgstr "" -#: src/skit/cli.py:4453 +#: src/skit/cli.py:4489 #, python-format msgid "Variable insertion is off — %(name)s travels to the agent exactly as written." msgstr "" -#: src/skit/cli.py:4465 +#: src/skit/cli.py:4501 #, python-format msgid "%(name)s isn't a managed parameter; --env-source skipped." msgstr "" -#: src/skit/cli.py:4472 src/skit/cli.py:4723 +#: src/skit/cli.py:4508 src/skit/cli.py:4762 #, python-format msgid "%(name)s isn't secret; --env-source only applies to secret parameters (mark it with --secret first)." msgstr "" -#: src/skit/cli.py:4499 +#: src/skit/cli.py:4544 #, python-format -msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from." +msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from. Declare one instead: skit params %(name)s --add PARAM" msgstr "" -#: src/skit/cli.py:4506 +#: src/skit/cli.py:4552 #, python-format -msgid "Declare one instead: skit params %(name)s --add PARAM" +msgid "%(name)s has no managed parameters — its kind has no analyzer to read them from." msgstr "" -#: src/skit/cli.py:4512 +#: src/skit/cli.py:4560 #, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Edit the [tool.skit] block in the source directly." msgstr "" -#: src/skit/cli.py:4533 +#: src/skit/cli.py:4581 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=text)." msgstr "" -#: src/skit/cli.py:4562 src/skit/cli.py:4848 +#: src/skit/cli.py:4610 src/skit/cli.py:4887 #, python-format msgid "Removed previously stored plaintext value(s) for now-secret parameter(s): %(names)s" msgstr "" -#: src/skit/cli.py:4569 +#: src/skit/cli.py:4617 #, python-format msgid "Updated %(name)s. Managed parameters: %(names)s" msgstr "" -#: src/skit/cli.py:4575 +#: src/skit/cli.py:4623 #, python-format msgid "The run form now asks for the managed parameters — the script's own command-line form (%(frameworks)s) is set aside until they are removed (--unmanage)." msgstr "" -#: src/skit/cli.py:4591 +#: src/skit/cli.py:4639 #, python-format msgid "%(name)s isn't a plain constant with a literal value, so there's nothing to normalize; skipped." msgstr "" -#: src/skit/cli.py:4594 +#: src/skit/cli.py:4642 #, python-format msgid "%(name)s is assigned more than once at the top level; normalizing it would change which value wins. Skipped." msgstr "" -#: src/skit/cli.py:4597 +#: src/skit/cli.py:4645 #, python-format msgid "%(name)s is readonly, so the script could never take a value from the environment; skipped." msgstr "" -#: src/skit/cli.py:4599 +#: src/skit/cli.py:4647 #, python-format msgid "%(name)s already reads from the environment; nothing to do." msgstr "" -#: src/skit/cli.py:4601 +#: src/skit/cli.py:4649 #, python-format msgid "%(name)s's value contains a character that can't be moved into ${...:-...} safely (one of } \" ` $ \\ or a newline); skipped — it keeps being injected into a temporary copy." msgstr "" -#: src/skit/cli.py:4605 +#: src/skit/cli.py:4653 msgid "Could not parse the script (syntax error); nothing was normalized." msgstr "" -#: src/skit/cli.py:4636 +#: src/skit/cli.py:4684 #, python-brace-format, python-format msgid "%(name)s has no --normalize: it is a shell idiom (VAR=value -> VAR=\"${VAR:-value}\")." msgstr "" -#: src/skit/cli.py:4644 +#: src/skit/cli.py:4692 #, python-brace-format, python-format msgid "%(name)s is in reference mode, and skit never writes the original file. Change the line to VAR=\"${VAR:-value}\" in the source directly." msgstr "" -#: src/skit/cli.py:4663 +#: src/skit/cli.py:4710 #, python-format msgid "%(name)s isn't valid UTF-8, so --normalize can't rewrite it safely; nothing was changed — its constants keep being injected into a temporary copy." msgstr "" -#: src/skit/cli.py:4702 +#: src/skit/cli.py:4741 #, python-format msgid "Normalized %(names)s in %(name)s: delivered as environment variables from now on (no temporary copy, and $0 stays your real file)." msgstr "" -#: src/skit/cli.py:4712 +#: src/skit/cli.py:4751 #, python-format msgid "%(name)s isn't a declared parameter; skipped." msgstr "" -#: src/skit/cli.py:4713 +#: src/skit/cli.py:4752 #, python-format msgid "%(name)s is already declared; skipped." msgstr "" -#: src/skit/cli.py:4714 +#: src/skit/cli.py:4753 #, python-format msgid "%(name)s: that delivery isn't available for this kind; skipped." msgstr "" -#: src/skit/cli.py:4716 +#: src/skit/cli.py:4755 #, python-format msgid "%(name)s isn't a template placeholder, so it can't use placeholder delivery; skipped." msgstr "" -#: src/skit/cli.py:4719 +#: src/skit/cli.py:4758 #, python-format msgid "%(name)s: unknown type; skipped (use str, int, float, bool, choice, or path)." msgstr "" -#: src/skit/cli.py:4721 +#: src/skit/cli.py:4760 #, python-format msgid "%(name)s: the default doesn't fit its type; skipped." msgstr "" -#: src/skit/cli.py:4726 +#: src/skit/cli.py:4765 #, python-format msgid "%(name)s: a choice parameter needs choices; set --choices %(name)s=a,b,c." msgstr "" -#: src/skit/cli.py:4729 +#: src/skit/cli.py:4768 #, python-format msgid "%(name)s is on by default, so its flag could only ever turn it on again. Declare the flag that turns it OFF instead (--no-%(name)s and the like), with default false." msgstr "" -#: src/skit/cli.py:4785 +#: src/skit/cli.py:4824 #, python-format msgid "Variable insertion is off for %(name)s — turn it on first with: skit params %(name)s --interpolate" msgstr "" -#: src/skit/cli.py:4799 +#: src/skit/cli.py:4838 #, python-format msgid "Ignored a malformed value: %(item)s (expected NAME=VALUE)." msgstr "" -#: src/skit/cli.py:4855 +#: src/skit/cli.py:4894 #, python-format msgid "Updated %(name)s. Declared parameters: %(names)s" msgstr "" -#: src/skit/cli.py:4883 +#: src/skit/cli.py:4922 #, python-format msgid "Dependencies of %(name)s: %(deps)s" msgstr "" -#: src/skit/cli.py:4892 +#: src/skit/cli.py:4931 #, python-format msgid "External commands needed by %(name)s: %(needs)s" msgstr "" -#: src/skit/cli.py:4899 +#: src/skit/cli.py:4938 msgid "View or update an entry's package dependencies, Python constraint, and needed commands." msgstr "" -#: src/skit/cli.py:4902 +#: src/skit/cli.py:4941 msgid "Examples: skit deps tool --dep \"requests>=2,<3\" --dep rich · skit deps clip --need jq" msgstr "" -#: src/skit/cli.py:4908 +#: src/skit/cli.py:4947 msgid "A dependency (repeat for more; replaces the whole list)" msgstr "" -#: src/skit/cli.py:4910 +#: src/skit/cli.py:4949 msgid "Remove every dependency" msgstr "" -#: src/skit/cli.py:4918 +#: src/skit/cli.py:4957 msgid "An external command the entry needs on PATH (repeat; replaces the whole list)" msgstr "" -#: src/skit/cli.py:4922 +#: src/skit/cli.py:4961 msgid "Remove every needed external command" msgstr "" -#: src/skit/cli.py:4942 +#: src/skit/cli.py:4981 #, python-format msgid "%(name)s doesn't take package dependencies — only --need applies here." msgstr "" -#: src/skit/cli.py:4948 +#: src/skit/cli.py:4987 msgid "Use --dep to set the list or --clear to empty it — not both." msgstr "" -#: src/skit/cli.py:4953 +#: src/skit/cli.py:4992 msgid "Use --need to set the list or --clear-needs to empty it — not both." msgstr "" -#: src/skit/cli.py:4988 +#: src/skit/cli.py:5027 #, python-format msgid "Dependencies of %(name)s updated: %(deps)s" msgstr "" -#: src/skit/cli.py:4992 +#: src/skit/cli.py:5031 #, python-format msgid "Python constraint of %(name)s updated: %(value)s" msgstr "" -#: src/skit/cli.py:5002 +#: src/skit/cli.py:5041 #, python-format msgid "Needs of %(name)s updated: %(needs)s" msgstr "" -#: src/skit/cli.py:5015 +#: src/skit/cli.py:5054 msgid "Connect skit to AI agents: install the official Agent Skill." msgstr "" -#: src/skit/cli.py:5030 +#: src/skit/cli.py:5071 #, python-format msgid "Could not write the skill there: %(error)s" msgstr "" -#: src/skit/cli.py:5033 src/skit/tui_prefs.py:196 +#: src/skit/cli.py:5074 src/skit/tui_prefs.py:196 #, python-format msgid "Installed the skit Agent Skill: %(path)s" msgstr "" -#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5080 src/skit/tui_prefs.py:69 msgid "user" msgstr "" -#: src/skit/cli.py:5039 src/skit/tui_prefs.py:69 +#: src/skit/cli.py:5080 src/skit/tui_prefs.py:69 msgid "project" msgstr "" -#: src/skit/cli.py:5040 +#: src/skit/cli.py:5081 msgid "Agent directories on this machine:" msgstr "" -#: src/skit/cli.py:5044 +#: src/skit/cli.py:5085 msgid "Install where?" msgstr "" -#: src/skit/cli.py:5051 +#: src/skit/cli.py:5092 #, python-format msgid "Write the skill into %(path)s?" msgstr "" -#: src/skit/cli.py:5061 +#: src/skit/cli.py:5102 msgid "Install skit's Agent Skill into an AI agent's skills directory." msgstr "" -#: src/skit/cli.py:5063 +#: src/skit/cli.py:5104 msgid "Examples: skit agent install · skit agent install claude · skit agent install codex --project · skit agent install --to ~/.claude/skills" msgstr "" -#: src/skit/cli.py:5070 +#: src/skit/cli.py:5111 msgid "Where to install: claude, codex, or agents (the cross-agent ./.agents directory)" msgstr "" -#: src/skit/cli.py:5076 +#: src/skit/cli.py:5117 msgid "Install into this skills directory instead of a named target" msgstr "" -#: src/skit/cli.py:5082 +#: src/skit/cli.py:5123 msgid "Install into the current project (./.claude, ./.codex) instead of your home directory" msgstr "" -#: src/skit/cli.py:5091 +#: src/skit/cli.py:5134 msgid "Use a named target (with optional --project) or --to — not both." msgstr "" -#: src/skit/cli.py:5103 +#: src/skit/cli.py:5146 #, python-format msgid "Unknown target %(name)s. Valid targets: claude, codex, agents." msgstr "" -#: src/skit/cli.py:5112 +#: src/skit/cli.py:5155 msgid "Nothing installed: name a target (claude, codex, agents) or pass --to DIR." msgstr "" -#: src/skit/cli.py:5119 +#: src/skit/cli.py:5162 msgid "No agent directories detected (~/.claude, ~/.codex, ./.agents, …). Pass --to DIR to choose one yourself." msgstr "" -#: src/skit/cli.py:5125 +#: src/skit/cli.py:5168 msgid "Cancelled — nothing was written." msgstr "" -#: src/skit/cli.py:5146 +#: src/skit/cli.py:5189 msgid "Check that uv is available and the entry library is intact." msgstr "" -#: src/skit/cli.py:5149 +#: src/skit/cli.py:5192 msgid "Rebuild the index from each entry's meta.toml" msgstr "" -#: src/skit/cli.py:5165 src/skit/tui_health.py:172 +#: src/skit/cli.py:5209 src/skit/tui_health.py:172 #, python-format msgid "Index rebuilt: %(count)s entry" msgid_plural "Index rebuilt: %(count)s entries" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:5211 src/skit/tui_health.py:62 +#: src/skit/cli.py:5255 src/skit/tui_health.py:62 #, python-format msgid "uv: %(path)s" msgstr "" -#: src/skit/cli.py:5214 src/skit/tui_health.py:65 +#: src/skit/cli.py:5258 src/skit/tui_health.py:65 msgid "uv: not found. Install it from https://docs.astral.sh/uv/getting-started/installation/" msgstr "" -#: src/skit/cli.py:5218 src/skit/tui_health.py:72 +#: src/skit/cli.py:5262 src/skit/tui_health.py:72 #, python-format msgid "%(count)s entry registered" msgid_plural "%(count)s entries registered" msgstr[0] "" msgstr[1] "" -#: src/skit/cli.py:5223 +#: src/skit/cli.py:5267 #, python-format msgid "%(name)s: the launch target is gone from disk" msgstr "" -#: src/skit/cli.py:5227 +#: src/skit/cli.py:5271 #, python-format msgid "%(name)s: form definitions are out of sync — run: skit params %(name)s --resync" msgstr "" -#: src/skit/cli.py:5231 +#: src/skit/cli.py:5275 #, python-format msgid "%(name)s: missing external command(s): %(tools)s" msgstr "" -#: src/skit/cli.py:5237 +#: src/skit/cli.py:5281 #, python-format msgid "%(name)s: a run would refuse to start — %(reason)s" msgstr "" -#: src/skit/cli.py:5241 +#: src/skit/cli.py:5285 #, python-format msgid "Ignored malformed runner row(s) in config: %(rows)s. Inspect and repair with: skit runner list --all" msgstr "" -#: src/skit/cli.py:5245 src/skit/tui_health.py:134 +#: src/skit/cli.py:5289 src/skit/tui_health.py:134 #, python-format msgid "Library: %(path)s (%(count)s · %(size)s)" msgstr "" -#: src/skit/cli.py:5263 +#: src/skit/cli.py:5307 #, python-format msgid "auto (%(locale)s)" msgstr "" -#: src/skit/cli.py:5300 +#: src/skit/cli.py:5344 msgid "default ($VISUAL / $EDITOR)" msgstr "" -#: src/skit/cli.py:5316 +#: src/skit/cli.py:5360 msgid "auto (bash on PATH)" msgstr "" -#: src/skit/cli.py:5320 +#: src/skit/cli.py:5364 msgid "auto (deno > bun > node)" msgstr "" -#: src/skit/cli.py:5355 +#: src/skit/cli.py:5399 msgid "Mirrors are switched off — run `skit config mirror on` to activate them." msgstr "" -#: src/skit/cli.py:5368 +#: src/skit/cli.py:5412 msgid "Nothing to enable: no mirror URLs are saved. Set an axis first: mirror.pypi / mirror.github / mirror.npm." msgstr "" -#: src/skit/cli.py:5373 +#: src/skit/cli.py:5417 #, python-format msgid "Unknown mirror value: %(name)s. \"mirror\" is the master switch (on / off); mirrors are picked per ecosystem: mirror.pypi (%(pypi)s), mirror.github (%(github)s), mirror.npm (%(npm)s) — each also takes a URL or \"off\"." msgstr "" -#: src/skit/cli.py:5389 +#: src/skit/cli.py:5433 #, python-format msgid "Unknown %(key)s value: %(name)s. Choose from: %(names)s, off — or give a full URL." msgstr "" -#: src/skit/cli.py:5413 +#: src/skit/cli.py:5457 #, python-format msgid "Unknown mirror.github value: %(name)s. Choose from: %(names)s, off — or give an https:// github-release base URL (the uv binary is downloaded and executed, so https:// is required)." msgstr "" -#: src/skit/cli.py:5430 +#: src/skit/cli.py:5474 #, python-format msgid "Unknown language: %(tag)s. Available: %(locales)s" msgstr "" -#: src/skit/cli.py:5447 +#: src/skit/cli.py:5491 #, python-format msgid "Unknown form style: %(value)s. Choose from: tui, plain" msgstr "" -#: src/skit/cli.py:5457 src/skit/tui_prefs.py:483 +#: src/skit/cli.py:5501 src/skit/tui_prefs.py:483 #, python-format msgid "No such file: %(path)s" msgstr "" -#: src/skit/cli.py:5464 +#: src/skit/cli.py:5508 #, python-format msgid "Unknown JS runner: %(value)s. Choose from: %(names)s" msgstr "" -#: src/skit/cli.py:5471 +#: src/skit/cli.py:5515 #, python-format msgid "Unknown after-run behavior: %(value)s. Choose from: exit, stay" msgstr "" -#: src/skit/cli.py:5479 +#: src/skit/cli.py:5523 msgid "Read or set skit's settings (language, editor, mirror, form style, after-run)." msgstr "" -#: src/skit/cli.py:5481 +#: src/skit/cli.py:5525 msgid "Examples: skit config · skit config lang zh-TW · skit config mirror.pypi tsinghua" msgstr "" -#: src/skit/cli.py:5488 +#: src/skit/cli.py:5532 msgid "Setting name: lang / editor / mirror / mirror.pypi / mirror.github / mirror.npm / form / after_run / shell.bash_path / js.runner" msgstr "" -#: src/skit/cli.py:5493 +#: src/skit/cli.py:5537 msgid "New value (omit to read; lang also accepts \"auto\")" msgstr "" -#: src/skit/cli.py:5510 +#: src/skit/cli.py:5554 #, python-format msgid "Unknown setting: %(key)s. Available: %(keys)s" msgstr "" -#: src/skit/cli.py:5542 src/skit/tui_prefs.py:413 src/skit/tui_prefs.py:453 +#: src/skit/cli.py:5586 src/skit/tui_prefs.py:413 src/skit/tui_prefs.py:453 msgid "A custom choice needs a URL." msgstr "" -#: src/skit/cli.py:5549 src/skit/tui_prefs.py:348 +#: src/skit/cli.py:5593 src/skit/tui_prefs.py:348 msgid "github-release mirror base URL" msgstr "" -#: src/skit/cli.py:5555 src/skit/tui_prefs.py:458 +#: src/skit/cli.py:5599 src/skit/tui_prefs.py:458 #, python-format msgid "The uv binary is downloaded and executed, so the github-release base URL must use https:// (got: %(url)s)." msgstr "" -#: src/skit/cli.py:5574 src/skit/tui_prefs.py:337 +#: src/skit/cli.py:5618 src/skit/tui_prefs.py:337 msgid "PyPI index (Python packages)" msgstr "" -#: src/skit/cli.py:5576 src/skit/tui_prefs.py:341 +#: src/skit/cli.py:5620 src/skit/tui_prefs.py:341 msgid "PyPI index URL" msgstr "" -#: src/skit/cli.py:5580 src/skit/tui_prefs.py:342 +#: src/skit/cli.py:5624 src/skit/tui_prefs.py:342 msgid "GitHub releases (Python builds, the uv binary)" msgstr "" -#: src/skit/cli.py:5589 src/skit/tui_prefs.py:351 +#: src/skit/cli.py:5633 src/skit/tui_prefs.py:351 msgid "npm registry (JS/TS packages)" msgstr "" -#: src/skit/cli.py:5591 src/skit/tui_prefs.py:355 +#: src/skit/cli.py:5635 src/skit/tui_prefs.py:355 msgid "npm registry URL" msgstr "" -#: src/skit/cli.py:5607 +#: src/skit/cli.py:5651 msgid "Network to PyPI / GitHub looks slow or blocked." msgstr "" -#: src/skit/cli.py:5609 +#: src/skit/cli.py:5653 msgid "Configure mirrors for faster installs (mainland China)?" msgstr "" @@ -2193,64 +2198,64 @@ msgstr "" msgid "node rejected the injected copy: %(detail)s" msgstr "" -#: src/skit/langs/launch.py:61 +#: src/skit/langs/launch.py:62 msgid "uv not found. Install it (https://docs.astral.sh/uv/) or run skit doctor for guidance." msgstr "" -#: src/skit/langs/launch.py:68 +#: src/skit/langs/launch.py:69 #, python-format msgid "The script file doesn't exist: %(path)s" msgstr "" -#: src/skit/langs/launch.py:76 +#: src/skit/langs/launch.py:77 #, python-format msgid "The executable doesn't exist: %(path)s" msgstr "" -#: src/skit/langs/launch.py:82 +#: src/skit/langs/launch.py:83 #, python-format msgid "%(path)s isn't a runnable file (it's a directory or special file)." msgstr "" -#: src/skit/langs/launch.py:87 +#: src/skit/langs/launch.py:88 #, python-format msgid "%(path)s exists but isn't executable (chmod +x it?)." msgstr "" -#: src/skit/langs/launch.py:298 +#: src/skit/langs/launch.py:306 msgid "Can't safely fill in a value inside double quotes nested in a `…` command substitution — the shell strips one layer of escaping there. Rewrite that part of the template with $(…) instead of backticks." msgstr "" -#: src/skit/langs/launch.py:365 src/skit/langs/prompt/render.py:53 +#: src/skit/langs/launch.py:373 src/skit/langs/prompt/render.py:53 #, python-format msgid "Missing parameter values: %(names)s" msgstr "" -#: src/skit/langs/launch.py:452 +#: src/skit/langs/launch.py:460 #, python-format msgid "%(name)s isn't available on this system. Install Git for Windows (its bash works) or WSL, or point skit at one with: skit config shell.bash_path " msgstr "" -#: src/skit/langs/launch.py:458 +#: src/skit/langs/launch.py:466 #, python-format msgid "The interpreter %(name)s isn't installed (or isn't on PATH)." msgstr "" -#: src/skit/langs/launch.py:546 +#: src/skit/langs/launch.py:554 #, python-format msgid "No JavaScript runtime found (looked for: %(names)s). Install deno, bun, or node — or pick one with: skit config js.runner " msgstr "" -#: src/skit/langs/launch.py:694 +#: src/skit/langs/launch.py:702 #, python-format msgid "The runner %(runner)s needs %(binary)s, which isn't installed (or isn't on PATH)." msgstr "" -#: src/skit/langs/launch.py:756 +#: src/skit/langs/launch.py:764 msgid "Warning: Pi would interpret the beginning of this prompt as a CLI option, file, or package command. skit prepended one newline and is continuing; the prompt delivered to Pi is one character longer than the rendered text." msgstr "" -#: src/skit/langs/launch.py:865 src/skit/langs/launch.py:882 +#: src/skit/langs/launch.py:873 src/skit/langs/launch.py:890 msgid "" msgstr "" @@ -2286,22 +2291,22 @@ msgstr "" msgid "⚠ This script reads its own location ($0 / $BASH_SOURCE), and the injected values run from a temporary copy — so it sees the copy's path, not the original's. Rewriting a constant as NAME=\"${NAME:-value}\" delivers the value through the environment instead, with no copy at all (`skit params