skit-cli 0.1.0__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.
- skit_cli-0.1.0/README.md → skit_cli-0.2.0/PKG-INFO +54 -1
- skit_cli-0.1.0/PKG-INFO → skit_cli-0.2.0/README.md +24 -31
- {skit_cli-0.1.0 → skit_cli-0.2.0}/pyproject.toml +20 -5
- skit_cli-0.2.0/src/skit/agentskill.py +99 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/atomic.py +25 -1
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/cli.py +355 -6
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/inlineform.py +6 -1
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/locales/skit.pot +349 -213
- skit_cli-0.2.0/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/locales/zh_CN/LC_MESSAGES/skit.po +613 -287
- skit_cli-0.2.0/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/locales/zh_TW/LC_MESSAGES/skit.po +613 -287
- skit_cli-0.2.0/src/skit/skills/skit/SKILL.md +143 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui.py +94 -24
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui_add.py +36 -23
- skit_cli-0.2.0/src/skit/tui_footer.py +129 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui_form.py +47 -15
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui_health.py +12 -9
- skit_cli-0.2.0/src/skit/tui_layout.py +50 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui_prefs.py +18 -10
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tui_settings.py +14 -11
- skit_cli-0.1.0/src/skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
- skit_cli-0.1.0/src/skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
- skit_cli-0.1.0/src/skit/tui_footer.py +0 -72
- {skit_cli-0.1.0 → skit_cli-0.2.0}/LICENSE +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/__init__.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/analyzer.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/argspec.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/argstate.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/config.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/editor.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/flows.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/i18n.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/launcher.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/metawriter.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/models.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/paths.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/pep723.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/promptform.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/reconcile.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/shim.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/store.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/theme.py +0 -0
- {skit_cli-0.1.0 → skit_cli-0.2.0}/src/skit/tokens.py +0 -0
- {skit_cli-0.1.0 → 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
|

|
|
2
32
|
|
|
3
33
|
[](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
|
-
|
|
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
|
|
|
@@ -128,12 +164,29 @@ The rest of the CLI exists for automation and AI agents — every TUI action, sc
|
|
|
128
164
|
```bash
|
|
129
165
|
skit run my_script -p fast # run with a saved preset
|
|
130
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
|
|
131
169
|
skit params my_script # show managed parameters and last-used values
|
|
132
170
|
skit list --json # machine-readable listing
|
|
133
171
|
skit config # settings: language, editor, mirror, form style
|
|
134
172
|
skit --help # everything else
|
|
135
173
|
```
|
|
136
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
|
+
|
|
137
190
|
## Languages
|
|
138
191
|
|
|
139
192
|
| Language | Status |
|
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: skit-cli
|
|
3
|
-
Version: 0.1.0
|
|
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
|

|
|
32
2
|
|
|
33
3
|
[](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
|
-
|
|
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
|
|
|
@@ -158,12 +134,29 @@ The rest of the CLI exists for automation and AI agents — every TUI action, sc
|
|
|
158
134
|
```bash
|
|
159
135
|
skit run my_script -p fast # run with a saved preset
|
|
160
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
|
|
161
139
|
skit params my_script # show managed parameters and last-used values
|
|
162
140
|
skit list --json # machine-readable listing
|
|
163
141
|
skit config # settings: language, editor, mirror, form style
|
|
164
142
|
skit --help # everything else
|
|
165
143
|
```
|
|
166
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
|
+
|
|
167
160
|
## Languages
|
|
168
161
|
|
|
169
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.
|
|
6
|
-
description = "Script launcher and parameter manager:
|
|
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 = [
|
|
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
|
|
178
|
-
|
|
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,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
|
-
|
|
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
|