@event4u/agent-config 1.41.1 → 2.0.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/.agent-src/templates/agents/agent-project-settings.example.yml +14 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +120 -11
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +34 -25
- package/config/agent-settings.template.yml +25 -0
- package/docs/architecture.md +46 -0
- package/docs/customization.md +125 -9
- package/docs/installation.md +9 -36
- package/docs/migration/v1-to-v2.md +98 -0
- package/docs/setup/per-ide/claude-code.md +0 -17
- package/docs/setup/per-ide/claude-desktop.md +35 -48
- package/docs/setup/per-ide/windsurf.md +0 -11
- package/package.json +1 -6
- package/scripts/_cli/__init__.py +0 -0
- package/scripts/_cli/cmd_migrate.py +270 -0
- package/scripts/_cli/cmd_update.py +226 -0
- package/scripts/_lib/agent_settings.py +120 -11
- package/scripts/_lib/agents_overlay.py +109 -0
- package/scripts/_lib/pin_resolver.py +152 -0
- package/scripts/_lib/update_check.py +183 -0
- package/scripts/agent-config +73 -1
- package/scripts/check_overlay_cascade_subdirs.py +125 -0
- package/scripts/check_template_pin_drift.py +112 -0
- package/scripts/check_update_banner.py +86 -0
- package/scripts/install +2 -37
- package/scripts/install.py +6 -207
- package/bin/install.php +0 -45
- package/composer.json +0 -33
- package/scripts/postinstall.sh +0 -76
- package/templates/global-install-manifest.yml +0 -91
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Migration — v1 → v2 (npx-only runtime)
|
|
2
|
+
|
|
3
|
+
> **Status:** skeleton. The one-shot `npx @event4u/agent-config migrate`
|
|
4
|
+
> is implemented in P3.5 of
|
|
5
|
+
> [`road-to-portable-runtime-and-update-check`](../../agents/roadmaps/road-to-portable-runtime-and-update-check.md);
|
|
6
|
+
> this document tracks the user-facing cutover contract so consumers can
|
|
7
|
+
> rehearse the change before the command lands.
|
|
8
|
+
|
|
9
|
+
## Why this change
|
|
10
|
+
|
|
11
|
+
v2 retires the local-install scheme (Composer dependency, npm
|
|
12
|
+
`postinstall`, `--global` symlink namespace under `~/.claude/`,
|
|
13
|
+
`~/.cursor/`, `~/.codeium/windsurf/`, `~/.config/agent-config/`) in
|
|
14
|
+
favour of an **npx-only runtime**. The trade-off and the council's Q1
|
|
15
|
+
rejection + the user's override are recorded in
|
|
16
|
+
[`docs/architecture.md` § Distribution model](../architecture.md#distribution-model--npx-only--version-pin-governance).
|
|
17
|
+
|
|
18
|
+
## TL;DR
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @event4u/agent-config migrate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
One command, idempotent. Re-runs on an already-migrated repo do nothing.
|
|
25
|
+
|
|
26
|
+
## What disappears from the consumer
|
|
27
|
+
|
|
28
|
+
| Path / entry | Reason |
|
|
29
|
+
|-------------------------------------------------------|-----------------------------------------|
|
|
30
|
+
| `composer.json` → `require-dev.event4u/agent-config` | No Composer dependency under v2. |
|
|
31
|
+
| `composer.lock` line for `event4u/agent-config` | Same; lockfile updated by Composer. |
|
|
32
|
+
| `package.json` → `devDependencies.@event4u/agent-config` | No npm dependency under v2. |
|
|
33
|
+
| `package-lock.json` / `pnpm-lock.yaml` entries | Same. |
|
|
34
|
+
| `node_modules/@event4u/agent-config/` | Removed by the package manager once the `package.json` entry is gone. |
|
|
35
|
+
| `vendor/event4u/agent-config/` | Removed by Composer once the require entry is gone. |
|
|
36
|
+
| `~/.claude/{rules,skills}/event4u/` | Retired `--global` namespace dir. |
|
|
37
|
+
| `~/.cursor/rules/imported/event4u/` | Same. |
|
|
38
|
+
| `~/.codeium/windsurf/global_workflows/event4u/` | Same. |
|
|
39
|
+
| `~/.config/agent-config/{rules,skills}/event4u/` | Same (fallback path). |
|
|
40
|
+
| Legacy `.gitignore` block lines marked `event4u/agent-config (legacy local install)` | Replaced by the v2 block written by `sync-gitignore`. |
|
|
41
|
+
|
|
42
|
+
The retired `templates/global-install-manifest.yml` shipped inside the
|
|
43
|
+
package and is gone in v2; consumers never carried it directly.
|
|
44
|
+
|
|
45
|
+
## What appears in the consumer
|
|
46
|
+
|
|
47
|
+
| Path / entry | Owner / shape |
|
|
48
|
+
|-------------------------------------------------------|-----------------------------------------|
|
|
49
|
+
| `.agent-settings.yml` → `agent_config_version: "<pin>"` | Project version pin, reviewed in PRs. |
|
|
50
|
+
| `.agent-settings.yml` → `update_check:` block | Defaults shipped by `init`; opt-out per knob. |
|
|
51
|
+
| Updated `.gitignore` block | v2 entries written by `sync-gitignore`. |
|
|
52
|
+
|
|
53
|
+
The per-tool glue (`.claude/`, `.cursor/`, `.clinerules/`,
|
|
54
|
+
`.windsurfrules`, `GEMINI.md`, `.github/copilot-instructions.md`,
|
|
55
|
+
`.augment/`, `.vscode/settings.json`) keeps the same shape as v1 — only
|
|
56
|
+
the source that writes them changed (from `vendor/` /
|
|
57
|
+
`node_modules/` scripts to the npx-resolved runtime).
|
|
58
|
+
|
|
59
|
+
## The `migrate` command — contract sketch
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx @event4u/agent-config migrate # interactive, default
|
|
63
|
+
npx @event4u/agent-config migrate --dry-run # plan only, no writes
|
|
64
|
+
npx @event4u/agent-config migrate --yes # non-interactive
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Order of operations (locked once P3.5 lands):
|
|
68
|
+
|
|
69
|
+
1. Detect pre-v2 markers: `composer.json` require entry,
|
|
70
|
+
`package.json` devDependency, `vendor/event4u/agent-config/`,
|
|
71
|
+
`node_modules/@event4u/agent-config/`, legacy `.gitignore` lines,
|
|
72
|
+
`~/.claude/{rules,skills}/event4u/` and siblings.
|
|
73
|
+
2. Print the planned change set (file removals, file writes, pin
|
|
74
|
+
value). Stop here under `--dry-run`.
|
|
75
|
+
3. Remove dependency entries via the appropriate package manager
|
|
76
|
+
(`composer remove`, `npm uninstall` / `pnpm remove` / `yarn remove`).
|
|
77
|
+
4. Wipe the retired user-scope `event4u/` namespace dirs.
|
|
78
|
+
5. Write / update `.agent-settings.yml` with the new shape +
|
|
79
|
+
`agent_config_version` pin.
|
|
80
|
+
6. Re-run `sync-gitignore` to refresh the project `.gitignore` block.
|
|
81
|
+
7. Print a one-screen post-migration verification list.
|
|
82
|
+
|
|
83
|
+
Idempotency: each step is a no-op when the v1 marker is absent. Re-runs
|
|
84
|
+
print *"already on v2 — nothing to do"* and exit 0.
|
|
85
|
+
|
|
86
|
+
## Verification after migration
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx @event4u/agent-config doctor # P3 — runtime sanity check
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Expected: pin resolved, no v1 markers detected, `update_check` reachable.
|
|
93
|
+
|
|
94
|
+
## See also
|
|
95
|
+
|
|
96
|
+
- [`docs/architecture.md` § Distribution model](../architecture.md#distribution-model--npx-only--version-pin-governance) — Q1 council rejection + override + pin substitution.
|
|
97
|
+
- [`agents/roadmaps/road-to-portable-runtime-and-update-check.md`](../../agents/roadmaps/road-to-portable-runtime-and-update-check.md) — full delivery plan and acceptance criteria.
|
|
98
|
+
- [`docs/installation.md`](../installation.md) — v2 install reference.
|
|
@@ -29,23 +29,6 @@ Either form populates:
|
|
|
29
29
|
- `CLAUDE.md` — agent root pointer (auto-loaded by Claude Code)
|
|
30
30
|
- `.agent-settings.yml` — your per-project knobs (kept out of git)
|
|
31
31
|
|
|
32
|
-
## Global install (cross-project skills)
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx @event4u/agent-config global --tools=claude-code
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Seeds `~/.claude/skills/` with the curated top-N skills from
|
|
39
|
-
[`templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml).
|
|
40
|
-
Available across every project on the machine; project-level files
|
|
41
|
-
always take precedence.
|
|
42
|
-
|
|
43
|
-
Uninstall:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx @event4u/agent-config global --uninstall
|
|
47
|
-
```
|
|
48
|
-
|
|
49
32
|
## Plugin marketplace (Claude Code 2026+)
|
|
50
33
|
|
|
51
34
|
Claude Code 2026 supports plugin marketplaces via
|
|
@@ -3,56 +3,46 @@
|
|
|
3
3
|
The fastest path to running our skills, rules, and (optionally) the MCP
|
|
4
4
|
server inside Claude Desktop. macOS / Windows / Linux. ~5 minutes.
|
|
5
5
|
|
|
6
|
-
> **TL;DR** —
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
6
|
+
> **TL;DR** — run `npx @event4u/agent-config init --tools=claude-code`
|
|
7
|
+
> inside each project that should expose the skills/rules to Claude
|
|
8
|
+
> Desktop. The package now ships as an npx-resolved runtime; the
|
|
9
|
+
> retired `--global` symlink scheme has been removed.
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
13
|
- Claude Desktop installed (free or paid plan — same install path).
|
|
14
|
-
- Node ≥ 18
|
|
15
|
-
(either route can run `--global`).
|
|
14
|
+
- Node ≥ 18 (`npx` resolves the package per-project).
|
|
16
15
|
- 5 minutes.
|
|
17
16
|
|
|
18
|
-
## Step 1 —
|
|
17
|
+
## Step 1 — project-local install
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
files under `~/.claude/`.
|
|
19
|
+
Run inside each project that should be visible to Claude Desktop:
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
|
|
25
|
-
npx @event4u/create-agent-config --global --tools=claude-code
|
|
26
|
-
|
|
27
|
-
# Or via curl (no Node).
|
|
28
|
-
curl -fsSL https://raw.githubusercontent.com/event4u/agent-config/main/setup.sh \
|
|
29
|
-
| bash -s -- --global --tools=claude-code
|
|
30
|
-
|
|
31
|
-
# Or from a local clone.
|
|
32
|
-
bash scripts/install --global --tools=claude-code
|
|
22
|
+
npx @event4u/agent-config init --tools=claude-code
|
|
33
23
|
```
|
|
34
24
|
|
|
35
25
|
> `--tools=claude-code` covers both Claude Code **and** Claude
|
|
36
|
-
> Desktop — the two surfaces share
|
|
37
|
-
> `--tools=claude-code,cursor,windsurf`
|
|
38
|
-
>
|
|
26
|
+
> Desktop — the two surfaces share the project's `.claude/`
|
|
27
|
+
> directory. Pass `--tools=claude-code,cursor,windsurf` to seed
|
|
28
|
+
> additional surfaces in the same run.
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
The init writes:
|
|
41
31
|
|
|
42
32
|
```
|
|
43
|
-
|
|
44
|
-
├── rules/
|
|
45
|
-
|
|
33
|
+
.claude/
|
|
34
|
+
├── rules/ # active rules for the project
|
|
35
|
+
├── skills/ # active skills for the project
|
|
36
|
+
└── commands/ # slash commands
|
|
46
37
|
```
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Edit and re-run `--global` to grow or shrink the set.
|
|
39
|
+
`.agent-settings.yml` carries the `agent_config_version` pin so every
|
|
40
|
+
`npx` invocation resolves the same runtime.
|
|
51
41
|
|
|
52
42
|
## Step 2 — verify
|
|
53
43
|
|
|
54
44
|
1. Restart Claude Desktop (full quit, not just window close).
|
|
55
|
-
2. Open a new conversation.
|
|
45
|
+
2. Open the project folder in a new conversation.
|
|
56
46
|
3. Type `/` — the curated skills (`/work`, `/commit`, `/create-pr`,
|
|
57
47
|
`/quality-fix`, `/review-changes`, `/agent-handoff`,
|
|
58
48
|
`/project-analyze`, …) appear in the slash-command menu.
|
|
@@ -61,7 +51,8 @@ Edit and re-run `--global` to grow or shrink the set.
|
|
|
61
51
|
|
|
62
52
|
If the menu is empty:
|
|
63
53
|
|
|
64
|
-
- Check `ls
|
|
54
|
+
- Check `ls .claude/skills/` inside the project — should list the
|
|
55
|
+
curated skills.
|
|
65
56
|
- Quit Claude Desktop (`Cmd+Q` on macOS, **not** just close the
|
|
66
57
|
window — the menubar process keeps the old skills cached).
|
|
67
58
|
- Re-open and try `/` again.
|
|
@@ -123,22 +114,23 @@ native HTTP) and per-client Bearer-auth snippets live in
|
|
|
123
114
|
|
|
124
115
|
## Claude Desktop ↔ Claude Code config sharing
|
|
125
116
|
|
|
126
|
-
Both surfaces read **the same
|
|
127
|
-
|
|
117
|
+
Both surfaces read **the same project `.claude/` directory**. Anything
|
|
118
|
+
the `npx … init` writes for one is automatically picked up by the
|
|
119
|
+
other when the project folder is opened:
|
|
128
120
|
|
|
129
121
|
| File / dir | Shared by Desktop & Code? |
|
|
130
122
|
| -------------------------------- | ------------------------- |
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
123
|
+
| `<project>/.claude/CLAUDE.md` | yes — project system prompt |
|
|
124
|
+
| `<project>/.claude/rules/` | yes — written by `npx … init` |
|
|
125
|
+
| `<project>/.claude/skills/` | yes — written by `npx … init` |
|
|
126
|
+
| `<project>/.claude/commands/` | yes — slash commands |
|
|
127
|
+
| `<project>/.claude/hooks/` | yes — lifecycle hooks |
|
|
136
128
|
| `claude_desktop_config.json` | Desktop only (MCP) |
|
|
137
129
|
| `~/.claude.json` (CLI config) | Code only |
|
|
138
130
|
|
|
139
|
-
Translation: run
|
|
140
|
-
|
|
141
|
-
view.
|
|
131
|
+
Translation: run `npx @event4u/agent-config init` once per project,
|
|
132
|
+
both clients pick the files up. Cross-link to
|
|
133
|
+
[`claude-code.md`](claude-code.md) for the CLI-side view.
|
|
142
134
|
|
|
143
135
|
## Claude Cowork
|
|
144
136
|
|
|
@@ -158,16 +150,11 @@ client-side feature set.
|
|
|
158
150
|
|
|
159
151
|
## Uninstall
|
|
160
152
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Removes only `~/.claude/{rules,skills}/event4u/`. Anything you added
|
|
166
|
-
under sibling paths (custom rules, your own slash commands) stays.
|
|
153
|
+
Remove the project's `.claude/`, `.agent-settings.yml`, and any bridge
|
|
154
|
+
files written by `npx … init`. Nothing lives under `~/.claude/` from
|
|
155
|
+
this package any more.
|
|
167
156
|
|
|
168
157
|
## See also
|
|
169
158
|
|
|
170
159
|
- Project-local install — [`../../installation.md`](../../installation.md)
|
|
171
|
-
- Global install reference — [`../../installation.md#global-user-level-install---global`](../../installation.md#global-user-level-install---global)
|
|
172
160
|
- MCP client transports — [`../mcp-client-config.md`](../mcp-client-config.md)
|
|
173
|
-
- Curation manifest — [`../../../templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml)
|
|
@@ -35,17 +35,6 @@ Combine with other surfaces:
|
|
|
35
35
|
npx @event4u/create-agent-config init --tools=windsurf,claude-code,cursor
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
## Global install
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npx @event4u/agent-config global --tools=windsurf
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Seeds `~/.codeium/windsurf/global_workflows/` with the curated
|
|
45
|
-
workflow set (see [`templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml)).
|
|
46
|
-
Available across every project; per-workspace `.windsurf/workflows/`
|
|
47
|
-
takes precedence on slug collisions.
|
|
48
|
-
|
|
49
38
|
## Wave-8 frontmatter
|
|
50
39
|
|
|
51
40
|
Each rule under `.windsurf/rules/` has the Windsurf-shaped header:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
".agent-src/",
|
|
17
17
|
".augment-plugin/",
|
|
18
18
|
".claude-plugin/",
|
|
19
|
-
"bin/",
|
|
20
19
|
"config/",
|
|
21
20
|
"docs/",
|
|
22
21
|
"scripts/",
|
|
@@ -26,15 +25,11 @@
|
|
|
26
25
|
"CONTRIBUTING.md",
|
|
27
26
|
"LICENSE",
|
|
28
27
|
"README.md",
|
|
29
|
-
"composer.json",
|
|
30
28
|
"llms.txt"
|
|
31
29
|
],
|
|
32
30
|
"bin": {
|
|
33
31
|
"agent-config": "scripts/agent-config"
|
|
34
32
|
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"postinstall": "bash scripts/postinstall.sh"
|
|
37
|
-
},
|
|
38
33
|
"publishConfig": {
|
|
39
34
|
"access": "public",
|
|
40
35
|
"provenance": true
|
|
File without changes
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""``agent-config migrate`` — one-shot migration off legacy install paths.
|
|
2
|
+
|
|
3
|
+
P3.5/P3.6 of road-to-portable-runtime-and-update-check.md. Migrates a
|
|
4
|
+
consumer project from the legacy composer / npm install paths onto
|
|
5
|
+
the ``npx``-only runtime described in ``docs/architecture.md``.
|
|
6
|
+
|
|
7
|
+
Steps performed (idempotent):
|
|
8
|
+
|
|
9
|
+
1. Detect legacy install signals (composer.json entry, package.json
|
|
10
|
+
devDependency, in-project symlinks pointing at vendor/ or
|
|
11
|
+
node_modules/).
|
|
12
|
+
2. Remove the package entry from composer.json / package.json
|
|
13
|
+
in-place, preserving sibling keys + formatting.
|
|
14
|
+
3. Delete agent-config managed symlinks that point inside the legacy
|
|
15
|
+
install dirs. User-added links elsewhere are preserved with a
|
|
16
|
+
warning.
|
|
17
|
+
4. Write a fresh ``.agent-settings.yml`` (only if missing) with
|
|
18
|
+
``agent_config_version`` pinned to the running version.
|
|
19
|
+
5. Update the consumer's ``.gitignore`` block (legacy paths out, new
|
|
20
|
+
project-scope entries in).
|
|
21
|
+
6. Print a summary so the developer can review + commit.
|
|
22
|
+
|
|
23
|
+
Re-runs on an already-migrated repo emit ``already migrated`` and
|
|
24
|
+
exit 0.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Iterable, Optional
|
|
34
|
+
|
|
35
|
+
PACKAGE_NAME_NPM = "@event4u/agent-config"
|
|
36
|
+
PACKAGE_NAME_COMPOSER = "event4u/agent-config"
|
|
37
|
+
LEGACY_DIRS = ("vendor", "node_modules")
|
|
38
|
+
MANAGED_SYMLINKS = (
|
|
39
|
+
".augment",
|
|
40
|
+
".claude",
|
|
41
|
+
".cursor",
|
|
42
|
+
".clinerules",
|
|
43
|
+
".windsurfrules",
|
|
44
|
+
)
|
|
45
|
+
GITIGNORE_BLOCK_START = "# >>> event4u/agent-config (managed) >>>"
|
|
46
|
+
GITIGNORE_BLOCK_END = "# <<< event4u/agent-config (managed) <<<"
|
|
47
|
+
GITIGNORE_NEW_BODY = (
|
|
48
|
+
".agent-settings.yml\n"
|
|
49
|
+
"agents/sessions/\n"
|
|
50
|
+
"agents/council-responses/\n"
|
|
51
|
+
"agents/council-sessions/\n"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _detect_npm(pkg_json: Path) -> bool:
|
|
56
|
+
if not pkg_json.is_file():
|
|
57
|
+
return False
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
60
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
61
|
+
return False
|
|
62
|
+
for key in ("dependencies", "devDependencies"):
|
|
63
|
+
section = data.get(key) or {}
|
|
64
|
+
if isinstance(section, dict) and PACKAGE_NAME_NPM in section:
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _detect_composer(composer_json: Path) -> bool:
|
|
70
|
+
if not composer_json.is_file():
|
|
71
|
+
return False
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(composer_json.read_text(encoding="utf-8"))
|
|
74
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
75
|
+
return False
|
|
76
|
+
for key in ("require", "require-dev"):
|
|
77
|
+
section = data.get(key) or {}
|
|
78
|
+
if isinstance(section, dict) and PACKAGE_NAME_COMPOSER in section:
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _strip_npm_entry(pkg_json: Path) -> bool:
|
|
84
|
+
try:
|
|
85
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
86
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
87
|
+
return False
|
|
88
|
+
changed = False
|
|
89
|
+
for key in ("dependencies", "devDependencies"):
|
|
90
|
+
section = data.get(key)
|
|
91
|
+
if isinstance(section, dict) and PACKAGE_NAME_NPM in section:
|
|
92
|
+
del section[PACKAGE_NAME_NPM]
|
|
93
|
+
changed = True
|
|
94
|
+
if not section:
|
|
95
|
+
del data[key]
|
|
96
|
+
if changed:
|
|
97
|
+
pkg_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
98
|
+
return changed
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _strip_composer_entry(composer_json: Path) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
data = json.loads(composer_json.read_text(encoding="utf-8"))
|
|
104
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
105
|
+
return False
|
|
106
|
+
changed = False
|
|
107
|
+
for key in ("require", "require-dev"):
|
|
108
|
+
section = data.get(key)
|
|
109
|
+
if isinstance(section, dict) and PACKAGE_NAME_COMPOSER in section:
|
|
110
|
+
del section[PACKAGE_NAME_COMPOSER]
|
|
111
|
+
changed = True
|
|
112
|
+
if not section:
|
|
113
|
+
del data[key]
|
|
114
|
+
if changed:
|
|
115
|
+
composer_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
116
|
+
return changed
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _classify_symlink(link: Path) -> Optional[str]:
|
|
120
|
+
"""Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
|
|
121
|
+
if not link.is_symlink():
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
|
|
125
|
+
except OSError:
|
|
126
|
+
return None
|
|
127
|
+
target_str = str(target)
|
|
128
|
+
if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
|
|
129
|
+
return "legacy"
|
|
130
|
+
return "user"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
|
|
134
|
+
removed: list[str] = []
|
|
135
|
+
preserved: list[str] = []
|
|
136
|
+
for name in MANAGED_SYMLINKS:
|
|
137
|
+
link = project / name
|
|
138
|
+
kind = _classify_symlink(link)
|
|
139
|
+
if kind == "legacy":
|
|
140
|
+
try:
|
|
141
|
+
link.unlink()
|
|
142
|
+
removed.append(name)
|
|
143
|
+
except OSError:
|
|
144
|
+
preserved.append(name)
|
|
145
|
+
elif kind == "user":
|
|
146
|
+
preserved.append(name)
|
|
147
|
+
return removed, preserved
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _write_settings(project: Path, version: str) -> bool:
|
|
151
|
+
settings = project / ".agent-settings.yml"
|
|
152
|
+
if settings.exists():
|
|
153
|
+
return False
|
|
154
|
+
body = (
|
|
155
|
+
"# .agent-settings.yml — generated by `agent-config migrate`.\n"
|
|
156
|
+
"# See docs/customization.md for the full key reference.\n"
|
|
157
|
+
f'agent_config_version: "{version}"\n'
|
|
158
|
+
)
|
|
159
|
+
settings.write_text(body, encoding="utf-8")
|
|
160
|
+
try:
|
|
161
|
+
settings.chmod(0o644)
|
|
162
|
+
except OSError:
|
|
163
|
+
pass
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _update_gitignore(project: Path) -> bool:
|
|
168
|
+
gitignore = project / ".gitignore"
|
|
169
|
+
block = (
|
|
170
|
+
f"{GITIGNORE_BLOCK_START}\n"
|
|
171
|
+
f"{GITIGNORE_NEW_BODY}"
|
|
172
|
+
f"{GITIGNORE_BLOCK_END}\n"
|
|
173
|
+
)
|
|
174
|
+
if not gitignore.exists():
|
|
175
|
+
gitignore.write_text(block, encoding="utf-8")
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
text = gitignore.read_text(encoding="utf-8")
|
|
179
|
+
pattern = re.compile(
|
|
180
|
+
re.escape(GITIGNORE_BLOCK_START) + r".*?" + re.escape(GITIGNORE_BLOCK_END) + r"\n?",
|
|
181
|
+
re.DOTALL,
|
|
182
|
+
)
|
|
183
|
+
if pattern.search(text):
|
|
184
|
+
new_text = pattern.sub(block, text)
|
|
185
|
+
else:
|
|
186
|
+
new_text = text
|
|
187
|
+
if new_text and not new_text.endswith("\n"):
|
|
188
|
+
new_text += "\n"
|
|
189
|
+
new_text += block
|
|
190
|
+
if new_text == text:
|
|
191
|
+
return False
|
|
192
|
+
gitignore.write_text(new_text, encoding="utf-8")
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _detect_already_migrated(project: Path) -> bool:
|
|
197
|
+
"""A repo counts as migrated when no legacy signal remains."""
|
|
198
|
+
if _detect_npm(project / "package.json"):
|
|
199
|
+
return False
|
|
200
|
+
if _detect_composer(project / "composer.json"):
|
|
201
|
+
return False
|
|
202
|
+
for name in MANAGED_SYMLINKS:
|
|
203
|
+
if _classify_symlink(project / name) == "legacy":
|
|
204
|
+
return False
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main(
|
|
209
|
+
argv: Optional[list[str]] = None,
|
|
210
|
+
*,
|
|
211
|
+
cwd: Optional[Path] = None,
|
|
212
|
+
version: Optional[str] = None,
|
|
213
|
+
out=sys.stdout,
|
|
214
|
+
err=sys.stderr, # noqa: ARG001 — reserved for future error paths
|
|
215
|
+
) -> int:
|
|
216
|
+
parser = argparse.ArgumentParser(
|
|
217
|
+
prog="agent-config migrate",
|
|
218
|
+
description="One-shot migration off legacy composer / npm install paths.",
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
221
|
+
help="Detect only; do not write any files.")
|
|
222
|
+
args = parser.parse_args(argv)
|
|
223
|
+
|
|
224
|
+
project = (cwd or Path.cwd()).resolve()
|
|
225
|
+
version = version or _detect_installed_version()
|
|
226
|
+
|
|
227
|
+
if _detect_already_migrated(project):
|
|
228
|
+
print("✅ already migrated — nothing to do.", file=out)
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
if args.dry_run:
|
|
232
|
+
print("ℹ️ legacy install detected — re-run without --dry-run to migrate.", file=out)
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
summary: list[str] = []
|
|
236
|
+
if _strip_npm_entry(project / "package.json"):
|
|
237
|
+
summary.append(f"removed {PACKAGE_NAME_NPM} from package.json")
|
|
238
|
+
if _strip_composer_entry(project / "composer.json"):
|
|
239
|
+
summary.append(f"removed {PACKAGE_NAME_COMPOSER} from composer.json")
|
|
240
|
+
removed_links, preserved_links = _purge_legacy_symlinks(project)
|
|
241
|
+
for name in removed_links:
|
|
242
|
+
summary.append(f"removed legacy symlink {name}")
|
|
243
|
+
for name in preserved_links:
|
|
244
|
+
summary.append(f"preserved user-managed {name} (review manually)")
|
|
245
|
+
if _write_settings(project, version):
|
|
246
|
+
summary.append(f".agent-settings.yml written (pinned to {version})")
|
|
247
|
+
if _update_gitignore(project):
|
|
248
|
+
summary.append(".gitignore agent-config block refreshed")
|
|
249
|
+
|
|
250
|
+
print("✅ migration complete:", file=out)
|
|
251
|
+
for line in summary:
|
|
252
|
+
print(f" - {line}", file=out)
|
|
253
|
+
print("\n Next: review the diff and commit.", file=out)
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _detect_installed_version() -> str:
|
|
258
|
+
pkg_json = Path(__file__).resolve().parents[2] / "package.json"
|
|
259
|
+
try:
|
|
260
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
261
|
+
version = data.get("version")
|
|
262
|
+
if isinstance(version, str) and version.strip():
|
|
263
|
+
return version.strip()
|
|
264
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
265
|
+
pass
|
|
266
|
+
return "0.0.0"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__": # pragma: no cover
|
|
270
|
+
sys.exit(main())
|