skill-automation-package 0.2.0 → 0.3.0
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.
- package/README.md +96 -33
- package/assets/.claude/skills/core-change-summary/SKILL.md +23 -0
- package/assets/.claude/skills/core-change-summary/skill.json +31 -0
- package/assets/.claude/skills/core-docs-entrypoint-guidance/SKILL.md +23 -0
- package/assets/.claude/skills/core-docs-entrypoint-guidance/skill.json +31 -0
- package/assets/.claude/skills/core-project-summary/SKILL.md +23 -0
- package/assets/.claude/skills/core-project-summary/skill.json +31 -0
- package/assets/.claude/skills/core-repo-structure-analysis/SKILL.md +23 -0
- package/assets/.claude/skills/core-repo-structure-analysis/skill.json +31 -0
- package/assets/.claude/tests/test_skill_agent.py +34 -0
- package/assets/.claude/tools/skill_agent.py +8 -1
- package/bin/skill-automation-package.js +40 -6
- package/package.json +12 -3
- package/scripts/install.py +237 -9
package/README.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Portable repo-local skill automation for Codex and Claude Code.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Install into the repository you are already in, then use the installed runtime immediately. The package is `npx`-first, but the installed runtime still uses Python 3.10+.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd your-repo
|
|
11
|
+
npx skill-automation-package install --target .
|
|
12
|
+
python3 .claude/tools/skill_agent.py auto "add tests" --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- no setup required beyond `npx` and Python 3.10+
|
|
16
|
+
- works on existing repositories
|
|
17
|
+
- safe: uses bounded managed blocks for generated guidance and ignore rules
|
|
18
|
+
|
|
19
|
+
The published npm package is `skill-automation-package`. It is a thin installer frontend over the same Python-based automation bundle; the runtime and install core still live in Python.
|
|
20
|
+
This remains a Python-installed automation bundle, not an npm runtime package.
|
|
6
21
|
|
|
7
22
|
## Why This Exists
|
|
8
23
|
|
|
@@ -17,39 +32,26 @@ After installation, an agent can:
|
|
|
17
32
|
- archive low-value skills that stay unused long enough to become cleanup candidates
|
|
18
33
|
- route future Codex and Claude Code sessions through the same workflow
|
|
19
34
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
- `.claude/tools/skill_agent.py`
|
|
23
|
-
- `.claude/skills/project-skill-router/`
|
|
24
|
-
- optional `.claude/tests/test_skill_agent.py`
|
|
25
|
-
- managed automation blocks for `AGENTS.md` and `CLAUDE.md`
|
|
26
|
-
|
|
27
|
-
## Quick Start
|
|
35
|
+
## Basic Flow
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
python3 scripts/install.py --target /path/to/target-repo
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Or use the npm wrapper entrypoint:
|
|
37
|
+
Use the published package to install into another repository:
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
40
|
npx skill-automation-package install --target /path/to/target-repo
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Update an existing target with version-aware reinstall behavior:
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
npx skill-automation-package update --target /path/to/target-repo
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
The npm entrypoint is a thin wrapper around the Python installer
|
|
48
|
-
|
|
49
|
+
Python 3.10 or newer is still required. The npm entrypoint is a thin wrapper around the Python installer and does not replace the Python core.
|
|
50
|
+
|
|
49
51
|
`install` always allows reinstall. `update` is version-aware and only reinstalls when the target reports an older installed version.
|
|
50
52
|
Before reinstalling, the wrapper checks `.claude/skill-automation-package.json` in the target repo and reports whether the target is not installed, already at the current version, or behind the current package version.
|
|
51
53
|
|
|
52
|
-
The installer copies the packaged assets, updates managed blocks in `AGENTS.md` and `CLAUDE.md` unless skipped, writes `.claude/skill-automation-package.json`, and refreshes `.claude/skills/registry.json`.
|
|
54
|
+
The installer copies the packaged assets, updates managed blocks in `AGENTS.md` and `CLAUDE.md` unless skipped, updates a generated-state `.gitignore` block unless disabled, writes `.claude/skill-automation-package.json`, and refreshes `.claude/skills/registry.json`.
|
|
53
55
|
|
|
54
56
|
Then, inside the target repository, start non-trivial work with:
|
|
55
57
|
|
|
@@ -63,6 +65,13 @@ If you want a preview before writing files:
|
|
|
63
65
|
python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json
|
|
64
66
|
```
|
|
65
67
|
|
|
68
|
+
## What It Installs
|
|
69
|
+
|
|
70
|
+
- `.claude/tools/skill_agent.py`
|
|
71
|
+
- five packaged core default skills under `.claude/skills/`: `project-skill-router/` plus read-only helpers for project summary, repo structure analysis, docs entrypoint guidance, and change summary
|
|
72
|
+
- optional `.claude/tests/test_skill_agent.py`
|
|
73
|
+
- managed automation blocks for `AGENTS.md` and `CLAUDE.md`
|
|
74
|
+
|
|
66
75
|
## Example Outcomes
|
|
67
76
|
|
|
68
77
|
Representative `auto` outcomes look like this.
|
|
@@ -96,35 +105,59 @@ Create a new skill when no strong match exists:
|
|
|
96
105
|
|
|
97
106
|
The exact skill name is inferred from the task, but the flow is stable: reuse when there is a strong match, otherwise scaffold a new reusable local skill and refresh the registry immediately.
|
|
98
107
|
|
|
99
|
-
## Installation
|
|
108
|
+
## Advanced Installation
|
|
100
109
|
|
|
101
|
-
Use this package when you
|
|
110
|
+
Use this package when you want to install the automation bundle into another repository. The published npm package is the default entrypoint, and the direct Python path remains available when you are working from a local checkout of this repository.
|
|
102
111
|
|
|
103
112
|
### Prerequisites
|
|
104
113
|
|
|
105
114
|
- Python 3.10 or newer
|
|
106
|
-
-
|
|
115
|
+
- Node.js 18 or newer
|
|
116
|
+
- npm or `npx` for the published package entrypoint
|
|
107
117
|
- a target repository where you want repo-local skill automation under `.claude/`
|
|
108
118
|
- write access to the target repository
|
|
109
119
|
|
|
110
120
|
### Standard Install
|
|
111
121
|
|
|
112
122
|
1. Choose the target repository.
|
|
113
|
-
2.
|
|
123
|
+
2. Use the published package:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx skill-automation-package install --target /path/to/target-repo
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Manual Installation
|
|
130
|
+
|
|
131
|
+
If you already have this repository checked out locally, the direct Python install path remains fully supported:
|
|
114
132
|
|
|
115
133
|
```bash
|
|
116
134
|
python3 scripts/install.py --target /path/to/target-repo
|
|
117
135
|
```
|
|
118
136
|
|
|
119
|
-
|
|
137
|
+
### What The Installer Writes
|
|
120
138
|
|
|
121
139
|
- copy `.claude/tools/skill_agent.py`
|
|
122
|
-
- copy `.claude/skills
|
|
140
|
+
- copy the packaged core default skills under `.claude/skills/`
|
|
123
141
|
- optionally copy `.claude/tests/test_skill_agent.py`
|
|
124
142
|
- insert managed automation blocks into `AGENTS.md` and `CLAUDE.md`
|
|
143
|
+
- insert a managed generated-state block into `.gitignore` unless disabled
|
|
125
144
|
- write `.claude/skill-automation-package.json`
|
|
126
145
|
- refresh `.claude/skills/registry.json`
|
|
127
146
|
|
|
147
|
+
### Install Vs Update
|
|
148
|
+
|
|
149
|
+
Install always runs and allows a deliberate reinstall:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npx skill-automation-package install --target /path/to/target-repo
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Update is version-aware and skips work when the target is already current:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx skill-automation-package update --target /path/to/target-repo
|
|
159
|
+
```
|
|
160
|
+
|
|
128
161
|
### Why `AGENTS.md` And `CLAUDE.md` Are Updated
|
|
129
162
|
|
|
130
163
|
The installer adds a bounded managed block so future Codex and Claude Code sessions start from the same routing command instead of bypassing the local skill system.
|
|
@@ -145,7 +178,7 @@ python3 .claude/tools/skill_agent.py list
|
|
|
145
178
|
python3 .claude/tools/skill_agent.py auto "find or create the right reusable workflow" --json
|
|
146
179
|
```
|
|
147
180
|
|
|
148
|
-
You should see the packaged
|
|
181
|
+
You should see the packaged core skills in the list, including the router, and `auto` should return either a reusable local skill match or a generated preview/result.
|
|
149
182
|
|
|
150
183
|
### Common Install Variants
|
|
151
184
|
|
|
@@ -153,17 +186,24 @@ You should see the packaged router skill in the list, and `auto` should return e
|
|
|
153
186
|
- Skip the packaged test file: `python3 scripts/install.py --target /path/to/target-repo --no-tests`
|
|
154
187
|
- Skip managed `AGENTS.md`: `python3 scripts/install.py --target /path/to/target-repo --skip-agents`
|
|
155
188
|
- Skip managed `CLAUDE.md`: `python3 scripts/install.py --target /path/to/target-repo --skip-claude`
|
|
189
|
+
- Disable `.gitignore` updates: `python3 scripts/install.py --target /path/to/target-repo --gitignore-mode none`
|
|
190
|
+
- Ignore the whole local-only install: `python3 scripts/install.py --target /path/to/target-repo --gitignore-mode local-only`
|
|
156
191
|
|
|
157
192
|
## Managed File Behavior
|
|
158
193
|
|
|
159
194
|
- If `AGENTS.md` or `CLAUDE.md` does not exist, install creates the file and inserts the managed block.
|
|
160
195
|
- If both package markers already exist, install replaces only the content inside that managed block.
|
|
161
196
|
- If the markers are missing, install appends the managed block to the end of the existing file.
|
|
197
|
+
- By default, install also creates or updates a managed `.gitignore` block for generated state only.
|
|
198
|
+
- Use `--gitignore-mode none` to leave `.gitignore` untouched, or `--gitignore-mode local-only` when the whole install should stay local to one checkout.
|
|
162
199
|
- `--dry-run` previews the install result without creating the target directory or writing package files, and its status lines use `Would ...` wording for changes that are only being previewed.
|
|
200
|
+
- Dry-run output includes package file groups for files that would be created, updated, left unchanged, or reported as previously installed but no longer shipped.
|
|
201
|
+
- Dry-run output also reports whether each managed guidance file would be created, replaced, appended, left unchanged, or skipped.
|
|
163
202
|
|
|
164
203
|
## Target Repo Git Hygiene
|
|
165
204
|
|
|
166
205
|
Decide up front whether the installed automation should be shared through version control or kept local to one checkout.
|
|
206
|
+
The default installer policy is shared-friendly: it adds only generated-state patterns to `.gitignore`.
|
|
167
207
|
|
|
168
208
|
### Shared Automation In Version Control
|
|
169
209
|
|
|
@@ -172,11 +212,11 @@ Use this when the repository wants shared repo-local skills and shared entrypoin
|
|
|
172
212
|
Usually commit:
|
|
173
213
|
|
|
174
214
|
- `.claude/tools/skill_agent.py`
|
|
175
|
-
- `.claude/skills
|
|
215
|
+
- the packaged core default skills under `.claude/skills/`
|
|
176
216
|
- `.claude/tests/test_skill_agent.py` when installed
|
|
177
217
|
- `AGENTS.md` and `CLAUDE.md` when you want the managed guidance blocks shared with the team
|
|
178
218
|
|
|
179
|
-
|
|
219
|
+
The installer adds this generated-state block by default:
|
|
180
220
|
|
|
181
221
|
```gitignore
|
|
182
222
|
.claude/skills/registry.json
|
|
@@ -197,10 +237,11 @@ Use this when the install is only for one developer checkout and should not affe
|
|
|
197
237
|
|
|
198
238
|
Typical approach:
|
|
199
239
|
|
|
200
|
-
- install with `--
|
|
240
|
+
- install with `--gitignore-mode local-only`
|
|
241
|
+
- add `--skip-agents --skip-claude` if you do not want top-level guidance files touched
|
|
201
242
|
- ignore the installed automation tree and any optional managed docs
|
|
202
243
|
|
|
203
|
-
|
|
244
|
+
The local-only mode manages this block:
|
|
204
245
|
|
|
205
246
|
```gitignore
|
|
206
247
|
.claude/
|
|
@@ -235,7 +276,17 @@ npx skill-automation-package update --target /path/to/target-repo
|
|
|
235
276
|
|
|
236
277
|
- `update` is a version-aware reinstall, not a partial update.
|
|
237
278
|
- `update` blocks implicit downgrade attempts; use `install` only when you intentionally want to replace the target with the current package version.
|
|
238
|
-
- If install metadata is malformed, `update` stops and asks you to use `install` for a deliberate reinstall.
|
|
279
|
+
- If install metadata is malformed, incomplete, for a different package, or missing a usable asset list, `update` stops and asks you to use `install` for a deliberate reinstall.
|
|
280
|
+
|
|
281
|
+
Metadata recovery behavior:
|
|
282
|
+
|
|
283
|
+
| State | `install` behavior | `update` behavior |
|
|
284
|
+
| --- | --- | --- |
|
|
285
|
+
| Missing metadata | Proceeds as a new install | Stops and asks for `install` |
|
|
286
|
+
| Same version | Reinstalls deliberately | No-ops |
|
|
287
|
+
| Older version | Reinstalls deliberately | Runs reinstall |
|
|
288
|
+
| Newer version | Warns and reinstalls deliberately | Blocks downgrade |
|
|
289
|
+
| Malformed or incomplete metadata | Warns and reinstalls deliberately | Stops and asks for `install` |
|
|
239
290
|
|
|
240
291
|
What gets updated in place:
|
|
241
292
|
|
|
@@ -342,10 +393,22 @@ Do not update `CLAUDE.md`:
|
|
|
342
393
|
python3 scripts/install.py --target /path/to/target-repo --skip-claude
|
|
343
394
|
```
|
|
344
395
|
|
|
396
|
+
Do not update `.gitignore`:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
python3 scripts/install.py --target /path/to/target-repo --gitignore-mode none
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Use a local-only `.gitignore` policy:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
python3 scripts/install.py --target /path/to/target-repo --gitignore-mode local-only
|
|
406
|
+
```
|
|
407
|
+
|
|
345
408
|
## Package Layout
|
|
346
409
|
|
|
347
410
|
- `assets/.claude/tools/skill_agent.py`: resolver, search, scaffold, usage tracking, refresh review/update, and prune CLI
|
|
348
|
-
- `assets/.claude/skills
|
|
411
|
+
- `assets/.claude/skills/`: packaged core default skills, including the router and four read-only helper skills
|
|
349
412
|
- `scripts/package_layout.py`: shared package manifest loader and asset copy helpers
|
|
350
413
|
- `templates/agents_block.md`: managed block for `AGENTS.md`
|
|
351
414
|
- `templates/claude_block.md`: managed block for `CLAUDE.md`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core-change-summary
|
|
3
|
+
description: Summarize the current working tree or recent changes in a read-only way. Use when an agent needs fast change context without editing the repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Core Change Summary
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Build a concise summary of what changed, where it changed, and what still looks important before deeper review or follow-up work.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Inspect the current working tree, changed files, and the smallest relevant diff or file set.
|
|
15
|
+
2. Group changes by area, intent, or likely effect instead of listing raw filenames only.
|
|
16
|
+
3. Call out obvious risks, open questions, or verification gaps that follow from the observed changes.
|
|
17
|
+
4. Keep the output read-only and limited to summary, not repo modification.
|
|
18
|
+
|
|
19
|
+
## Guardrails
|
|
20
|
+
|
|
21
|
+
- Do not create, edit, or delete repository files.
|
|
22
|
+
- Do not infer intent that is not supported by the observed changes.
|
|
23
|
+
- Prefer a compact status summary over a file-by-file changelog.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"category": "workflow",
|
|
3
|
+
"summary": "Summarize the current working tree or recent changes without editing the repository.",
|
|
4
|
+
"management_mode": "locked",
|
|
5
|
+
"tags": [
|
|
6
|
+
"changes",
|
|
7
|
+
"summary",
|
|
8
|
+
"read-only",
|
|
9
|
+
"review"
|
|
10
|
+
],
|
|
11
|
+
"triggers": [
|
|
12
|
+
"summarize the current changes",
|
|
13
|
+
"what changed in this repo",
|
|
14
|
+
"give me a change summary",
|
|
15
|
+
"show me the current working tree status"
|
|
16
|
+
],
|
|
17
|
+
"steps": [
|
|
18
|
+
"Inspect the current working tree, changed files, and the smallest relevant diff or file set.",
|
|
19
|
+
"Group changes by area, intent, or likely effect.",
|
|
20
|
+
"Call out obvious risks, open questions, or verification gaps from the observed changes.",
|
|
21
|
+
"Keep the result read-only and focused on summary."
|
|
22
|
+
],
|
|
23
|
+
"validation": [
|
|
24
|
+
"Reference the files or diffs used to build the summary.",
|
|
25
|
+
"Avoid modifying repository files while summarizing changes."
|
|
26
|
+
],
|
|
27
|
+
"examples": [
|
|
28
|
+
"Summarize the current working tree before I continue.",
|
|
29
|
+
"Explain the recent changes and any obvious follow-up risks."
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core-docs-entrypoint-guidance
|
|
3
|
+
description: Identify the best documentation entrypoints and reading order in a read-only way. Use when an agent needs to find canonical docs without changing the repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Core Docs Entrypoint Guidance
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Point an agent to the smallest set of documents worth reading first and distinguish active references from lower-priority material.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Inspect the root `README.md`, `docs/` tree, and any obvious operations or reference directories.
|
|
15
|
+
2. Separate canonical docs from working notes, archives, examples, or historical records.
|
|
16
|
+
3. Recommend a practical reading order for the current task.
|
|
17
|
+
4. Keep the output read-only and focused on navigation, not documentation edits.
|
|
18
|
+
|
|
19
|
+
## Guardrails
|
|
20
|
+
|
|
21
|
+
- Do not create, edit, or delete repository files.
|
|
22
|
+
- Do not treat archived or draft material as current truth unless the repo says so.
|
|
23
|
+
- Prefer a short ordered list of entrypoints over a broad document dump.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"category": "docs",
|
|
3
|
+
"summary": "Identify the best documentation entrypoints and reading order from the existing repo.",
|
|
4
|
+
"management_mode": "locked",
|
|
5
|
+
"tags": [
|
|
6
|
+
"docs",
|
|
7
|
+
"navigation",
|
|
8
|
+
"read-only",
|
|
9
|
+
"onboarding"
|
|
10
|
+
],
|
|
11
|
+
"triggers": [
|
|
12
|
+
"where should I start reading docs",
|
|
13
|
+
"find the canonical documentation",
|
|
14
|
+
"guide me through this repo's docs",
|
|
15
|
+
"what docs matter first"
|
|
16
|
+
],
|
|
17
|
+
"steps": [
|
|
18
|
+
"Inspect the root README and the main docs directories.",
|
|
19
|
+
"Separate canonical references from working notes or archives.",
|
|
20
|
+
"Recommend the smallest useful reading order for the current task.",
|
|
21
|
+
"Keep the result read-only and grounded in the existing doc structure."
|
|
22
|
+
],
|
|
23
|
+
"validation": [
|
|
24
|
+
"Call out which docs are active references and which are background material.",
|
|
25
|
+
"Avoid changing documentation files while guiding entrypoints."
|
|
26
|
+
],
|
|
27
|
+
"examples": [
|
|
28
|
+
"Show me which docs to read first in this repository.",
|
|
29
|
+
"Point me to the canonical docs before I start implementation."
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core-project-summary
|
|
3
|
+
description: Produce a read-only summary of the repository's purpose, main components, and current constraints. Use when an agent needs fast onboarding context without changing the repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Core Project Summary
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Build a short, source-grounded summary of what the repository does and what matters before deeper work.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Read the root `README.md`, top-level manifests, and the most relevant entry directories before summarizing.
|
|
15
|
+
2. Identify the repository purpose, main subsystems, runtime or build stack, and current constraints from existing files only.
|
|
16
|
+
3. Keep the output read-only: summarize what exists, cite the files you used, and do not create or modify repository files.
|
|
17
|
+
4. End with the smallest set of next files or directories worth reading for the current task.
|
|
18
|
+
|
|
19
|
+
## Guardrails
|
|
20
|
+
|
|
21
|
+
- Do not create, edit, or delete repository files.
|
|
22
|
+
- Do not invent architecture, roadmap, or ownership details that are not supported by source material.
|
|
23
|
+
- Prefer a short orientation summary over a long document.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"category": "docs",
|
|
3
|
+
"summary": "Summarize the repository purpose, main components, and current constraints from existing files.",
|
|
4
|
+
"management_mode": "locked",
|
|
5
|
+
"tags": [
|
|
6
|
+
"summary",
|
|
7
|
+
"onboarding",
|
|
8
|
+
"read-only",
|
|
9
|
+
"context"
|
|
10
|
+
],
|
|
11
|
+
"triggers": [
|
|
12
|
+
"summarize this project",
|
|
13
|
+
"give me a project overview",
|
|
14
|
+
"what does this repo do",
|
|
15
|
+
"onboard me to this repository"
|
|
16
|
+
],
|
|
17
|
+
"steps": [
|
|
18
|
+
"Read the root README, top-level manifests, and the most relevant entry directories.",
|
|
19
|
+
"Extract the repository purpose, main components, and current constraints from existing files only.",
|
|
20
|
+
"Keep the result read-only and grounded in the files you inspected.",
|
|
21
|
+
"Point to the next files or directories worth reading."
|
|
22
|
+
],
|
|
23
|
+
"validation": [
|
|
24
|
+
"Name the files or directories used to build the summary.",
|
|
25
|
+
"Avoid unsupported claims or repo changes."
|
|
26
|
+
],
|
|
27
|
+
"examples": [
|
|
28
|
+
"Summarize this repository before we start coding.",
|
|
29
|
+
"Give me a quick project overview and the most important files to read next."
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core-repo-structure-analysis
|
|
3
|
+
description: Map the repository layout, key entrypoints, and likely edit locations in a read-only way. Use when an agent needs to understand where code, docs, tests, or config live without changing the repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Core Repo Structure Analysis
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Create a compact map of the repository layout so an agent can find the right area before making changes.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Inspect the top-level directories, manifests, and obvious entrypoint files before drawing conclusions.
|
|
15
|
+
2. Group the repository into major areas such as application code, tests, docs, scripts, tooling, or generated assets.
|
|
16
|
+
3. Explain which directories are likely to matter for the current task and why.
|
|
17
|
+
4. Keep the analysis read-only and limit the output to navigation guidance rather than implementation advice.
|
|
18
|
+
|
|
19
|
+
## Guardrails
|
|
20
|
+
|
|
21
|
+
- Do not create, edit, or delete repository files.
|
|
22
|
+
- Do not claim a directory is authoritative unless the repository structure supports that conclusion.
|
|
23
|
+
- Prefer a practical map of likely edit locations over a full file inventory.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"category": "workflow",
|
|
3
|
+
"summary": "Map the repository layout, entrypoints, and likely edit locations without changing files.",
|
|
4
|
+
"management_mode": "locked",
|
|
5
|
+
"tags": [
|
|
6
|
+
"structure",
|
|
7
|
+
"navigation",
|
|
8
|
+
"read-only",
|
|
9
|
+
"codebase"
|
|
10
|
+
],
|
|
11
|
+
"triggers": [
|
|
12
|
+
"analyze this repo structure",
|
|
13
|
+
"map this codebase",
|
|
14
|
+
"where is everything in this repo",
|
|
15
|
+
"find the right directory for this task"
|
|
16
|
+
],
|
|
17
|
+
"steps": [
|
|
18
|
+
"Inspect the top-level directories, manifests, and obvious entrypoints.",
|
|
19
|
+
"Group the repository into major areas such as app code, tests, docs, and tooling.",
|
|
20
|
+
"Call out the directories or files most relevant to the current task.",
|
|
21
|
+
"Keep the result read-only and focused on navigation."
|
|
22
|
+
],
|
|
23
|
+
"validation": [
|
|
24
|
+
"Reference the directories or files that support the structure map.",
|
|
25
|
+
"Avoid changing the repository while analyzing it."
|
|
26
|
+
],
|
|
27
|
+
"examples": [
|
|
28
|
+
"Map this repository before I start editing files.",
|
|
29
|
+
"Show me where the main code, tests, docs, and tooling live."
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -452,6 +452,40 @@ class SkillAgentTests(unittest.TestCase):
|
|
|
452
452
|
self.assertEqual(payload[0]["name"], "dusty-skill")
|
|
453
453
|
self.assertEqual(payload[0]["status"], "candidate")
|
|
454
454
|
|
|
455
|
+
def test_usage_keeps_packaged_core_helper_protected(self) -> None:
|
|
456
|
+
old_timestamp = (datetime.now(UTC) - timedelta(days=60)).replace(microsecond=0).isoformat()
|
|
457
|
+
self.write_skill(
|
|
458
|
+
"core-project-summary",
|
|
459
|
+
description="Summarize the repository in a read-only way.",
|
|
460
|
+
metadata={
|
|
461
|
+
"category": "docs",
|
|
462
|
+
"summary": "Summarize the repository in a read-only way.",
|
|
463
|
+
"created_at": old_timestamp,
|
|
464
|
+
"updated_at": old_timestamp,
|
|
465
|
+
"management_mode": "locked",
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
result = subprocess.run(
|
|
470
|
+
[
|
|
471
|
+
sys.executable,
|
|
472
|
+
str(SCRIPT_PATH),
|
|
473
|
+
"usage",
|
|
474
|
+
"--repo-root",
|
|
475
|
+
str(self.repo_root),
|
|
476
|
+
"--status",
|
|
477
|
+
"protected",
|
|
478
|
+
"--json",
|
|
479
|
+
],
|
|
480
|
+
check=True,
|
|
481
|
+
capture_output=True,
|
|
482
|
+
text=True,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
payload = json.loads(result.stdout)
|
|
486
|
+
self.assertEqual(payload[0]["name"], "core-project-summary")
|
|
487
|
+
self.assertEqual(payload[0]["status"], "protected")
|
|
488
|
+
|
|
455
489
|
def test_prune_apply_archives_candidate_skill(self) -> None:
|
|
456
490
|
old_timestamp = (datetime.now(UTC) - timedelta(days=60)).replace(microsecond=0).isoformat()
|
|
457
491
|
self.write_skill(
|
|
@@ -335,7 +335,14 @@ TITLE_CASE_OVERRIDES = {
|
|
|
335
335
|
"xcode": "Xcode",
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
PROTECTED_SKILLS = {
|
|
338
|
+
PROTECTED_SKILLS = {
|
|
339
|
+
"project-skill-router",
|
|
340
|
+
"core-project-summary",
|
|
341
|
+
"core-repo-structure-analysis",
|
|
342
|
+
"core-docs-entrypoint-guidance",
|
|
343
|
+
"core-change-summary",
|
|
344
|
+
"omc-reference",
|
|
345
|
+
}
|
|
339
346
|
ARCHIVE_DIRNAME = "_archived"
|
|
340
347
|
USAGE_FILENAME = "usage.json"
|
|
341
348
|
USAGE_HISTORY_LIMIT = 12
|
|
@@ -7,6 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
|
|
9
9
|
const MIN_PYTHON = { major: 3, minor: 10 };
|
|
10
|
+
const PACKAGE_NAME = "skill-automation-package";
|
|
10
11
|
const INSTALL_METADATA_PATH = path.join(".claude", "skill-automation-package.json");
|
|
11
12
|
|
|
12
13
|
function main() {
|
|
@@ -145,10 +146,35 @@ function detectInstallState(targetRoot, currentVersion) {
|
|
|
145
146
|
try {
|
|
146
147
|
const raw = fs.readFileSync(metadataPath, "utf8");
|
|
147
148
|
const metadata = JSON.parse(raw);
|
|
149
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
150
|
+
return { status: "unknown", targetRoot, metadataPath, reason: "invalid-shape" };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (typeof metadata.name !== "string" || metadata.name.trim() === "") {
|
|
154
|
+
return { status: "unknown", targetRoot, metadataPath, reason: "missing-name" };
|
|
155
|
+
}
|
|
156
|
+
const installedName = metadata.name.trim();
|
|
157
|
+
if (installedName !== PACKAGE_NAME) {
|
|
158
|
+
return {
|
|
159
|
+
status: "unknown",
|
|
160
|
+
targetRoot,
|
|
161
|
+
metadataPath,
|
|
162
|
+
reason: "wrong-package",
|
|
163
|
+
installedName,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
if (typeof metadata.version !== "string" || metadata.version.trim() === "") {
|
|
149
168
|
return { status: "unknown", targetRoot, metadataPath, reason: "missing-version" };
|
|
150
169
|
}
|
|
151
170
|
|
|
171
|
+
if (!Array.isArray(metadata.assets)) {
|
|
172
|
+
return { status: "unknown", targetRoot, metadataPath, reason: "invalid-assets" };
|
|
173
|
+
}
|
|
174
|
+
if (!metadata.assets.every((asset) => typeof asset === "string" && asset.trim() !== "")) {
|
|
175
|
+
return { status: "unknown", targetRoot, metadataPath, reason: "invalid-assets" };
|
|
176
|
+
}
|
|
177
|
+
|
|
152
178
|
const installedVersion = metadata.version.trim();
|
|
153
179
|
const installedParsed = parseVersion(installedVersion);
|
|
154
180
|
const currentParsed = parseVersion(currentVersion);
|
|
@@ -252,12 +278,7 @@ function printLifecycleSafety() {
|
|
|
252
278
|
|
|
253
279
|
function describeUnknownMetadata(state, mode, currentVersion) {
|
|
254
280
|
const metadataPath = state?.metadataPath || INSTALL_METADATA_PATH;
|
|
255
|
-
const
|
|
256
|
-
"invalid-json": "could not be parsed as JSON",
|
|
257
|
-
"missing-version": "does not contain a usable version",
|
|
258
|
-
"invalid-version": "contains a version that could not be compared",
|
|
259
|
-
};
|
|
260
|
-
const detail = reasonLabel[state?.reason] || "could not be compared";
|
|
281
|
+
const detail = describeMetadataProblem(state);
|
|
261
282
|
|
|
262
283
|
if (mode === "install") {
|
|
263
284
|
return `skill-automation-package: existing install metadata at ${metadataPath} ${detail}; reinstalling with version ${currentVersion}.`;
|
|
@@ -266,6 +287,19 @@ function describeUnknownMetadata(state, mode, currentVersion) {
|
|
|
266
287
|
return `skill-automation-package: existing install metadata at ${metadataPath} ${detail}; use install to force a reinstall.`;
|
|
267
288
|
}
|
|
268
289
|
|
|
290
|
+
function describeMetadataProblem(state) {
|
|
291
|
+
const reasonLabel = {
|
|
292
|
+
"invalid-json": "could not be parsed as JSON",
|
|
293
|
+
"invalid-shape": "is not a JSON object",
|
|
294
|
+
"missing-name": "does not contain a usable package name",
|
|
295
|
+
"wrong-package": `belongs to package "${state?.installedName || "(unknown)"}" instead of ${PACKAGE_NAME}`,
|
|
296
|
+
"missing-version": "does not contain a usable version",
|
|
297
|
+
"invalid-version": "contains a version that could not be compared",
|
|
298
|
+
"invalid-assets": "does not contain a usable assets list",
|
|
299
|
+
};
|
|
300
|
+
return reasonLabel[state?.reason] || "could not be compared";
|
|
301
|
+
}
|
|
302
|
+
|
|
269
303
|
function printUsage(subcommand) {
|
|
270
304
|
if (subcommand) {
|
|
271
305
|
console.error(`skill-automation-package: unsupported subcommand "${subcommand}".`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-automation-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Portable repo-local skill automation bundle for Codex and Claude Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skill-automation-package": "bin/skill-automation-package.js"
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"scripts/package_layout.py",
|
|
12
12
|
"assets/.claude/tools/skill_agent.py",
|
|
13
13
|
"assets/.claude/skills/project-skill-router/",
|
|
14
|
+
"assets/.claude/skills/core-project-summary/",
|
|
15
|
+
"assets/.claude/skills/core-repo-structure-analysis/",
|
|
16
|
+
"assets/.claude/skills/core-docs-entrypoint-guidance/",
|
|
17
|
+
"assets/.claude/skills/core-change-summary/",
|
|
14
18
|
"assets/.claude/tests/test_skill_agent.py",
|
|
15
19
|
"templates/agents_block.md",
|
|
16
20
|
"templates/claude_block.md",
|
|
@@ -25,12 +29,17 @@
|
|
|
25
29
|
"test:wrapper": "node --test tests/node/wrapper.test.js",
|
|
26
30
|
"test:assets": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s assets/.claude/tests -p 'test_*.py'",
|
|
27
31
|
"install:dry-run": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/install.py --target /tmp/skill-automation-package-dry-run --dry-run",
|
|
32
|
+
"release:integrity": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/check_release_integrity.py",
|
|
28
33
|
"sync-assets": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/sync_assets.py",
|
|
29
|
-
"release:check": "npm run test && npm run test:assets && npm run test:wrapper && npm run install:dry-run && npm pack --dry-run --cache /tmp/skill-automation-package-npm-cache"
|
|
34
|
+
"release:check": "npm run test && npm run test:assets && npm run test:wrapper && npm run release:integrity && npm run install:dry-run && npm pack --dry-run --cache /tmp/skill-automation-package-npm-cache"
|
|
30
35
|
},
|
|
31
36
|
"managed_assets": [
|
|
32
37
|
".claude/tools/skill_agent.py",
|
|
33
|
-
".claude/skills/project-skill-router"
|
|
38
|
+
".claude/skills/project-skill-router",
|
|
39
|
+
".claude/skills/core-project-summary",
|
|
40
|
+
".claude/skills/core-repo-structure-analysis",
|
|
41
|
+
".claude/skills/core-docs-entrypoint-guidance",
|
|
42
|
+
".claude/skills/core-change-summary"
|
|
34
43
|
],
|
|
35
44
|
"optional_assets": [
|
|
36
45
|
".claude/tests/test_skill_agent.py"
|
package/scripts/install.py
CHANGED
|
@@ -5,6 +5,7 @@ import argparse
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ from package_layout import (
|
|
|
21
22
|
PackageLayout,
|
|
22
23
|
TEMPLATES_ROOT,
|
|
23
24
|
copy_assets,
|
|
25
|
+
iter_asset_files,
|
|
24
26
|
load_package_layout,
|
|
25
27
|
)
|
|
26
28
|
|
|
@@ -34,6 +36,44 @@ CLAUDE_MARKERS = (
|
|
|
34
36
|
"<!-- SKILL-AUTOMATION:CLAUDE:END -->",
|
|
35
37
|
)
|
|
36
38
|
|
|
39
|
+
GITIGNORE_MARKERS = (
|
|
40
|
+
"# SKILL-AUTOMATION:GITIGNORE:START",
|
|
41
|
+
"# SKILL-AUTOMATION:GITIGNORE:END",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
GITIGNORE_PATTERNS = {
|
|
45
|
+
"generated": (
|
|
46
|
+
"# Generated skill automation state",
|
|
47
|
+
".claude/skills/registry.json",
|
|
48
|
+
".claude/skills/usage.json",
|
|
49
|
+
".claude/skills/_archived/",
|
|
50
|
+
".claude/skill-automation-package.json",
|
|
51
|
+
".claude/**/__pycache__/",
|
|
52
|
+
".claude/**/*.pyc",
|
|
53
|
+
),
|
|
54
|
+
"local-only": (
|
|
55
|
+
"# Local-only skill automation install",
|
|
56
|
+
".claude/",
|
|
57
|
+
"AGENTS.md",
|
|
58
|
+
"CLAUDE.md",
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class ManagedBlockPlan:
|
|
65
|
+
changed: bool
|
|
66
|
+
state: str
|
|
67
|
+
updated: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class PackageFilePreview:
|
|
72
|
+
would_create: tuple[Path, ...]
|
|
73
|
+
would_update: tuple[Path, ...]
|
|
74
|
+
unchanged: tuple[Path, ...]
|
|
75
|
+
stale: tuple[Path, ...]
|
|
76
|
+
|
|
37
77
|
|
|
38
78
|
def main() -> int:
|
|
39
79
|
parser = build_parser()
|
|
@@ -52,11 +92,11 @@ def main() -> int:
|
|
|
52
92
|
executable_assets=layout.executable_assets,
|
|
53
93
|
dry_run=args.dry_run,
|
|
54
94
|
)
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
agents_plan = ManagedBlockPlan(changed=False, state="skipped", updated="")
|
|
96
|
+
claude_plan = ManagedBlockPlan(changed=False, state="skipped", updated="")
|
|
57
97
|
|
|
58
98
|
if not args.skip_agents:
|
|
59
|
-
|
|
99
|
+
agents_plan = install_managed_block(
|
|
60
100
|
target_file=target / "AGENTS.md",
|
|
61
101
|
template_path=TEMPLATES_ROOT / "agents_block.md",
|
|
62
102
|
markers=AGENTS_MARKERS,
|
|
@@ -65,7 +105,7 @@ def main() -> int:
|
|
|
65
105
|
)
|
|
66
106
|
|
|
67
107
|
if not args.skip_claude:
|
|
68
|
-
|
|
108
|
+
claude_plan = install_managed_block(
|
|
69
109
|
target_file=target / "CLAUDE.md",
|
|
70
110
|
template_path=TEMPLATES_ROOT / "claude_block.md",
|
|
71
111
|
markers=CLAUDE_MARKERS,
|
|
@@ -73,6 +113,12 @@ def main() -> int:
|
|
|
73
113
|
dry_run=args.dry_run,
|
|
74
114
|
)
|
|
75
115
|
|
|
116
|
+
gitignore_plan = install_gitignore_block(
|
|
117
|
+
target_file=target / ".gitignore",
|
|
118
|
+
mode=args.gitignore_mode,
|
|
119
|
+
dry_run=args.dry_run,
|
|
120
|
+
)
|
|
121
|
+
|
|
76
122
|
manifest_path = target / ".claude" / "skill-automation-package.json"
|
|
77
123
|
wrote_manifest = write_install_manifest(
|
|
78
124
|
manifest_path=manifest_path,
|
|
@@ -90,12 +136,26 @@ def main() -> int:
|
|
|
90
136
|
agents_label = "Would update AGENTS.md" if args.dry_run else "Updated AGENTS.md"
|
|
91
137
|
claude_label = "Would update CLAUDE.md" if args.dry_run else "Updated CLAUDE.md"
|
|
92
138
|
manifest_label = "Would write install manifest" if args.dry_run else "Wrote install manifest"
|
|
139
|
+
copied_label = "Would copy files" if args.dry_run else "Copied files"
|
|
140
|
+
gitignore_label = "Would update .gitignore" if args.dry_run else "Updated .gitignore"
|
|
93
141
|
print(f"Installed skill automation package {layout.version} into {target}")
|
|
94
|
-
print(f"
|
|
95
|
-
print(f"{agents_label}: {'yes' if
|
|
96
|
-
print(f"{claude_label}: {'yes' if
|
|
142
|
+
print(f"{copied_label}: {len(copied_files)}")
|
|
143
|
+
print(f"{agents_label}: {'yes' if agents_plan.changed else 'no'}")
|
|
144
|
+
print(f"{claude_label}: {'yes' if claude_plan.changed else 'no'}")
|
|
145
|
+
print(f"{gitignore_label}: {'yes' if gitignore_plan.changed else 'no'}")
|
|
97
146
|
print(f"{manifest_label}: {'yes' if wrote_manifest else 'no'}")
|
|
98
147
|
print(f"Refreshed registry: {'yes' if refreshed else 'no'}")
|
|
148
|
+
if args.dry_run:
|
|
149
|
+
print_dry_run_preview(
|
|
150
|
+
build_package_file_preview(
|
|
151
|
+
layout=layout,
|
|
152
|
+
target_root=target,
|
|
153
|
+
asset_paths=selected_assets,
|
|
154
|
+
),
|
|
155
|
+
agents_state=agents_plan.state,
|
|
156
|
+
claude_state=claude_plan.state,
|
|
157
|
+
gitignore_state=gitignore_plan.state,
|
|
158
|
+
)
|
|
99
159
|
return 0
|
|
100
160
|
|
|
101
161
|
|
|
@@ -129,6 +189,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
129
189
|
action="store_true",
|
|
130
190
|
help="Preview the installation without writing files.",
|
|
131
191
|
)
|
|
192
|
+
parser.add_argument(
|
|
193
|
+
"--gitignore-mode",
|
|
194
|
+
choices=("generated", "local-only", "none"),
|
|
195
|
+
default="generated",
|
|
196
|
+
help=(
|
|
197
|
+
"Manage a package-owned .gitignore block. `generated` ignores only "
|
|
198
|
+
"generated state, `local-only` ignores the whole local install, and "
|
|
199
|
+
"`none` leaves .gitignore untouched."
|
|
200
|
+
),
|
|
201
|
+
)
|
|
132
202
|
return parser
|
|
133
203
|
|
|
134
204
|
|
|
@@ -139,16 +209,85 @@ def install_managed_block(
|
|
|
139
209
|
markers: tuple[str, str],
|
|
140
210
|
title: str,
|
|
141
211
|
dry_run: bool,
|
|
142
|
-
) ->
|
|
212
|
+
) -> ManagedBlockPlan:
|
|
213
|
+
plan = build_managed_block_plan(
|
|
214
|
+
target_file=target_file,
|
|
215
|
+
template_path=template_path,
|
|
216
|
+
markers=markers,
|
|
217
|
+
title=title,
|
|
218
|
+
)
|
|
219
|
+
if not dry_run:
|
|
220
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
target_file.write_text(plan.updated, encoding="utf-8")
|
|
222
|
+
return plan
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build_managed_block_plan(
|
|
226
|
+
*,
|
|
227
|
+
target_file: Path,
|
|
228
|
+
template_path: Path,
|
|
229
|
+
markers: tuple[str, str],
|
|
230
|
+
title: str,
|
|
231
|
+
) -> ManagedBlockPlan:
|
|
143
232
|
block = template_path.read_text(encoding="utf-8").strip() + "\n"
|
|
144
233
|
start_marker, end_marker = markers
|
|
145
234
|
existing = target_file.read_text(encoding="utf-8") if target_file.exists() else ""
|
|
146
235
|
updated = upsert_block(existing, block, start_marker, end_marker, title)
|
|
236
|
+
state = classify_managed_block_state(existing, updated, start_marker, end_marker)
|
|
237
|
+
return ManagedBlockPlan(changed=updated != existing, state=state, updated=updated)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def install_gitignore_block(
|
|
241
|
+
*,
|
|
242
|
+
target_file: Path,
|
|
243
|
+
mode: str,
|
|
244
|
+
dry_run: bool,
|
|
245
|
+
) -> ManagedBlockPlan:
|
|
246
|
+
if mode == "none":
|
|
247
|
+
return ManagedBlockPlan(changed=False, state="skipped", updated="")
|
|
248
|
+
|
|
249
|
+
block = build_gitignore_block(mode)
|
|
250
|
+
existing = target_file.read_text(encoding="utf-8") if target_file.exists() else ""
|
|
251
|
+
updated = upsert_block(
|
|
252
|
+
existing,
|
|
253
|
+
block,
|
|
254
|
+
GITIGNORE_MARKERS[0],
|
|
255
|
+
GITIGNORE_MARKERS[1],
|
|
256
|
+
"",
|
|
257
|
+
)
|
|
258
|
+
state = classify_managed_block_state(
|
|
259
|
+
existing,
|
|
260
|
+
updated,
|
|
261
|
+
GITIGNORE_MARKERS[0],
|
|
262
|
+
GITIGNORE_MARKERS[1],
|
|
263
|
+
)
|
|
147
264
|
|
|
148
265
|
if not dry_run:
|
|
149
266
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
150
267
|
target_file.write_text(updated, encoding="utf-8")
|
|
151
|
-
return updated != existing
|
|
268
|
+
return ManagedBlockPlan(changed=updated != existing, state=state, updated=updated)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_gitignore_block(mode: str) -> str:
|
|
272
|
+
patterns = GITIGNORE_PATTERNS[mode]
|
|
273
|
+
return "\n".join([GITIGNORE_MARKERS[0], *patterns, GITIGNORE_MARKERS[1]]) + "\n"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def classify_managed_block_state(
|
|
277
|
+
existing: str,
|
|
278
|
+
updated: str,
|
|
279
|
+
start_marker: str,
|
|
280
|
+
end_marker: str,
|
|
281
|
+
) -> str:
|
|
282
|
+
if updated == existing:
|
|
283
|
+
return "unchanged"
|
|
284
|
+
if not existing.strip():
|
|
285
|
+
return "create"
|
|
286
|
+
start_index = existing.find(start_marker)
|
|
287
|
+
end_index = existing.find(end_marker)
|
|
288
|
+
if start_index != -1 and end_index != -1 and end_index >= start_index:
|
|
289
|
+
return "replace"
|
|
290
|
+
return "append"
|
|
152
291
|
|
|
153
292
|
|
|
154
293
|
def upsert_block(
|
|
@@ -204,5 +343,94 @@ def refresh_registry(target: Path) -> None:
|
|
|
204
343
|
)
|
|
205
344
|
|
|
206
345
|
|
|
346
|
+
def build_package_file_preview(
|
|
347
|
+
*,
|
|
348
|
+
layout: PackageLayout,
|
|
349
|
+
target_root: Path,
|
|
350
|
+
asset_paths: list[Path],
|
|
351
|
+
) -> PackageFilePreview:
|
|
352
|
+
current_assets: set[Path] = set()
|
|
353
|
+
would_create: list[Path] = []
|
|
354
|
+
would_update: list[Path] = []
|
|
355
|
+
unchanged: list[Path] = []
|
|
356
|
+
|
|
357
|
+
for source_path, relative_path in iter_asset_files(ASSETS_ROOT, asset_paths):
|
|
358
|
+
current_assets.add(relative_path)
|
|
359
|
+
target_path = target_root / relative_path
|
|
360
|
+
if not target_path.exists():
|
|
361
|
+
would_create.append(relative_path)
|
|
362
|
+
continue
|
|
363
|
+
if files_match(source_path, target_path):
|
|
364
|
+
unchanged.append(relative_path)
|
|
365
|
+
continue
|
|
366
|
+
would_update.append(relative_path)
|
|
367
|
+
|
|
368
|
+
stale = [
|
|
369
|
+
path
|
|
370
|
+
for path in read_previous_install_assets(target_root)
|
|
371
|
+
if path not in current_assets and (target_root / path).exists()
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
return PackageFilePreview(
|
|
375
|
+
would_create=tuple(sorted(would_create)),
|
|
376
|
+
would_update=tuple(sorted(would_update)),
|
|
377
|
+
unchanged=tuple(sorted(unchanged)),
|
|
378
|
+
stale=tuple(sorted(stale)),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def files_match(source_path: Path, target_path: Path) -> bool:
|
|
383
|
+
if not target_path.is_file():
|
|
384
|
+
return False
|
|
385
|
+
return source_path.read_bytes() == target_path.read_bytes()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def read_previous_install_assets(target_root: Path) -> list[Path]:
|
|
389
|
+
manifest_path = target_root / ".claude" / "skill-automation-package.json"
|
|
390
|
+
if not manifest_path.exists():
|
|
391
|
+
return []
|
|
392
|
+
try:
|
|
393
|
+
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
394
|
+
except json.JSONDecodeError:
|
|
395
|
+
return []
|
|
396
|
+
assets = payload.get("assets")
|
|
397
|
+
if not isinstance(assets, list):
|
|
398
|
+
return []
|
|
399
|
+
|
|
400
|
+
previous: list[Path] = []
|
|
401
|
+
for value in assets:
|
|
402
|
+
if not isinstance(value, str):
|
|
403
|
+
continue
|
|
404
|
+
path = Path(value)
|
|
405
|
+
if path.is_absolute():
|
|
406
|
+
continue
|
|
407
|
+
previous.append(path)
|
|
408
|
+
return previous
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def print_dry_run_preview(
|
|
412
|
+
preview: PackageFilePreview,
|
|
413
|
+
*,
|
|
414
|
+
agents_state: str,
|
|
415
|
+
claude_state: str,
|
|
416
|
+
gitignore_state: str,
|
|
417
|
+
) -> None:
|
|
418
|
+
print("Package file preview:")
|
|
419
|
+
print_preview_paths("would create", preview.would_create)
|
|
420
|
+
print_preview_paths("would update", preview.would_update)
|
|
421
|
+
print_preview_paths("unchanged", preview.unchanged)
|
|
422
|
+
print_preview_paths("previously installed but no longer shipped", preview.stale)
|
|
423
|
+
print("Managed file preview:")
|
|
424
|
+
print(f" AGENTS.md: {agents_state}")
|
|
425
|
+
print(f" CLAUDE.md: {claude_state}")
|
|
426
|
+
print(f" .gitignore: {gitignore_state}")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def print_preview_paths(label: str, paths: tuple[Path, ...]) -> None:
|
|
430
|
+
print(f" {label}: {len(paths)}")
|
|
431
|
+
for path in paths:
|
|
432
|
+
print(f" - {path}")
|
|
433
|
+
|
|
434
|
+
|
|
207
435
|
if __name__ == "__main__":
|
|
208
436
|
raise SystemExit(main())
|