skit-cli 0.0.1__tar.gz → 0.2.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.
Files changed (46) hide show
  1. skit_cli-0.0.1/README.md → skit_cli-0.2.0/PKG-INFO +63 -1
  2. skit_cli-0.0.1/PKG-INFO → skit_cli-0.2.0/README.md +33 -31
  3. {skit_cli-0.0.1 → skit_cli-0.2.0}/pyproject.toml +20 -5
  4. skit_cli-0.2.0/src/skit/__init__.py +20 -0
  5. skit_cli-0.2.0/src/skit/agentskill.py +99 -0
  6. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/atomic.py +25 -1
  7. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/cli.py +410 -29
  8. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/config.py +22 -2
  9. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/flows.py +12 -0
  10. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/inlineform.py +6 -1
  11. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/locales/skit.pot +521 -347
  12. skit_cli-0.2.0/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
  13. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/locales/zh_CN/LC_MESSAGES/skit.po +787 -420
  14. skit_cli-0.2.0/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
  15. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/locales/zh_TW/LC_MESSAGES/skit.po +787 -420
  16. skit_cli-0.2.0/src/skit/skills/skit/SKILL.md +143 -0
  17. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui.py +195 -30
  18. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui_add.py +130 -25
  19. skit_cli-0.2.0/src/skit/tui_footer.py +129 -0
  20. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui_form.py +53 -16
  21. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui_health.py +12 -9
  22. skit_cli-0.2.0/src/skit/tui_layout.py +50 -0
  23. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui_prefs.py +46 -13
  24. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tui_settings.py +18 -11
  25. skit_cli-0.0.1/src/skit/__init__.py +0 -3
  26. skit_cli-0.0.1/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
  27. skit_cli-0.0.1/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
  28. skit_cli-0.0.1/src/skit/tui_footer.py +0 -38
  29. {skit_cli-0.0.1 → skit_cli-0.2.0}/LICENSE +0 -0
  30. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/analyzer.py +0 -0
  31. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/argspec.py +0 -0
  32. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/argstate.py +0 -0
  33. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/editor.py +0 -0
  34. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/i18n.py +0 -0
  35. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/launcher.py +0 -0
  36. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/metawriter.py +0 -0
  37. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/models.py +0 -0
  38. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/paths.py +0 -0
  39. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/pep723.py +0 -0
  40. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/promptform.py +0 -0
  41. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/reconcile.py +0 -0
  42. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/shim.py +0 -0
  43. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/store.py +0 -0
  44. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/theme.py +0 -0
  45. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/tokens.py +0 -0
  46. {skit_cli-0.0.1 → skit_cli-0.2.0}/src/skit/uvman.py +0 -0
@@ -1,3 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: skit-cli
3
+ Version: 0.2.0
4
+ Summary: Script launcher and parameter manager: all your scripts in one menu, their flags, input() calls, and constants auto-extracted into a run form — no more opening an IDE just to tweak a value.
5
+ Keywords: script,launcher,uv,tui,pep723,script-manager,script-runner,automation,ai-agent,agent-skills
6
+ Author: Yi-Ting Chiu
7
+ Author-email: Yi-Ting Chiu <mytim710@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: Console :: Curses
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Classifier: Topic :: Utilities
21
+ Requires-Dist: typer>=0.26,<1
22
+ Requires-Dist: platformdirs>=4.10,<5
23
+ Requires-Dist: tomli-w>=1.2,<2
24
+ Requires-Dist: textual>=8.2,<9
25
+ Requires-Dist: rich>=15,<16
26
+ Requires-Python: >=3.12
27
+ Project-URL: Repository, https://github.com/t41372/skit
28
+ Project-URL: Issues, https://github.com/t41372/skit/issues
29
+ Description-Content-Type: text/markdown
30
+
1
31
  ![skit — script launcher and parameter manager](https://raw.githubusercontent.com/t41372/skit/main/docs/assets/banner.png)
2
32
 
3
33
  [![CI](https://github.com/t41372/skit/actions/workflows/ci.yml/badge.svg)](https://github.com/t41372/skit/actions/workflows/ci.yml)
@@ -17,7 +47,11 @@ skit stores your Python scripts in one place and makes them painless to launch.
17
47
 
18
48
  **AI writes the scripts. skit gives them a home.**
19
49
 
20
- <video src="https://github.com/user-attachments/assets/65c9eed8-d6fa-4a16-836c-7017eb08bb9c" controls></video>
50
+ And it's a home you share with your agent: the library you drive from a menu, AI agents
51
+ drive through a deterministic CLI — checking it before writing yet another one-off
52
+ script, and (with your OK) saving the good ones back, so they outlive the chat.
53
+
54
+ <video src="https://github.com/user-attachments/assets/d27121fe-5855-4270-91b0-b0ee9e5d11ee" controls></video>
21
55
 
22
56
  ## What it does
23
57
 
@@ -27,6 +61,7 @@ skit stores your Python scripts in one place and makes them painless to launch.
27
61
  - **No environment mess.** skit declares each script's dependencies in the script itself (PEP 723) and runs it through uv in an isolated, cached environment — no venvs to manage, nothing installed globally.
28
62
  - **Mouse or keyboard.** Plain `skit` opens the full TUI; every key hint on screen is also a clickable button.
29
63
  - **Automation-ready.** Every TUI action is also a CLI command with `--json` output and meaningful exit codes — for shell scripts, CI, and AI agents.
64
+ - **Your agent's script library too.** The official [Agent Skill](https://agentskills.io) teaches Claude Code, Codex, Cursor, Gemini CLI, and friends the whole drill: discover scripts with `skit list`, read a parameter schema with `skit show`, run with `skit run --set … --no-input`. One `skit agent install` away — see [Works with your AI agent](#works-with-your-ai-agent).
30
65
  - **Speaks your language.** English, 繁體中文, and 简体中文, with more to come. See [Languages](#languages).
31
66
 
32
67
  | Problem | What skit does |
@@ -34,6 +69,7 @@ skit stores your Python scripts in one place and makes them painless to launch.
34
69
  | Scripts scattered all over the place | One central menu, with search |
35
70
  | Scripts with weird external dependencies | An isolated environment per script — dependencies declared in the file (PEP 723), resolved by uv |
36
71
  | CLI flags you forget ten minutes later, `input()` prompts, hard-coded constants meant to be edited by hand | Static analysis extracts them all into an interactive form — no code changes. Last-used values come prefilled; favorites save as presets. |
72
+ | The weird script an AI wrote for you dies with the chat session | Agents check the library first, reuse what's there, and save the keepers — one-off scripts become permanent, parameterized tools |
37
73
 
38
74
  Nothing to set up per script — no refactoring, no config to maintain. The script an AI wrote last week and the one you barely remember from last year launch the same way.
39
75
 
@@ -72,6 +108,15 @@ uv tool install git+https://github.com/t41372/skit # latest development
72
108
  uvx --from git+https://github.com/t41372/skit skit --help # try it without installing
73
109
  ```
74
110
 
111
+ ## Update
112
+
113
+ ```bash
114
+ uv tool upgrade skit-cli # update to the latest release — also how you "check": it says up to date if you are
115
+ skit --version # the version you're on
116
+ ```
117
+
118
+ `uv tool upgrade` follows whatever source you installed from: PyPI installs track PyPI releases, `git+…` installs re-fetch the main branch.
119
+
75
120
  ## Uninstall
76
121
 
77
122
  ```bash
@@ -119,12 +164,29 @@ The rest of the CLI exists for automation and AI agents — every TUI action, sc
119
164
  ```bash
120
165
  skit run my_script -p fast # run with a saved preset
121
166
  skit run my_script --dry-run # print the exact command, don't run it
167
+ skit run my_script --set width=800 --no-input # set values explicitly, never prompt
168
+ skit show my_script --json # one script's full parameter schema, machine-readable
122
169
  skit params my_script # show managed parameters and last-used values
123
170
  skit list --json # machine-readable listing
124
171
  skit config # settings: language, editor, mirror, form style
125
172
  skit --help # everything else
126
173
  ```
127
174
 
175
+ ## Works with your AI agent
176
+
177
+ skit is a script repository for humans *and* AI agents: one library — you get the
178
+ forms, agents get a deterministic CLI. The official [Agent Skill](https://agentskills.io)
179
+ teaches compatible agents (Claude Code, Codex, Cursor, Gemini CLI, and many more) to
180
+ check your library before writing yet another one-off script, to inspect and run what's
181
+ already there, and to offer to save the useful scripts they write — so they outlive the
182
+ session that created them.
183
+
184
+ ```bash
185
+ skit agent install # pick one of the agent directories found on your machine
186
+ skit agent install claude # or name it: claude / codex / agents (--project for this repo only)
187
+ npx skills add t41372/skit # or install through skills.sh into 70+ agents
188
+ ```
189
+
128
190
  ## Languages
129
191
 
130
192
  | Language | Status |
@@ -1,33 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: skit-cli
3
- Version: 0.0.1
4
- Summary: Script launcher and parameter manager: pick a script, fill in a form, run — no more opening an IDE just to tweak constants.
5
- Keywords: script,launcher,uv,tui,pep723,script-manager
6
- Author: Yi-Ting Chiu
7
- Author-email: Yi-Ting Chiu <mytim710@gmail.com>
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Environment :: Console
12
- Classifier: Environment :: Console :: Curses
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Intended Audience :: End Users/Desktop
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Topic :: Software Development :: Build Tools
20
- Classifier: Topic :: Utilities
21
- Requires-Dist: typer>=0.26,<1
22
- Requires-Dist: platformdirs>=4.10,<5
23
- Requires-Dist: tomli-w>=1.2,<2
24
- Requires-Dist: textual>=8.2,<9
25
- Requires-Dist: rich>=15,<16
26
- Requires-Python: >=3.12
27
- Project-URL: Repository, https://github.com/t41372/skit
28
- Project-URL: Issues, https://github.com/t41372/skit/issues
29
- Description-Content-Type: text/markdown
30
-
31
1
  ![skit — script launcher and parameter manager](https://raw.githubusercontent.com/t41372/skit/main/docs/assets/banner.png)
32
2
 
33
3
  [![CI](https://github.com/t41372/skit/actions/workflows/ci.yml/badge.svg)](https://github.com/t41372/skit/actions/workflows/ci.yml)
@@ -47,7 +17,11 @@ skit stores your Python scripts in one place and makes them painless to launch.
47
17
 
48
18
  **AI writes the scripts. skit gives them a home.**
49
19
 
50
- <video src="https://github.com/user-attachments/assets/65c9eed8-d6fa-4a16-836c-7017eb08bb9c" controls></video>
20
+ And it's a home you share with your agent: the library you drive from a menu, AI agents
21
+ drive through a deterministic CLI — checking it before writing yet another one-off
22
+ script, and (with your OK) saving the good ones back, so they outlive the chat.
23
+
24
+ <video src="https://github.com/user-attachments/assets/d27121fe-5855-4270-91b0-b0ee9e5d11ee" controls></video>
51
25
 
52
26
  ## What it does
53
27
 
@@ -57,6 +31,7 @@ skit stores your Python scripts in one place and makes them painless to launch.
57
31
  - **No environment mess.** skit declares each script's dependencies in the script itself (PEP 723) and runs it through uv in an isolated, cached environment — no venvs to manage, nothing installed globally.
58
32
  - **Mouse or keyboard.** Plain `skit` opens the full TUI; every key hint on screen is also a clickable button.
59
33
  - **Automation-ready.** Every TUI action is also a CLI command with `--json` output and meaningful exit codes — for shell scripts, CI, and AI agents.
34
+ - **Your agent's script library too.** The official [Agent Skill](https://agentskills.io) teaches Claude Code, Codex, Cursor, Gemini CLI, and friends the whole drill: discover scripts with `skit list`, read a parameter schema with `skit show`, run with `skit run --set … --no-input`. One `skit agent install` away — see [Works with your AI agent](#works-with-your-ai-agent).
60
35
  - **Speaks your language.** English, 繁體中文, and 简体中文, with more to come. See [Languages](#languages).
61
36
 
62
37
  | Problem | What skit does |
@@ -64,6 +39,7 @@ skit stores your Python scripts in one place and makes them painless to launch.
64
39
  | Scripts scattered all over the place | One central menu, with search |
65
40
  | Scripts with weird external dependencies | An isolated environment per script — dependencies declared in the file (PEP 723), resolved by uv |
66
41
  | CLI flags you forget ten minutes later, `input()` prompts, hard-coded constants meant to be edited by hand | Static analysis extracts them all into an interactive form — no code changes. Last-used values come prefilled; favorites save as presets. |
42
+ | The weird script an AI wrote for you dies with the chat session | Agents check the library first, reuse what's there, and save the keepers — one-off scripts become permanent, parameterized tools |
67
43
 
68
44
  Nothing to set up per script — no refactoring, no config to maintain. The script an AI wrote last week and the one you barely remember from last year launch the same way.
69
45
 
@@ -102,6 +78,15 @@ uv tool install git+https://github.com/t41372/skit # latest development
102
78
  uvx --from git+https://github.com/t41372/skit skit --help # try it without installing
103
79
  ```
104
80
 
81
+ ## Update
82
+
83
+ ```bash
84
+ uv tool upgrade skit-cli # update to the latest release — also how you "check": it says up to date if you are
85
+ skit --version # the version you're on
86
+ ```
87
+
88
+ `uv tool upgrade` follows whatever source you installed from: PyPI installs track PyPI releases, `git+…` installs re-fetch the main branch.
89
+
105
90
  ## Uninstall
106
91
 
107
92
  ```bash
@@ -149,12 +134,29 @@ The rest of the CLI exists for automation and AI agents — every TUI action, sc
149
134
  ```bash
150
135
  skit run my_script -p fast # run with a saved preset
151
136
  skit run my_script --dry-run # print the exact command, don't run it
137
+ skit run my_script --set width=800 --no-input # set values explicitly, never prompt
138
+ skit show my_script --json # one script's full parameter schema, machine-readable
152
139
  skit params my_script # show managed parameters and last-used values
153
140
  skit list --json # machine-readable listing
154
141
  skit config # settings: language, editor, mirror, form style
155
142
  skit --help # everything else
156
143
  ```
157
144
 
145
+ ## Works with your AI agent
146
+
147
+ skit is a script repository for humans *and* AI agents: one library — you get the
148
+ forms, agents get a deterministic CLI. The official [Agent Skill](https://agentskills.io)
149
+ teaches compatible agents (Claude Code, Codex, Cursor, Gemini CLI, and many more) to
150
+ check your library before writing yet another one-off script, to inspect and run what's
151
+ already there, and to offer to save the useful scripts they write — so they outlive the
152
+ session that created them.
153
+
154
+ ```bash
155
+ skit agent install # pick one of the agent directories found on your machine
156
+ skit agent install claude # or name it: claude / codex / agents (--project for this repo only)
157
+ npx skills add t41372/skit # or install through skills.sh into 70+ agents
158
+ ```
159
+
158
160
  ## Languages
159
161
 
160
162
  | Language | Status |
@@ -2,14 +2,25 @@
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.0.1"
6
- description = "Script launcher and parameter manager: pick a script, fill in a form, run — no more opening an IDE just to tweak constants."
5
+ version = "0.2.0"
6
+ description = "Script launcher and parameter manager: all your scripts in one menu, their flags, input() calls, and constants auto-extracted into a run form — no more opening an IDE just to tweak a value."
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.12"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
11
11
  authors = [{ name = "Yi-Ting Chiu", email = "mytim710@gmail.com" }]
12
- keywords = ["script", "launcher", "uv", "tui", "pep723", "script-manager"]
12
+ keywords = [
13
+ "script",
14
+ "launcher",
15
+ "uv",
16
+ "tui",
17
+ "pep723",
18
+ "script-manager",
19
+ "script-runner",
20
+ "automation",
21
+ "ai-agent",
22
+ "agent-skills",
23
+ ]
13
24
  classifiers = [
14
25
  "Development Status :: 3 - Alpha",
15
26
  "Environment :: Console",
@@ -174,5 +185,9 @@ ignore = [
174
185
  "tests/corpus/**" = ["ALL"] # corpus files are verbatim analyzer inputs, not code style subjects
175
186
 
176
187
  [tool.mutmut]
177
- paths_to_mutate = ["src/skit/"]
178
- tests_dir = ["tests/"]
188
+ # Modern key names (paths_to_mutate / tests_dir print deprecation warnings on 3.6).
189
+ source_paths = ["src/skit/"]
190
+ pytest_add_cli_args_test_selection = ["tests/"]
191
+ # The skill-sync test (tests/test_agent_skill.py) compares the repo-root skill against
192
+ # the packaged copy; mutmut's mutants/ tree only carries src/ + tests/ by default.
193
+ also_copy = ["skills/"]
@@ -0,0 +1,20 @@
1
+ """skit — a launcher and parameter manager for your scripts."""
2
+
3
+ import os
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+ # Textual ≥ 8.2.7 enables the kitty keyboard protocol's "report all keys" mode, which
7
+ # iTerm2 (3.6.x) implements in a way that fights the macOS IME: candidate-selection
8
+ # digits and Enter reach the app as raw key events and the composed CJK text is never
9
+ # delivered at all (iTerm2 issue 12906) — Chinese/Japanese/Korean typing breaks. No
10
+ # skit binding needs the protocol, so opt out before the first textual import
11
+ # (textual.constants reads this at import time). setdefault keeps an explicit user
12
+ # override (=0 re-enables) winning.
13
+ os.environ.setdefault("TEXTUAL_DISABLE_KITTY_KEY", "1")
14
+
15
+ # pyproject.toml is the single source of the version; installed distributions carry it
16
+ # as metadata (a wheel doesn't ship pyproject.toml), so read it from there.
17
+ try:
18
+ __version__ = version("skit-cli")
19
+ except PackageNotFoundError: # a bare checkout on sys.path, no installed dist
20
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,99 @@
1
+ """The bundled Agent Skill and where it can be installed (headless; the CLI renders).
2
+
3
+ skit ships an official Agent Skill (skills/skit/SKILL.md, agentskills.io format) that
4
+ teaches AI coding agents to drive the library through the CLI. This module owns the
5
+ headless half of `skit agent install`: locating the bundled copy inside the installed
6
+ package and computing which agent skill directories exist on this machine.
7
+
8
+ Non-invasive by design (AGENTS.md principle #6): detection only reports directories
9
+ whose parent tool is already present (`~/.claude` exists, `./.codex` exists, …) — skit
10
+ never conjures another tool's config tree uninvited. Writing anywhere requires either
11
+ an explicit target/--to (consent by flag) or an interactive confirmation.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from importlib import resources
18
+ from pathlib import Path
19
+
20
+ SKILL_DIR_NAME = "skit"
21
+ SKILL_FILE_NAME = "SKILL.md"
22
+
23
+ # Known agent conventions: tool marker directory -> skills subdirectory lives inside it.
24
+ # claude: Claude Code (user ~/.claude/skills, project .claude/skills)
25
+ # codex: OpenAI Codex CLI (user ~/.codex/skills, project .codex/skills)
26
+ # agents: the cross-agent project convention (.agents/skills), project-only.
27
+ USER_TARGETS = {"claude": ".claude", "codex": ".codex"}
28
+ PROJECT_TARGETS = {"claude": ".claude", "codex": ".codex", "agents": ".agents"}
29
+
30
+
31
+ @dataclass
32
+ class Target:
33
+ """One place the skill can be installed: `<base>/skills/skit/SKILL.md`."""
34
+
35
+ name: str # "claude" | "codex" | "agents"
36
+ scope: str # "user" | "project"
37
+ base: Path # the tool's marker directory (e.g. ~/.claude)
38
+
39
+ @property
40
+ def skills_dir(self) -> Path:
41
+ return self.base / "skills"
42
+
43
+
44
+ def default_roots() -> tuple[Path, Path]:
45
+ """(home, cwd) for target resolution — one seam for the CLI and for tests. Tests
46
+ redirect this instead of monkeypatching os.getcwd, which mutmut's stats
47
+ bookkeeping also relies on mid-test."""
48
+ return Path.home(), Path.cwd()
49
+
50
+
51
+ def skill_text() -> str:
52
+ """The bundled SKILL.md, read from the installed package (single source: the repo's
53
+ skills/skit/SKILL.md is a test-enforced byte-identical copy of this file)."""
54
+ res = resources.files("skit").joinpath("skills", SKILL_DIR_NAME, SKILL_FILE_NAME)
55
+ # I/O kwarg mutants ("utf-8"→"UTF-8"/None) are equivalent aliases here; content is
56
+ # pinned by test_skill_text_is_the_bundled_skill (see docs/mutation-ledger.md).
57
+ return res.read_text(encoding="utf-8") # pragma: no mutate
58
+
59
+
60
+ def named_target(name: str, *, project: bool, home: Path, cwd: Path) -> Target | None:
61
+ """Resolve an explicit target name ("claude" / "codex" / "agents") to a Target.
62
+ None for an unknown name. `agents` is a project-level convention, so it resolves
63
+ to the project scope regardless of the --project flag."""
64
+ if name == "agents":
65
+ return Target(name=name, scope="project", base=cwd / PROJECT_TARGETS[name])
66
+ if project:
67
+ marker = PROJECT_TARGETS.get(name)
68
+ return Target(name=name, scope="project", base=cwd / marker) if marker else None
69
+ marker = USER_TARGETS.get(name)
70
+ return Target(name=name, scope="user", base=home / marker) if marker else None
71
+
72
+
73
+ def detect_targets(*, home: Path, cwd: Path) -> list[Target]:
74
+ """Every known target whose marker directory already exists — the tool is in use,
75
+ so offering to drop a skill inside it is not an intrusion. User scope first."""
76
+ found: list[Target] = []
77
+ for name, marker in USER_TARGETS.items():
78
+ base = home / marker
79
+ if base.is_dir():
80
+ found.append(Target(name=name, scope="user", base=base))
81
+ for name, marker in PROJECT_TARGETS.items():
82
+ base = cwd / marker
83
+ if base.is_dir():
84
+ found.append(Target(name=name, scope="project", base=base))
85
+ return found
86
+
87
+
88
+ def install_into(skills_dir: Path, text: str) -> Path:
89
+ """Write `text` as `<skills_dir>/skit/SKILL.md` (idempotent: rewriting is how an
90
+ upgrade lands). Returns the written file's path. Deliberately write-only — the
91
+ caller resolves the bundled copy via skill_text() first, so a broken installation
92
+ fails loudly there instead of masquerading as a destination write error here."""
93
+ dest = skills_dir / SKILL_DIR_NAME
94
+ dest.mkdir(parents=True, exist_ok=True)
95
+ out = dest / SKILL_FILE_NAME
96
+ # Same I/O kwarg equivalence; written content is pinned byte-for-byte by
97
+ # test_install_into_writes_and_upgrades (see docs/mutation-ledger.md).
98
+ out.write_text(text, encoding="utf-8") # pragma: no mutate
99
+ return out
@@ -7,6 +7,7 @@ import os
7
7
  import shutil
8
8
  import sys
9
9
  import tempfile
10
+ import time
10
11
  import tomllib
11
12
  from dataclasses import dataclass
12
13
  from pathlib import Path
@@ -14,6 +15,29 @@ from typing import Any
14
15
 
15
16
  import tomli_w
16
17
 
18
+ # Windows can't replace a file that another handle has open (sharing violation →
19
+ # PermissionError); concurrent readers of registry.toml hold it for microseconds, so a
20
+ # bounded exponential backoff is the standard idiom (total worst-case wait ≈ 1.3 s).
21
+ # POSIX replaces open files freely, so this path never fires there.
22
+ _REPLACE_RETRIES = 7 # sleeps: 0.01 · 0.02 · 0.04 · 0.08 · 0.16 · 0.32 · 0.64 s
23
+ _REPLACE_BACKOFF_START = 0.01
24
+
25
+
26
+ def _replace_with_retry(src: str, dst: Path) -> None:
27
+ """os.replace that rides out transient Windows sharing violations. After the
28
+ retries are exhausted, the final attempt's PermissionError propagates — a target
29
+ held open indefinitely (antivirus, an actual leak) must stay loud."""
30
+ delay = _REPLACE_BACKOFF_START
31
+ for _ in range(_REPLACE_RETRIES):
32
+ try:
33
+ os.replace(src, dst)
34
+ except PermissionError:
35
+ time.sleep(delay)
36
+ delay *= 2
37
+ else:
38
+ return
39
+ os.replace(src, dst)
40
+
17
41
 
18
42
  def _fsync_dir(dir_path: Path) -> None:
19
43
  """Fsync a directory fd so a prior os.replace()'s rename entry is durable on stable storage.
@@ -39,7 +63,7 @@ def atomic_write_bytes(path: Path, data: bytes) -> None:
39
63
  f.write(data)
40
64
  f.flush()
41
65
  os.fsync(f.fileno()) # durable on disk BEFORE the rename, not just before this returns
42
- os.replace(tmp, path)
66
+ _replace_with_retry(tmp, path)
43
67
  if sys.platform != "win32": # os.open can't open a directory on Windows
44
68
  with contextlib.suppress(OSError):
45
69
  _fsync_dir(path.parent) # best-effort: persist the rename's directory entry too