skit-cli 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {skit_cli-0.3.0 → skit_cli-0.4.0}/PKG-INFO +7 -6
- {skit_cli-0.3.0 → skit_cli-0.4.0}/README.md +5 -4
- {skit_cli-0.3.0 → skit_cli-0.4.0}/pyproject.toml +2 -2
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/analysis.py +106 -12
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/argstate.py +31 -7
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/cli.py +49 -7
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/flows.py +102 -10
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/cli_reader.py +161 -15
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/argspec.py +112 -22
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/shell/analyzer.py +4 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/locales/skit.pot +579 -563
- skit_cli-0.4.0/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/locales/zh_CN/LC_MESSAGES/skit.po +578 -562
- skit_cli-0.4.0/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/locales/zh_TW/LC_MESSAGES/skit.po +578 -562
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/promptform.py +4 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/skills/skit/SKILL.md +3 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui.py +11 -1
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_form.py +139 -18
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_settings.py +27 -14
- skit_cli-0.3.0/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
- skit_cli-0.3.0/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/LICENSE +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/agentskill.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/argv_text.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/atomic.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/callmatch.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/config.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/editor.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/healthcheck.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/i18n.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/inlineform.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/kindnames.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/base.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/fish/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/fish/analyzer.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/fish/cli_reader.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/analyzer.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/deps.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/inject.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/javascript/io.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/launch.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/powershell/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/powershell/cli_reader.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/prompt/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/prompt/analyzer.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/prompt/render.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/prompt/text.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/analyzer.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/metawriter.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/reconcile.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/python/shim.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/registry.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/shell/__init__.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/shell/cli_reader.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/shell/inject.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/langs/shell/normalize.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/launcher.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/models.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/params.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/paths.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/pep723.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/rewrite.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/store.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/theme.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tokens.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_add.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_footer.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_health.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_layout.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_pathpick.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_prefs.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_prompt.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/tui_runner.py +0 -0
- {skit_cli-0.3.0 → skit_cli-0.4.0}/src/skit/uvman.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skit-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A terminal script manager and launcher for humans and AI agents: put any script, prompt, or program in one library; skit turns its inputs and flags into a run form and remembers past parameters. Ships an Agent Skill so agents reuse the scripts they wrote.
|
|
5
5
|
Keywords: script,launcher,uv,tui,pep723,script-manager,script-runner,automation,ai-agent,ai-prompt,agent-skills
|
|
6
6
|
Author: Yi-Ting Chiu
|
|
7
7
|
Author-email: Yi-Ting Chiu <mytim710@gmail.com>
|
|
@@ -48,7 +48,8 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
|
|
49
49
|
**skit is a launcher and a home for your scripts.**
|
|
50
50
|
|
|
51
|
-
skit stores your scripts in one place and makes them painless to launch
|
|
51
|
+
skit stores your scripts in one place and makes them painless to launch — Python, shell, and
|
|
52
|
+
JS/TS go deepest, a dozen kinds in all.
|
|
52
53
|
|
|
53
54
|
**AI writes the scripts. skit gives them a home.**
|
|
54
55
|
|
|
@@ -56,14 +57,14 @@ And it's a home you share with your agent: the library you drive from a menu, AI
|
|
|
56
57
|
drive through a deterministic CLI — checking it before writing yet another one-off
|
|
57
58
|
script, and (with your OK) saving the good ones back, so they outlive the chat.
|
|
58
59
|
|
|
59
|
-
<video src="https://github.com/user-attachments/assets/
|
|
60
|
+
<video src="https://github.com/user-attachments/assets/8a1f27cd-f2f2-42db-977a-b4f8ea207340" controls></video>
|
|
60
61
|
|
|
61
62
|
## What it does
|
|
62
63
|
|
|
63
64
|
- **One home for your scripts and prompts.** `skit add` collects scattered scripts and prompts into a searchable library — keep a copy in the library, or reference the original file. Run it bare and it asks what you're adding; hand it a file it can't classify and it asks which kind, instead of erroring out.
|
|
64
65
|
- **Parameters without the pain.** Flags, `input()` calls, and the constants you tick become form fields (choices → pickers, booleans → checkboxes, paths → completion and a file browser, types enforced).
|
|
65
|
-
- **Paths you don't have to type.**
|
|
66
|
-
- **It remembers.**
|
|
66
|
+
- **Paths you don't have to type.** Every text field carries a `📁 browse` link that opens a file picker, and path fields complete as you type — press `→` to accept the suggestion. You never hand-type a directory again. (`Ctrl+T` → *File or folder…* is the keyboard route to the same picker.)
|
|
67
|
+
- **It remembers.** Script defaults arrive prefilled and track the source when it changes; last-used values come back automatically, and `↺ default` (Ctrl+O) puts the script's own value back. Save favorites as named presets. Parameters marked secret never touch disk. Tokens like `{cwd}` and `{today}` keep presets portable.
|
|
67
68
|
- **No environment mess.** Python scripts declare their dependencies inline (PEP 723) and run via uv in isolated environments; JS/TS scripts get a per-script `node_modules`, installed from their declared packages on first run. Nothing global either way. Other languages use the tools already on your machine — skit checks that declared external commands are on your `PATH` before running.
|
|
68
69
|
- **Mouse or keyboard.** Plain `skit` opens the full TUI; every key hint on screen is also a clickable button.
|
|
69
70
|
- **Automation-ready.** Every TUI action is also a CLI command with `--json` output and meaningful exit codes — for shell scripts, CI, and AI agents.
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
**skit is a launcher and a home for your scripts.**
|
|
15
15
|
|
|
16
|
-
skit stores your scripts in one place and makes them painless to launch
|
|
16
|
+
skit stores your scripts in one place and makes them painless to launch — Python, shell, and
|
|
17
|
+
JS/TS go deepest, a dozen kinds in all.
|
|
17
18
|
|
|
18
19
|
**AI writes the scripts. skit gives them a home.**
|
|
19
20
|
|
|
@@ -21,14 +22,14 @@ And it's a home you share with your agent: the library you drive from a menu, AI
|
|
|
21
22
|
drive through a deterministic CLI — checking it before writing yet another one-off
|
|
22
23
|
script, and (with your OK) saving the good ones back, so they outlive the chat.
|
|
23
24
|
|
|
24
|
-
<video src="https://github.com/user-attachments/assets/
|
|
25
|
+
<video src="https://github.com/user-attachments/assets/8a1f27cd-f2f2-42db-977a-b4f8ea207340" controls></video>
|
|
25
26
|
|
|
26
27
|
## What it does
|
|
27
28
|
|
|
28
29
|
- **One home for your scripts and prompts.** `skit add` collects scattered scripts and prompts into a searchable library — keep a copy in the library, or reference the original file. Run it bare and it asks what you're adding; hand it a file it can't classify and it asks which kind, instead of erroring out.
|
|
29
30
|
- **Parameters without the pain.** Flags, `input()` calls, and the constants you tick become form fields (choices → pickers, booleans → checkboxes, paths → completion and a file browser, types enforced).
|
|
30
|
-
- **Paths you don't have to type.**
|
|
31
|
-
- **It remembers.**
|
|
31
|
+
- **Paths you don't have to type.** Every text field carries a `📁 browse` link that opens a file picker, and path fields complete as you type — press `→` to accept the suggestion. You never hand-type a directory again. (`Ctrl+T` → *File or folder…* is the keyboard route to the same picker.)
|
|
32
|
+
- **It remembers.** Script defaults arrive prefilled and track the source when it changes; last-used values come back automatically, and `↺ default` (Ctrl+O) puts the script's own value back. Save favorites as named presets. Parameters marked secret never touch disk. Tokens like `{cwd}` and `{today}` keep presets portable.
|
|
32
33
|
- **No environment mess.** Python scripts declare their dependencies inline (PEP 723) and run via uv in isolated environments; JS/TS scripts get a per-script `node_modules`, installed from their declared packages on first run. Nothing global either way. Other languages use the tools already on your machine — skit checks that declared external commands are on your `PATH` before running.
|
|
33
34
|
- **Mouse or keyboard.** Plain `skit` opens the full TUI; every key hint on screen is also a clickable button.
|
|
34
35
|
- **Automation-ready.** Every TUI action is also a CLI command with `--json` output and meaningful exit codes — for shell scripts, CI, and AI agents.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# Distribution name only ("skit" is admin-prohibited on PyPI); the import package,
|
|
3
3
|
# CLI command, and config dirs all stay "skit".
|
|
4
4
|
name = "skit-cli"
|
|
5
|
-
version = "0.
|
|
6
|
-
description = "
|
|
5
|
+
version = "0.4.0"
|
|
6
|
+
description = "A terminal script manager and launcher for humans and AI agents: put any script, prompt, or program in one library; skit turns its inputs and flags into a run form and remembers past parameters. Ships an Agent Skill so agents reuse the scripts they wrote."
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.12"
|
|
9
9
|
license = "MIT"
|
|
@@ -25,10 +25,10 @@ from dataclasses import dataclass, field, replace
|
|
|
25
25
|
from typing import TYPE_CHECKING
|
|
26
26
|
|
|
27
27
|
from .callmatch import match_calls
|
|
28
|
-
from .params import ParamDecl
|
|
28
|
+
from .params import ParamDecl, coerce_default
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
|
-
from collections.abc import Callable
|
|
31
|
+
from collections.abc import Callable, Mapping
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@dataclass
|
|
@@ -55,6 +55,11 @@ class Candidate:
|
|
|
55
55
|
# envdefault only: the ${NAME} variable the value is read from at run time (== name; kept
|
|
56
56
|
# explicit so an env-delivery caller never has to re-derive it). "" for const/input.
|
|
57
57
|
env_name: str = ""
|
|
58
|
+
# envdefault only: an explicitly empty environment value still activates the
|
|
59
|
+
# source's fallback. Shell's colon operators (:- and :=) have this behavior;
|
|
60
|
+
# the non-colon operators and fish's set-query idiom do not. Kept as a semantic
|
|
61
|
+
# boolean so the language-neutral form layer never needs to know shell syntax.
|
|
62
|
+
empty_uses_default: bool = False
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
@dataclass
|
|
@@ -92,6 +97,20 @@ class Report:
|
|
|
92
97
|
rebind: list[tuple[ParamDecl, Candidate]] = field(default_factory=list)
|
|
93
98
|
new: list[Candidate] = field(default_factory=list)
|
|
94
99
|
syntax_error: bool = False
|
|
100
|
+
# The SOURCE's current default for every ok-matched const/envdefault spec, keyed by
|
|
101
|
+
# name. The stored block default is a cache written at manage time; the script is the
|
|
102
|
+
# truth, and a user who edits `X = "hello"` to `X = "bonjour"` must see (and prefill,
|
|
103
|
+
# and reset to) "bonjour" — the stale cache used to be silently re-injected OVER that
|
|
104
|
+
# edit. A value change is deliberately NOT drift: tracking the source is the expected
|
|
105
|
+
# behavior, not a warning. Two exclusions, both in _records_default/_type_matches: a
|
|
106
|
+
# SECRET's literal never travels (C3), and a type-mismatched default never overlays a
|
|
107
|
+
# spec (a const carries a drift warning and keeps its old prefill until resync; an env
|
|
108
|
+
# param stays ok but its default must still fit the declared type).
|
|
109
|
+
current_defaults: dict[str, str | int | float | bool] = field(default_factory=dict)
|
|
110
|
+
# Managed envdefault names for which an empty environment value still selects
|
|
111
|
+
# the source fallback. Like current_defaults, this is live source semantics that
|
|
112
|
+
# is projected onto the run form without changing the frozen [tool.skit] block.
|
|
113
|
+
empty_uses_default: set[str] = field(default_factory=set)
|
|
95
114
|
|
|
96
115
|
@property
|
|
97
116
|
def has_drift(self) -> bool:
|
|
@@ -107,6 +126,18 @@ class Report:
|
|
|
107
126
|
return self.ok + [spec for spec, _ in self.changed] + [spec for spec, _ in self.rebind]
|
|
108
127
|
|
|
109
128
|
|
|
129
|
+
def effective_default(
|
|
130
|
+
spec: ParamDecl, current: Mapping[str, str | int | float | bool]
|
|
131
|
+
) -> str | int | float | bool | None:
|
|
132
|
+
"""The default a READ surface should show for a managed spec: the source's current
|
|
133
|
+
literal when reconcile published one (Report.current_defaults), else the stored
|
|
134
|
+
block value. THE one place that rule lives — `skit params`, `params --json` and the
|
|
135
|
+
Entry-settings pane all show the same record, and the comments on each of them
|
|
136
|
+
promise they cannot disagree; two hand-rolled copies of a fallback is exactly how
|
|
137
|
+
that promise rots."""
|
|
138
|
+
return current.get(spec.name, spec.default)
|
|
139
|
+
|
|
140
|
+
|
|
110
141
|
def drift_lines(report: Report, name: str) -> list[str]:
|
|
111
142
|
"""The display lines for a drift report (shared by CLI/TUI). The only copy exit point in this
|
|
112
143
|
module: plain-text old/new comparison; rich markup/color is wrapped by the caller."""
|
|
@@ -231,8 +262,8 @@ def edit_specs(
|
|
|
231
262
|
# insertion order, so this only changes anything when specs already has duplicate names).
|
|
232
263
|
order: list[str] = list(by_name) # keep original order; new ones appended at the end
|
|
233
264
|
|
|
234
|
-
# 1) resync: prune missing and update changed types per the current script
|
|
235
|
-
# secret/prompt
|
|
265
|
+
# 1) resync: prune missing and update changed types/defaults per the current script
|
|
266
|
+
# (keeping custom secret/prompt).
|
|
236
267
|
if resync:
|
|
237
268
|
_apply_resync(text, specs, by_name, order, warnings, analyze=analyze)
|
|
238
269
|
|
|
@@ -275,20 +306,39 @@ def _apply_resync(
|
|
|
275
306
|
warnings.append("resync-skipped")
|
|
276
307
|
return
|
|
277
308
|
missing_names = {s.name for s in report.missing}
|
|
278
|
-
|
|
309
|
+
changed_pairs = {spec.name: cand for spec, cand in report.changed}
|
|
279
310
|
rebind_targets = {spec.name: cand for spec, cand in report.rebind}
|
|
280
311
|
for name in list(order):
|
|
281
312
|
if name in missing_names:
|
|
282
313
|
warnings.append(f"resync-dropped:{name}")
|
|
283
314
|
del by_name[name]
|
|
284
315
|
order.remove(name)
|
|
285
|
-
elif name in
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
|
|
316
|
+
elif name in changed_pairs:
|
|
317
|
+
# cand.type is a plain str while ParamDecl.type is the closed ParamType
|
|
318
|
+
# literal, so re-anchor through dataclasses.replace (whose kwargs are
|
|
319
|
+
# untyped) rather than a direct attribute write. by_name[name] is already a
|
|
320
|
+
# private shallow copy (see the replace() at the top of edit_specs), so
|
|
321
|
+
# swapping in a fresh copy is the same visible effect as mutating in place.
|
|
322
|
+
# The default rides along with the type: once the spec is retyped to the
|
|
323
|
+
# source's current shape, keeping the old-typed default would leave stale
|
|
324
|
+
# junk (an int 3 on a now-str constant) in the refreshed record. It rides
|
|
325
|
+
# through the SAME secret gate the ok lane uses (_recordable_default): a
|
|
326
|
+
# secret's literal must not be written into the block, from where
|
|
327
|
+
# to_block_dict would publish it to `params --json` / `show --json`. A
|
|
328
|
+
# retyped secret therefore comes out with no default at all — honest, and
|
|
329
|
+
# the only leak-free answer once its old default no longer fits the type.
|
|
330
|
+
cand = changed_pairs[name]
|
|
331
|
+
by_name[name] = replace(
|
|
332
|
+
by_name[name],
|
|
333
|
+
type=cand.type,
|
|
334
|
+
default=_recordable_default(by_name[name], cand),
|
|
335
|
+
)
|
|
336
|
+
elif name in report.current_defaults:
|
|
337
|
+
# An ok-matched const/envdefault whose SOURCE default moved on: resync's job
|
|
338
|
+
# is to make the stored record match the script again, and the cached default
|
|
339
|
+
# is part of that record (the run form already tracks the source either way —
|
|
340
|
+
# see Report.current_defaults).
|
|
341
|
+
by_name[name] = replace(by_name[name], default=report.current_defaults[name])
|
|
292
342
|
elif name in rebind_targets:
|
|
293
343
|
# The prompt no longer uniquely resolves; re-anchor to whichever call site position
|
|
294
344
|
# currently supplied it, so the *next* run's plain reconcile() (no --resync) sees an
|
|
@@ -338,6 +388,12 @@ def _apply_tweaks(
|
|
|
338
388
|
by_name[name].env_source = "" # an env source only means anything on a secret
|
|
339
389
|
else:
|
|
340
390
|
warnings.append(f"not-managed:{name}")
|
|
391
|
+
# Apply the final-state rule after both lists: `--secret X --no-secret X` ends
|
|
392
|
+
# public and must keep its public default, while every explicit final transition
|
|
393
|
+
# to secret removes the cached literal before the block/JSON surfaces can expose it.
|
|
394
|
+
for name in secret:
|
|
395
|
+
if name in by_name and by_name[name].secret:
|
|
396
|
+
by_name[name].default = None
|
|
341
397
|
for name, prompt in prompts.items():
|
|
342
398
|
if name in by_name:
|
|
343
399
|
by_name[name].prompt = prompt
|
|
@@ -345,6 +401,40 @@ def _apply_tweaks(
|
|
|
345
401
|
warnings.append(f"not-managed:{name}")
|
|
346
402
|
|
|
347
403
|
|
|
404
|
+
def _record_default(report: Report, spec: ParamDecl, cand: Candidate) -> None:
|
|
405
|
+
"""Publish the source's current literal as this spec's default — the one rule both
|
|
406
|
+
matched lanes share. It travels only when it MAY (see _recordable_default) and when
|
|
407
|
+
the DECLARED type can actually hold it.
|
|
408
|
+
|
|
409
|
+
Fitness is coercibility, not type equality: the analyzers type a literal by its
|
|
410
|
+
shape, so a `str` param defaulting to `8080` reads back as an int candidate and must
|
|
411
|
+
still refresh (its value is text either way). What must not pass is a default the
|
|
412
|
+
declared type cannot represent — an env param stays ok through a type change (the
|
|
413
|
+
value arrives by environment regardless), so `PORT=${PORT:-$FALLBACK}` on an int
|
|
414
|
+
param would otherwise prefill text that fails the form's own validation and hand
|
|
415
|
+
resync that text to write onto an int spec."""
|
|
416
|
+
default = _recordable_default(spec, cand)
|
|
417
|
+
if default is None:
|
|
418
|
+
return
|
|
419
|
+
try:
|
|
420
|
+
coerce_default(str(default), spec.type)
|
|
421
|
+
except ValueError:
|
|
422
|
+
return
|
|
423
|
+
report.current_defaults[spec.name] = default
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _recordable_default(spec: ParamDecl, cand: Candidate) -> str | int | float | bool | None:
|
|
427
|
+
"""The source literal this match may publish as the spec's current default, or None
|
|
428
|
+
when it may not.
|
|
429
|
+
|
|
430
|
+
A SECRET is the one hard exclusion (C3): its literal is exactly the thing that must
|
|
431
|
+
not travel. current_defaults feeds `params --json`, `show --json` and the settings
|
|
432
|
+
pane — machine-facing surfaces with no masking of their own — so a hardcoded
|
|
433
|
+
`TOKEN = "sk-live-…"` would leave the script's own text for the first time. The
|
|
434
|
+
secret's stored block default (if any) still describes it, masked, exactly as before."""
|
|
435
|
+
return None if spec.secret else cand.default
|
|
436
|
+
|
|
437
|
+
|
|
348
438
|
def _type_matches(spec: ParamDecl, cand: Candidate) -> bool:
|
|
349
439
|
"""Whether a stored const type still matches what the source derives. A declared
|
|
350
440
|
``path`` over a derived ``str`` is a user refinement — a string constant is exactly
|
|
@@ -402,6 +492,9 @@ def reconcile( # noqa: PLR0912 — one branch per binding and drift category; a
|
|
|
402
492
|
else:
|
|
403
493
|
covered_envs.add(spec.name)
|
|
404
494
|
report.ok.append(spec) # default-text/type changes stay ok (env delivery)
|
|
495
|
+
_record_default(report, spec, cand)
|
|
496
|
+
if cand.empty_uses_default:
|
|
497
|
+
report.empty_uses_default.add(spec.name)
|
|
405
498
|
continue
|
|
406
499
|
cand = consts.get(spec.name)
|
|
407
500
|
if cand is None:
|
|
@@ -412,6 +505,7 @@ def reconcile( # noqa: PLR0912 — one branch per binding and drift category; a
|
|
|
412
505
|
else:
|
|
413
506
|
covered_consts.add(spec.name)
|
|
414
507
|
report.ok.append(spec)
|
|
508
|
+
_record_default(report, spec, cand)
|
|
415
509
|
|
|
416
510
|
for cand in analysis.candidates:
|
|
417
511
|
if (
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
- File shape: [values] (last-used), extra_args, [presets.<name>] (named presets).
|
|
5
5
|
- **C3 is enforced structurally here**: every write entry point requires secret_names, and any key
|
|
6
6
|
in that list is stripped before it hits disk, so a secret value can never appear in a state file
|
|
7
|
-
(there are tests for this). This
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
(there are tests for this). This includes the complete values snapshot nested under last_run,
|
|
8
|
+
which powers `preset save --from-last`. This holds for *new* writes; it says nothing about a value
|
|
9
|
+
that was written while the parameter was still public. purge_secret() retroactively scrubs that
|
|
10
|
+
plaintext once a parameter transitions to secret, and save_last() also drops any now-secret key
|
|
11
|
+
left over from before, even on calls that carry no new value for it — so nothing written while a
|
|
12
|
+
parameter was public can outlive it becoming secret.
|
|
12
13
|
- Value resolution (this run's input > preset > last-used > definition default) lives in
|
|
13
14
|
flows.prefill; this module only stores and strips.
|
|
14
15
|
"""
|
|
@@ -150,6 +151,16 @@ def purge_secret(slug: str, names: Iterable[str]) -> set[str]:
|
|
|
150
151
|
new_presets[name] = cleaned
|
|
151
152
|
doc["presets"] = new_presets
|
|
152
153
|
|
|
154
|
+
# The exact last-run snapshot is another value-bearing surface. A parameter that
|
|
155
|
+
# becomes secret after it ran publicly must be scrubbed here too, or --from-last
|
|
156
|
+
# could copy the old plaintext back into a preset.
|
|
157
|
+
last_run = dict(doc.get("last_run", {}))
|
|
158
|
+
if "values" in last_run:
|
|
159
|
+
last_values = dict(last_run.get("values", {}))
|
|
160
|
+
removed |= banned & last_values.keys()
|
|
161
|
+
last_run["values"] = _strip_secrets(last_values, banned)
|
|
162
|
+
doc["last_run"] = last_run
|
|
163
|
+
|
|
153
164
|
_save_doc(slug, doc)
|
|
154
165
|
return removed
|
|
155
166
|
|
|
@@ -177,12 +188,25 @@ def save_last_runner(name: str) -> None:
|
|
|
177
188
|
atomic_write_toml(state_dir() / "prompt.toml", {"last_runner": name})
|
|
178
189
|
|
|
179
190
|
|
|
180
|
-
def record_run(
|
|
191
|
+
def record_run(
|
|
192
|
+
slug: str,
|
|
193
|
+
exit_code: int,
|
|
194
|
+
*,
|
|
195
|
+
at: str,
|
|
196
|
+
values: dict[str, str] | None = None,
|
|
197
|
+
secret_names: Iterable[str] = (),
|
|
198
|
+
) -> None:
|
|
181
199
|
"""Remember when the entry last ran and how it exited (Library sort order, detail pane,
|
|
182
200
|
and the r-rerun context key all read this). Stored as a table — a bare `last_exit = 0`
|
|
183
201
|
top-level key would be dropped by _save_doc's empty-section pruning (0 is falsy)."""
|
|
184
202
|
doc = _load_doc(slug)
|
|
185
|
-
|
|
203
|
+
last_run: dict[str, Any] = {"at": at, "exit": exit_code}
|
|
204
|
+
if values is not None:
|
|
205
|
+
# Unlike last-used [values], this is the exact accepted invocation: values
|
|
206
|
+
# equal to defaults and delivered empty strings stay so --from-last can pin
|
|
207
|
+
# what actually ran instead of reconstructing it from a later source version.
|
|
208
|
+
last_run["values"] = _strip_secrets(values, secret_names)
|
|
209
|
+
doc["last_run"] = last_run
|
|
186
210
|
_save_doc(slug, doc)
|
|
187
211
|
|
|
188
212
|
|
|
@@ -2334,6 +2334,10 @@ def _field_to_dict(f: flows.FormField) -> dict[str, object]:
|
|
|
2334
2334
|
"flag": f.flag,
|
|
2335
2335
|
"action": f.action,
|
|
2336
2336
|
"env_source": f.env_source,
|
|
2337
|
+
# True = clearing this field (or --set NAME=) delivers an EMPTY STRING; false =
|
|
2338
|
+
# an empty value means "unset — the script's own default applies". Additive key
|
|
2339
|
+
# so an agent can tell the two apart before assembling a run.
|
|
2340
|
+
"delivers_empty": f.delivers_empty,
|
|
2337
2341
|
}
|
|
2338
2342
|
|
|
2339
2343
|
|
|
@@ -3132,7 +3136,10 @@ def run(
|
|
|
3132
3136
|
argstate.save_preset(
|
|
3133
3137
|
entry.slug,
|
|
3134
3138
|
save_preset,
|
|
3135
|
-
|
|
3139
|
+
# A preset is the exact snapshot of this run — a cleared field included, so
|
|
3140
|
+
# it pins "stays cleared" over whatever last-used remembers (secrets are
|
|
3141
|
+
# stripped structurally by argstate).
|
|
3142
|
+
values,
|
|
3136
3143
|
secret_names=plan.secret_names,
|
|
3137
3144
|
)
|
|
3138
3145
|
# stderr, like every run-adjacent skit line ("Reusing your last arguments",
|
|
@@ -3536,15 +3543,34 @@ def preset_save(
|
|
|
3536
3543
|
EXIT_USAGE,
|
|
3537
3544
|
)
|
|
3538
3545
|
if from_last:
|
|
3539
|
-
|
|
3546
|
+
state = argstate.load_state(entry.slug)
|
|
3547
|
+
if not state["last_run"] and not state["values"]:
|
|
3548
|
+
raise _fail(
|
|
3549
|
+
gettext("%(name)s has no remembered values yet — run it once first.")
|
|
3550
|
+
% {"name": entry.meta.name},
|
|
3551
|
+
1,
|
|
3552
|
+
)
|
|
3540
3553
|
keys = {f.key for f in plan.fields}
|
|
3541
|
-
|
|
3542
|
-
if
|
|
3554
|
+
last_snapshot = state["last_run"].get("values")
|
|
3555
|
+
if isinstance(last_snapshot, dict):
|
|
3556
|
+
# The exact accepted invocation, filtered only for fields removed since
|
|
3557
|
+
# that run. Never overlay today's defaults: a changed/new definition did
|
|
3558
|
+
# not participate in the historical run this command promises to save.
|
|
3559
|
+
values = {k: str(v) for k, v in last_snapshot.items() if k in keys}
|
|
3560
|
+
elif state["last_run"]:
|
|
3561
|
+
# Legacy state with a run stamp but no exact snapshot cannot be rebuilt:
|
|
3562
|
+
# remembered values deliberately omitted accepted defaults. Asking for a
|
|
3563
|
+
# fresh run is honest; current defaults would fabricate history.
|
|
3543
3564
|
raise _fail(
|
|
3544
3565
|
gettext("%(name)s has no remembered values yet — run it once first.")
|
|
3545
3566
|
% {"name": entry.meta.name},
|
|
3546
3567
|
1,
|
|
3547
3568
|
)
|
|
3569
|
+
else:
|
|
3570
|
+
# Older state from before run stamps existed may still have explicit
|
|
3571
|
+
# last-used values. Preserve that narrow compatibility lane without
|
|
3572
|
+
# inventing missing defaults or newly-added fields.
|
|
3573
|
+
values = {k: v for k, v in state["values"].items() if k in keys}
|
|
3548
3574
|
elif _is_interactive():
|
|
3549
3575
|
values = promptform.collect(plan, flows.prefill(plan, entry.slug), console=console)
|
|
3550
3576
|
else:
|
|
@@ -3558,7 +3584,14 @@ def preset_save(
|
|
|
3558
3584
|
% {"names": ", ".join(escape(n) for n in sorted(secret_overlap))}
|
|
3559
3585
|
+ _DIM_CLOSE
|
|
3560
3586
|
)
|
|
3561
|
-
argstate.save_preset(
|
|
3587
|
+
argstate.save_preset(
|
|
3588
|
+
entry.slug,
|
|
3589
|
+
preset_name,
|
|
3590
|
+
# The exact snapshot, cleared fields included — one rule for every preset
|
|
3591
|
+
# writer (the run form's Ctrl+S and `run --save-preset` store the same way).
|
|
3592
|
+
values,
|
|
3593
|
+
secret_names=plan.secret_names,
|
|
3594
|
+
)
|
|
3562
3595
|
console.print(
|
|
3563
3596
|
f"[green]{gettext('Preset "%(preset)s" saved for %(name)s.') % {'preset': escape(preset_name), 'name': escape(entry.meta.name)}}[/green]"
|
|
3564
3597
|
)
|
|
@@ -3787,6 +3820,7 @@ def _show_params(entry: store.Entry, as_json: bool) -> None:
|
|
|
3787
3820
|
# vanished analyzable script must not invent either signal (test_cli_mut_part06 pins it).
|
|
3788
3821
|
self_locating, reader_driven = False, False
|
|
3789
3822
|
ref_mode = entry.meta.mode == "reference"
|
|
3823
|
+
current_defaults: dict[str, str | int | float | bool] = {}
|
|
3790
3824
|
if entry_spec is not None and entry_spec.analyzer is not None: # noqa: SIM102
|
|
3791
3825
|
# Keep the text gate nested instead of pragma-suppressing a three-way BooleanOperation:
|
|
3792
3826
|
# changing ``and text`` to ``or text`` is equivalent under the registry invariant, but a
|
|
@@ -3797,6 +3831,9 @@ def _show_params(entry: store.Entry, as_json: bool) -> None:
|
|
|
3797
3831
|
# only the WRITE ops differ, and the advice below switches voice on that).
|
|
3798
3832
|
report = entry_spec.analyzer.reconcile(text, specs)
|
|
3799
3833
|
an = entry_spec.analyzer.analyze(text)
|
|
3834
|
+
# The Default column shows the SOURCE's current value, exactly like the run
|
|
3835
|
+
# form's prefill — two surfaces reading one record must not disagree.
|
|
3836
|
+
current_defaults = report.current_defaults
|
|
3800
3837
|
# A MODELED reader form (flows.reader_fields — the one trap predicate every
|
|
3801
3838
|
# surface shares) is the entry's real interface: plan_for_entry prefers managed
|
|
3802
3839
|
# params, so a --manage advice here would sell REPLACING that form. Self-parsing
|
|
@@ -3822,6 +3859,10 @@ def _show_params(entry: store.Entry, as_json: bool) -> None:
|
|
|
3822
3859
|
if as_json:
|
|
3823
3860
|
payload = {
|
|
3824
3861
|
"params": [s.to_block_dict() for s in specs],
|
|
3862
|
+
# The SOURCE's current default per managed name (additive key). "params"
|
|
3863
|
+
# stays the verbatim stored record — an agent that wants the value a run
|
|
3864
|
+
# would actually prefill reads it here, same truth as the human table.
|
|
3865
|
+
"current_defaults": current_defaults,
|
|
3825
3866
|
"unmanaged": unmanaged,
|
|
3826
3867
|
"placeholders": entry.meta.params or [],
|
|
3827
3868
|
"declared": [d.to_meta_dict() for d in declared],
|
|
@@ -3887,10 +3928,11 @@ def _show_params(entry: store.Entry, as_json: bool) -> None:
|
|
|
3887
3928
|
last_shown = gettext("•••") if s.name in last else "—"
|
|
3888
3929
|
else:
|
|
3889
3930
|
last_shown = last.get(s.name, "—")
|
|
3931
|
+
shown_default = analysis.effective_default(s, current_defaults)
|
|
3890
3932
|
default_shown = (
|
|
3891
3933
|
gettext("•••")
|
|
3892
|
-
if s.secret and
|
|
3893
|
-
else ("—" if
|
|
3934
|
+
if s.secret and shown_default is not None
|
|
3935
|
+
else ("—" if shown_default is None else str(shown_default))
|
|
3894
3936
|
)
|
|
3895
3937
|
table.add_row(
|
|
3896
3938
|
escape(s.name),
|