claude-dev-env 2.14.0 → 2.15.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/AGENTS.md +106 -32
- package/agents/AGENTS.md +0 -5
- package/agents/test_agent_frontmatter.py +7 -7
- package/bin/AGENTS.md +6 -4
- package/bin/install-constants.mjs +51 -0
- package/bin/install.codex-rules.test.mjs +173 -0
- package/bin/install.cursor-rules.test.mjs +103 -0
- package/bin/install.mjs +130 -19
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +3 -3
- package/bin/install.transaction.test.mjs +1 -0
- package/bin/install.uninstall-transaction.test.mjs +1 -0
- package/bin/resolve-install-root.mjs +43 -10
- package/codex-rules/claude-dev-env.rules +12 -0
- package/commands/AGENTS.md +0 -10
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
- package/hooks/diagnostic/AGENTS.md +32 -0
- package/hooks/diagnostic/hook_log_init.py +2 -2
- package/output-styles/AGENTS.md +1 -1
- package/package.json +2 -1
- package/scripts/sync_to_cursor/AGENTS.md +3 -3
- package/scripts/sync_to_cursor/canonical_docs.py +11 -11
- package/scripts/sync_to_cursor/config/__init__.py +8 -0
- package/scripts/sync_to_cursor/engine.py +26 -1
- package/scripts/sync_to_cursor/rules.py +76 -5
- package/scripts/test_active_capability_references.py +2 -2
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/skills/anthropic-plan/AGENTS.md +1 -1
- package/skills/anthropic-plan/SKILL.md +1 -1
- package/skills/anthropic-plan/test_skill_contract.py +8 -6
- package/skills/prototype/SKILL.md +1 -2
- package/skills/prototype/reference/promotion-tasks.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/agents/caveman.md +0 -73
- package/agents/clasp-deployment-orchestrator.md +0 -608
- package/agents/code-advisor.md +0 -23
- package/agents/deep-research.md +0 -152
- package/agents/docs-agent.md +0 -85
- package/commands/commit.md +0 -28
- package/commands/docupdate.md +0 -322
- package/commands/hook-log-extract.md +0 -70
- package/commands/hook-log-init.md +0 -76
- package/commands/implement.md +0 -102
- package/commands/plan.md +0 -14
- package/commands/pr-comments.md +0 -47
- package/commands/review-plan.md +0 -5
- package/commands/right-size.md +0 -15
- package/commands/sum.md +0 -30
- package/scripts/sync_to_cursor/config.py +0 -5
package/AGENTS.md
CHANGED
|
@@ -1,61 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Scope
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every rule in this file governs all text everywhere: chat replies, tool-call sentences, plans, questions you ask, code, code comments, test names, commit subjects, pull request and issue bodies, documentation, and every file you write. No rule stops at the edge of a chat message.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A rule that names a form in order to forbid it passes its own check, and so does a two-column table that teaches a rewrite.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Ask when ambiguity materially changes scope or implementation. Collect credentials through secure UI only; never request secrets in chat.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Documentation
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Describe only the current system state. Keep documentation self-contained and free of historical, transitional, conversational, or version-transition language. Never use negative prose or antipatterns. Always state what to do, specifically.
|
|
12
|
+
Code and tests
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Tests must exercise real behavior, real data, and production paths. Test theater is forbidden.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|---|---|
|
|
17
|
-
| `~/.claude/docs/references/advisor-tool.md` | When to call a stronger reviewer, hard rule before first write, how to treat advice |
|
|
18
|
-
| `/team-advisor` skill | Standing warm advisor bind (map: `docs/references/team-advisor-skill.md`) |
|
|
19
|
-
| `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, model floor, lifecycle |
|
|
16
|
+
For multi-step code tasks:
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
Coders consult a warm session-advisor when blocked (Sol xHigh).
|
|
19
|
+
Repair reported findings when that review mode is selected.
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
Research and delegation
|
|
22
|
+
Delegate fact extraction when multiple files or search patterns are required. Request precise file-and-line answers.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
Use warm & reusable parallel luna (you decide effort level per task) fast subagents for unrelated questions; threaded & named appropriately.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
Read or search directly only in files you will modify via es.exe.
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
For code navigation, prefer es.exe, then content search or globbing.
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
Scope every es.exe search.
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Never scan an entire drive or network share.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Task tracking
|
|
35
|
+
Track every task using `update_plan`.
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Definitions
|
|
38
|
+
Warm agent: Any agent who has acted within the past 30 minutes.
|
|
39
|
+
|
|
40
|
+
# Response and working style
|
|
41
|
+
|
|
42
|
+
Mid-run and closing narration follow `rules/opus5-communication-contract.md` (`opus5-communication-contract-v1`): first progress update is one sentence; later updates only for important discoveries or direction changes; the final starts with the outcome.
|
|
43
|
+
|
|
44
|
+
Keep responses focused, brief, and concise. Keep disclaimers and caveats short, and spend most of the response on the main answer. When asked to explain something, give a high-level summary unless an in-depth explanation is specifically requested.
|
|
45
|
+
|
|
46
|
+
# Word budget
|
|
47
|
+
|
|
48
|
+
Say it in the fewest words that stay accurate and complete. Before sending, cut every sentence that does not change what the reader thinks or does.
|
|
49
|
+
|
|
50
|
+
Cut these on sight:
|
|
51
|
+
|
|
52
|
+
- Deliberation. State the decision, not the reasoning that reached it, unless the reader has to weigh it themselves.
|
|
53
|
+
- Why you did not do something. Say what you did; add the reason only if the reader must decide whether to do it.
|
|
54
|
+
- Incidental findings from your own process. Report one only when the reader must act on it, and give it one line.
|
|
55
|
+
- Any sentence that restates a fact already stated in a heading, a list, or an earlier line.
|
|
56
|
+
|
|
57
|
+
When you have more than two facts of the same kind, use a list or a table. Prose paragraphs hide facts; rows expose them.
|
|
58
|
+
|
|
59
|
+
# No contrast framing
|
|
60
|
+
|
|
61
|
+
Write the claim. Never prop it up against what it is not.
|
|
38
62
|
|
|
39
|
-
|
|
63
|
+
The banned shape is a claim paired with a rejected alternative, in any wording:
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
| Banned | Write instead |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Verified against the remote, not just locally | Verified against the remote |
|
|
68
|
+
| This is a design flaw, not a typo | This is a design flaw |
|
|
69
|
+
| Not a copy of the shared script, but an ad |
|
|
70
|
+
| Rather than patching the caller, the fix moves into the helper | The fix moves into the helper |
|
|
71
|
+
| Instead of three passes, it runs one | It runs one pass |
|
|
72
|
+
| It is not only faster; it is correct | It is correct and faster |
|
|
73
|
+
| This is less a bug than a missing feature
|
|
74
|
+
| Let me read the log rather than guessing | Reading the log. |
|
|
75
|
+
| I'll patch the helper instead of the caller | Patching the helper. |
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
Every wording of the shape is banned, including `X, not Y`, `not Y but X`, `rather than Y, X`, `instead of Y, X`, `X over Y`, `not just X — Y`, `less X than Y`, and a negated sentence followed by its po
|
|
78
|
+
──── (152 lines hidden) ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
the name.
|
|
44
80
|
|
|
45
|
-
|
|
81
|
+
| Written on the day | Named for the subject |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `august_cert_failures.py` | `cert_rejections.py` |
|
|
84
|
+
| `fix_august_bug()` | `normalize_calendar_color()` |
|
|
85
|
+
| `AUGUST_REJECTION_CODES` | `REJECTION_CODE
|
|
86
|
+
| `test_august_failures` | `test_rejects_wrong_calendar_color` |
|
|
87
|
+
| `jira4821_validator.py` | `manifest_validator.py` |
|
|
88
|
+
| `q3_migration/` | `add_tenant_id_column/` |
|
|
89
|
+
| `v2_client.py` | `retrying_client.py` |
|
|
90
|
+
| `legacy_export.py` | `csv_export.py` |
|
|
91
|
+
| `temp_fix.py` | `unicode_path_workaround.p
|
|
92
|
+
| "Fix August cert failures" | "Fix calendar color mismatch in cert export" |
|
|
46
93
|
|
|
47
|
-
|
|
94
|
+
A branch name is a name. It carries no date,ither. Someone reads it to decide whether to check the branch out, so it has to say what the work does.
|
|
48
95
|
|
|
49
|
-
|
|
96
|
+
| Written on the day | Named for the subject |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `fix/cert-2026-08-b3-calendar-widget-color` | `fix-calendar-widget-color` |
|
|
99
|
+
| `parse-rejection-emails-cert-2026-08-b3` |
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
One prefix spreads. Once `august_` sits in one name, the next name matches it for consistency, and within a week the month reads as a real domain concept that forty places depend on. Rename it the hour you notice it.
|
|
52
102
|
|
|
53
|
-
|
|
103
|
+
# Change size
|
|
54
104
|
|
|
55
|
-
|
|
105
|
+
When planning work or opening a pull request, size the change first: one self-contained change, around 100 lines, with its tests. Read the small-changelists guide for the numbers, the allowed exceptions, and how to split.
|
|
56
106
|
|
|
57
|
-
|
|
107
|
+
## Execution and delegation
|
|
108
|
+
|
|
109
|
+
Delegate all task work to Tier 3 agents.
|
|
110
|
+
|
|
111
|
+
Draft a separate assignment for each agent. Each assignment must be clear, concise, tightly scoped, independently executable, and explicit about ownership, constraints, deliverables, and verification.
|
|
112
|
+
|
|
113
|
+
Run independent assignments in parallel. Keep overlapping work sequential. The primary agent coordinates agents, resolves dependencies, verifies results, and reports outcomes.
|
|
58
114
|
|
|
59
115
|
## Definitions
|
|
60
116
|
|
|
61
|
-
|
|
117
|
+
Tier 3 agent: A strong execution specialist that independently completes a bounded assignment, follows repository contracts, repairs routine failures, tests production behavior, and escalates decisions that materially affect architecture or scope.
|
|
118
|
+
|
|
119
|
+
Warm agent: An agent that has acted within the past 30 minutes. Reuse warm agents for related follow-up work.
|
|
120
|
+
|
|
121
|
+
# Corrections
|
|
122
|
+
|
|
123
|
+
Only correct an earlier statement when the ecode, conclusions, or decisions. Statecorrections plainly and briefly, then continue the task. For slips that change nothing for the user, make the fix and move on without noting it.
|
|
124
|
+
|
|
125
|
+
# Tool calls and output hygiene
|
|
126
|
+
|
|
127
|
+
When you use a tool, you may say a brief sentence first. If no tool can express what the user asked for, say so. Do not include internal or system XML tags in your response.
|
|
128
|
+
|
|
129
|
+
# Code review
|
|
130
|
+
|
|
131
|
+
When reviewing code, report everything you find. Filtering belongs in a separate pass.
|
|
132
|
+
|
|
133
|
+
<tone_preference>
|
|
134
|
+
Keep outputs reasonably concise.
|
|
135
|
+
</tone_preference>
|
package/agents/AGENTS.md
CHANGED
|
@@ -6,13 +6,8 @@ Agent definition files installed into `~/.claude/agents/` by `bin/install.mjs`.
|
|
|
6
6
|
|
|
7
7
|
| File | Agent name | Role |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `caveman.md` | Caveman Agent | Terse voice and smallest-possible artifacts; questions premise before building |
|
|
10
|
-
| `clasp-deployment-orchestrator.md` | Clasp Deployment Orchestrator | Creates and deploys Google Apps Script projects with multiple files |
|
|
11
9
|
| `clean-coder.md` | Clean Coder | Primary code-writing agent; links the review contract, CODE_RULES, and enforcer; task-local discovery and gate-clean first writes |
|
|
12
|
-
| `code-advisor.md` | Code Advisor | Single-executor mid-run advisor (PLAN/CORRECTION/STOP as final text); distinct from session-advisor |
|
|
13
10
|
| `code-quality-agent.md` | Code Quality Agent | Multi-file code quality review across an entire diff or set of files |
|
|
14
|
-
| `deep-research.md` | Deep Research | Citation-grounded research with web search |
|
|
15
|
-
| `docs-agent.md` | Docs Agent | Documentation authoring and maintenance |
|
|
16
11
|
| `git-commit-crafter.md` | Git Commit Crafter | Stages changes, writes conventional commit messages, creates commits |
|
|
17
12
|
| `issue-tracker.md` | Issue Tracker | Primary handler for one GitHub issue action per spawn; loads the issue-tracker skill (plain-brief); returns issue numbers and URLs |
|
|
18
13
|
| `plan-packet-validator.md` | Plan Packet Validator | Fresh-context validator for workflow-generated plan packets under `docs/plans/` |
|
|
@@ -42,8 +42,8 @@ non-empty string, and a `name` equal to its file stem — a mapping that loads
|
|
|
42
42
|
but binds `description` to nothing, or names an agent the file does not,
|
|
43
43
|
registers a subagent the caller cannot spawn::
|
|
44
44
|
|
|
45
|
-
ok:
|
|
46
|
-
flag:
|
|
45
|
+
ok: clean-coder.md -> name: clean-coder
|
|
46
|
+
flag: clean-coder.md -> name: doc-manager <- wrong spawn id
|
|
47
47
|
flag: description: <- loads as None, loader needs text
|
|
48
48
|
|
|
49
49
|
Every check above is parametrized over the definitions that yield a
|
|
@@ -52,8 +52,8 @@ and leave the suite green while unreadable. The block is what the fence lines
|
|
|
52
52
|
delimit, so the file that opens no fence or never closes one is exactly the
|
|
53
53
|
broken file these checks exist to catch::
|
|
54
54
|
|
|
55
|
-
ok:
|
|
56
|
-
flag:
|
|
55
|
+
ok: clean-coder.md -> --- name/description --- <- block found
|
|
56
|
+
flag: clean-coder.md -> --- name/description <- no closing fence,
|
|
57
57
|
silently uncovered
|
|
58
58
|
|
|
59
59
|
`test_every_agent_definition_yields_a_frontmatter_block` holds that floor: it
|
|
@@ -218,8 +218,8 @@ def _agent_name_problem(parsed_frontmatter: object, expected_name: str) -> str |
|
|
|
218
218
|
A subagent registers under the name in its frontmatter, so a name that is
|
|
219
219
|
not the file stem is spawned by an id no caller uses::
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
clean-coder.md -> name: clean-coder -> ok: None
|
|
222
|
+
clean-coder.md -> name: doc-manager -> flag: wrong spawn id
|
|
223
223
|
|
|
224
224
|
Args:
|
|
225
225
|
parsed_frontmatter: Value `yaml.safe_load` produced for the block.
|
|
@@ -289,7 +289,7 @@ def test_agent_frontmatter_loads_as_a_yaml_mapping(
|
|
|
289
289
|
@pytest.mark.parametrize(
|
|
290
290
|
"agent_file_name",
|
|
291
291
|
(
|
|
292
|
-
"
|
|
292
|
+
"clean-coder.md",
|
|
293
293
|
"issue-tracker.md",
|
|
294
294
|
"skill-writer-agent.md",
|
|
295
295
|
),
|
package/bin/AGENTS.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# bin
|
|
2
2
|
|
|
3
|
-
The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks,
|
|
3
|
+
The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, writes `~/.mypy.ini` under the process home, and copies Codex exec-policy files into `~/.codex/rules` (`CODEX_HOME/rules` when that variable is set), and generates Cursor `.mdc` files into `~/.cursor/rules` from the installed Claude rules.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
9
|
| `install.mjs` | Main installer: builds a read-only plan via `install-plan.mjs`, then runs mutations inside `install-transaction.mjs` recovery (copy content directories, merge hooks, install skills, prune, git hooks, mypy.ini); routes `CLAUDE_HOME`, the manifest path, and `~/.mypy.ini` through `resolve-install-root.mjs`; resolves single or multi-profile targets before mutation and writes one ownership manifest per target |
|
|
10
|
-
| `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` |
|
|
10
|
+
| `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` plus files under the Codex rules directory and the Cursor home |
|
|
11
11
|
| `select-install-targets.mjs` | Pure target selection for main-default, explicit `--target`, and `--profile`/`--profiles`; rejects ambiguous or duplicate targets; builds per-target manifest records with `targetIdentity` and `managedRoot` |
|
|
12
12
|
| `install-plan.mjs` | Read-only install and uninstall plans: install preflight (managed root, source conflicts, Python, settings when hooks install) and uninstall preflight (settings JSON before removal, removable vs skipped manifest records), freezes plans E2/F execute |
|
|
13
13
|
| `install-transaction.mjs` | Install, update, and uninstall transaction journal: captures prior settings, manifest, managed files, and `core.hooksPath`, restores them on failure, and supports fault injection phases for recovery tests |
|
|
14
14
|
| `install.transaction.test.mjs` | Unit and sandbox installer tests for snapshot/restore and fault phases (`after_file_staging`, `after_settings_write`, `after_git_config`, `after_manifest_write`) |
|
|
15
15
|
| `install.uninstall-transaction.test.mjs` | Uninstall plan preflight and recovery: malformed/non-object settings fail before removal, each fault phase restores files/settings/manifest/`core.hooksPath`, retry succeeds, selected-root containment |
|
|
16
16
|
| `install.profile-root.test.mjs` | Contract tests for the install-root resolver: precedence, containment boundary, external allowlist, and the install.mjs import smoke check |
|
|
17
|
-
| `install-
|
|
17
|
+
| `install.codex-rules.test.mjs` | Tests that Codex exec-policy files copy to `~/.codex/rules`, honor `CODEX_HOME`, skip `--only journal`, and uninstall without touching `default.rules` |
|
|
18
|
+
| `install.cursor-rules.test.mjs` | Tests that Cursor `.mdc` files generate into `~/.cursor/rules` from Claude rules, skip `--only journal`, and leave a local extra `.mdc` in place |
|
|
19
|
+
| `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes, plus the Codex home and rules directory names `resolve-install-root.mjs` uses |
|
|
18
20
|
| `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
|
|
19
21
|
| `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
|
|
20
22
|
| `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
|
|
@@ -78,7 +80,7 @@ A run that moves nothing sweeps nothing, so every recovery point the user holds
|
|
|
78
80
|
|
|
79
81
|
`--uninstall` builds a read-only uninstall plan, captures a recovery snapshot, then removes each file the plan lists.
|
|
80
82
|
|
|
81
|
-
Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
|
|
83
|
+
Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory, or it sits under the Codex rules directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
|
|
82
84
|
|
|
83
85
|
The uninstall runs inside the same snapshot/restore journal as install: prior settings, manifest, managed files, and `core.hooksPath` restore when a later phase fails, so a retry starts from a complete ownership record. The journal is discarded only after a successful commit.
|
|
84
86
|
|
|
@@ -85,3 +85,54 @@ export const SETTINGS_FILE_NAME = 'settings.json';
|
|
|
85
85
|
* removes the file the install created.
|
|
86
86
|
*/
|
|
87
87
|
export const MYPY_INI_FILE_NAME = '.mypy.ini';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Environment variable Codex uses for its config home. When unset, Codex reads
|
|
91
|
+
* `~/.codex`. The installer copies shipped exec-policy files into
|
|
92
|
+
* `<that home>/rules`.
|
|
93
|
+
*/
|
|
94
|
+
export const CODEX_HOME_ENVIRONMENT_VARIABLE = 'CODEX_HOME';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Directory name Codex uses under the user home when `CODEX_HOME` is unset.
|
|
98
|
+
*/
|
|
99
|
+
export const DEFAULT_CODEX_DIRECTORY_NAME = '.codex';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Directory name under the Codex home that holds `*.rules` exec-policy files.
|
|
103
|
+
* Codex loads every file in that directory; see `load_exec_policy` in Codex.
|
|
104
|
+
*/
|
|
105
|
+
export const CODEX_RULES_DIRECTORY_NAME = 'rules';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Package subdirectory that holds the shipped Codex exec-policy files. The
|
|
109
|
+
* installer copies this tree into the Codex rules directory, not into
|
|
110
|
+
* `~/.claude/`.
|
|
111
|
+
*/
|
|
112
|
+
export const CODEX_RULES_PACKAGE_DIRECTORY_NAME = 'codex-rules';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Shipped exec-policy file name. A distinct name keeps a local `default.rules`
|
|
116
|
+
* file in place.
|
|
117
|
+
*/
|
|
118
|
+
export const CODEX_RULES_SHIPPED_FILE_NAME = 'claude-dev-env.rules';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Directory name Cursor uses under the user home for editor config.
|
|
122
|
+
*/
|
|
123
|
+
export const DEFAULT_CURSOR_DIRECTORY_NAME = '.cursor';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Directory name under the Cursor home that holds generated `.mdc` rule files.
|
|
127
|
+
*/
|
|
128
|
+
export const CURSOR_RULES_DIRECTORY_NAME = 'rules';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Installed script that writes Cursor `.mdc` files from Claude rules.
|
|
132
|
+
*/
|
|
133
|
+
export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Windows Python launcher command the installer may bake into hook settings.
|
|
137
|
+
*/
|
|
138
|
+
export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import {
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import {
|
|
16
|
+
resolveInstallRoot,
|
|
17
|
+
isAllowedInstallDestination,
|
|
18
|
+
} from './resolve-install-root.mjs';
|
|
19
|
+
import {
|
|
20
|
+
CODEX_HOME_ENVIRONMENT_VARIABLE,
|
|
21
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
22
|
+
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
23
|
+
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
24
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
25
|
+
} from './install-constants.mjs';
|
|
26
|
+
import { CONTENT_DIRECTORIES, INSTALL_GROUPS } from './install.mjs';
|
|
27
|
+
|
|
28
|
+
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
30
|
+
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
31
|
+
const SHIPPED_RULES_SOURCE_PATH = join(
|
|
32
|
+
PACKAGE_DIRECTORY,
|
|
33
|
+
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
34
|
+
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function runInstaller(homeDirectory, extraArguments) {
|
|
38
|
+
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
39
|
+
cwd: PACKAGE_DIRECTORY,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
HOME: homeDirectory,
|
|
44
|
+
USERPROFILE: homeDirectory,
|
|
45
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
46
|
+
[CODEX_HOME_ENVIRONMENT_VARIABLE]: join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('CONTENT_DIRECTORIES omits codex-rules because that tree installs to the Codex home', () => {
|
|
52
|
+
assert.equal(CONTENT_DIRECTORIES.includes(CODEX_RULES_PACKAGE_DIRECTORY_NAME), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('the core group installs Codex exec-policy files', () => {
|
|
56
|
+
assert.equal(INSTALL_GROUPS.core.includeCodexRules, true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('resolveInstallRoot names ~/.codex/rules and allows files under it', () => {
|
|
60
|
+
const homeDirectory = join(tmpdir(), 'cdev-codex-rules-home');
|
|
61
|
+
const resolution = resolveInstallRoot({
|
|
62
|
+
homeDirectory,
|
|
63
|
+
environment: {},
|
|
64
|
+
explicitTarget: null,
|
|
65
|
+
});
|
|
66
|
+
const expectedDirectory = join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME, CODEX_RULES_DIRECTORY_NAME);
|
|
67
|
+
assert.equal(resolution.codexRulesInstallDirectory, expectedDirectory);
|
|
68
|
+
assert.equal(
|
|
69
|
+
isAllowedInstallDestination(join(expectedDirectory, CODEX_RULES_SHIPPED_FILE_NAME), resolution),
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
assert.equal(
|
|
73
|
+
isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
|
|
74
|
+
false,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('CODEX_HOME relocates the Codex rules destination', () => {
|
|
79
|
+
const homeDirectory = join(tmpdir(), 'cdev-codex-home-default');
|
|
80
|
+
const relocatedHome = join(tmpdir(), 'cdev-codex-home-relocated');
|
|
81
|
+
const resolution = resolveInstallRoot({
|
|
82
|
+
homeDirectory,
|
|
83
|
+
environment: { [CODEX_HOME_ENVIRONMENT_VARIABLE]: relocatedHome },
|
|
84
|
+
explicitTarget: null,
|
|
85
|
+
});
|
|
86
|
+
assert.equal(
|
|
87
|
+
resolution.codexRulesInstallDirectory,
|
|
88
|
+
join(relocatedHome, CODEX_RULES_DIRECTORY_NAME),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a full install copies shipped Codex rules and leaves a local default.rules in place', () => {
|
|
93
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-install-'));
|
|
94
|
+
try {
|
|
95
|
+
const userRulesPath = join(
|
|
96
|
+
homeDirectory,
|
|
97
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
98
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
99
|
+
'default.rules',
|
|
100
|
+
);
|
|
101
|
+
mkdirSync(dirname(userRulesPath), { recursive: true });
|
|
102
|
+
writeFileSync(userRulesPath, 'prefix_rule(pattern=["echo", "hi"], decision="allow")\n');
|
|
103
|
+
|
|
104
|
+
runInstaller(homeDirectory, []);
|
|
105
|
+
|
|
106
|
+
const installedRulesPath = join(
|
|
107
|
+
homeDirectory,
|
|
108
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
109
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
110
|
+
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
111
|
+
);
|
|
112
|
+
assert.equal(existsSync(installedRulesPath), true);
|
|
113
|
+
assert.equal(
|
|
114
|
+
readFileSync(installedRulesPath, 'utf8'),
|
|
115
|
+
readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8'),
|
|
116
|
+
);
|
|
117
|
+
assert.equal(
|
|
118
|
+
readFileSync(userRulesPath, 'utf8'),
|
|
119
|
+
'prefix_rule(pattern=["echo", "hi"], decision="allow")\n',
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a full install Codex rules file contains no personal home path', () => {
|
|
127
|
+
const shippedText = readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8');
|
|
128
|
+
assert.equal(shippedText.includes('Users\\jon'), false);
|
|
129
|
+
assert.equal(shippedText.includes('Users/jon'), false);
|
|
130
|
+
assert.equal(shippedText.includes('JonEcho'), false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('--only journal skips Codex rules; --only core copies them', () => {
|
|
134
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-groups-'));
|
|
135
|
+
try {
|
|
136
|
+
const installedRulesPath = join(
|
|
137
|
+
homeDirectory,
|
|
138
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
139
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
140
|
+
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
141
|
+
);
|
|
142
|
+
runInstaller(homeDirectory, ['--only', 'journal']);
|
|
143
|
+
assert.equal(existsSync(installedRulesPath), false);
|
|
144
|
+
|
|
145
|
+
runInstaller(homeDirectory, ['--only', 'core']);
|
|
146
|
+
assert.equal(existsSync(installedRulesPath), true);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('uninstall removes the shipped Codex rules file and leaves default.rules', () => {
|
|
153
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-uninstall-'));
|
|
154
|
+
try {
|
|
155
|
+
const rulesDirectory = join(
|
|
156
|
+
homeDirectory,
|
|
157
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
158
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
159
|
+
);
|
|
160
|
+
const userRulesPath = join(rulesDirectory, 'default.rules');
|
|
161
|
+
mkdirSync(rulesDirectory, { recursive: true });
|
|
162
|
+
writeFileSync(userRulesPath, 'keep-me\n');
|
|
163
|
+
|
|
164
|
+
runInstaller(homeDirectory, []);
|
|
165
|
+
runInstaller(homeDirectory, ['--uninstall']);
|
|
166
|
+
|
|
167
|
+
const installedRulesPath = join(rulesDirectory, CODEX_RULES_SHIPPED_FILE_NAME);
|
|
168
|
+
assert.equal(existsSync(installedRulesPath), false);
|
|
169
|
+
assert.equal(readFileSync(userRulesPath, 'utf8'), 'keep-me\n');
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import {
|
|
9
|
+
resolveInstallRoot,
|
|
10
|
+
isAllowedInstallDestination,
|
|
11
|
+
} from './resolve-install-root.mjs';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
14
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
15
|
+
} from './install-constants.mjs';
|
|
16
|
+
|
|
17
|
+
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
19
|
+
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
20
|
+
|
|
21
|
+
function runInstaller(homeDirectory, extraArguments) {
|
|
22
|
+
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
23
|
+
cwd: PACKAGE_DIRECTORY,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
HOME: homeDirectory,
|
|
28
|
+
USERPROFILE: homeDirectory,
|
|
29
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
|
|
35
|
+
const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
|
|
36
|
+
const resolution = resolveInstallRoot({
|
|
37
|
+
homeDirectory,
|
|
38
|
+
environment: {},
|
|
39
|
+
explicitTarget: null,
|
|
40
|
+
});
|
|
41
|
+
const expectedDirectory = join(
|
|
42
|
+
homeDirectory,
|
|
43
|
+
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
44
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
45
|
+
);
|
|
46
|
+
assert.equal(resolution.cursorRulesInstallDirectory, expectedDirectory);
|
|
47
|
+
assert.equal(
|
|
48
|
+
isAllowedInstallDestination(join(expectedDirectory, 'plain-language.mdc'), resolution),
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
assert.equal(
|
|
52
|
+
isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
|
|
53
|
+
false,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a full install writes stem-named Cursor rules and leaves a local extra mdc in place', () => {
|
|
58
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-install-'));
|
|
59
|
+
try {
|
|
60
|
+
const extraRulePath = join(
|
|
61
|
+
homeDirectory,
|
|
62
|
+
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
63
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
64
|
+
'user-local.mdc',
|
|
65
|
+
);
|
|
66
|
+
mkdirSync(dirname(extraRulePath), { recursive: true });
|
|
67
|
+
writeFileSync(extraRulePath, 'keep-me\n');
|
|
68
|
+
|
|
69
|
+
runInstaller(homeDirectory, []);
|
|
70
|
+
|
|
71
|
+
const generatedPath = join(
|
|
72
|
+
homeDirectory,
|
|
73
|
+
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
74
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
75
|
+
'plain-language.mdc',
|
|
76
|
+
);
|
|
77
|
+
assert.equal(existsSync(generatedPath), true);
|
|
78
|
+
const generatedText = readFileSync(generatedPath, 'utf8');
|
|
79
|
+
assert.equal(generatedText.includes('alwaysApply: true'), true);
|
|
80
|
+
assert.equal(readFileSync(extraRulePath, 'utf8'), 'keep-me\n');
|
|
81
|
+
} finally {
|
|
82
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('--only journal skips Cursor rule generation; --only core writes them', () => {
|
|
87
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-groups-'));
|
|
88
|
+
try {
|
|
89
|
+
const generatedPath = join(
|
|
90
|
+
homeDirectory,
|
|
91
|
+
DEFAULT_CURSOR_DIRECTORY_NAME,
|
|
92
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
93
|
+
'plain-language.mdc',
|
|
94
|
+
);
|
|
95
|
+
runInstaller(homeDirectory, ['--only', 'journal']);
|
|
96
|
+
assert.equal(existsSync(generatedPath), false);
|
|
97
|
+
|
|
98
|
+
runInstaller(homeDirectory, ['--only', 'core']);
|
|
99
|
+
assert.equal(existsSync(generatedPath), true);
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|