claude-dev-env 2.14.1 → 2.15.1
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/_shared/advisor/scripts/tier_model_ids.py +6 -2
- 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 +165 -51
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +42 -8
- 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/package.json +3 -2
- package/scripts/AGENTS.md +0 -7
- package/scripts/claude_chain_runner.py +49 -2
- package/scripts/invoke_code_review.py +48 -51
- package/scripts/resolve_worker_spawn.py +42 -47
- 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_claude_chain_runner.py +28 -0
- package/scripts/test_dispatcher_profile_import.py +184 -0
- package/scripts/test_resolve_worker_spawn.py +119 -1
- package/scripts/test_validate_instruction_pairs.py +30 -0
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +0 -25
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +0 -60
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +0 -54
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +0 -64
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +0 -180
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +0 -288
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +0 -275
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +0 -221
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +0 -108
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +0 -147
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +0 -102
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +0 -210
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +0 -299
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +0 -127
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +0 -51
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +0 -77
- package/scripts/sync_to_cursor/config.py +0 -5
|
@@ -30,8 +30,12 @@ from collections.abc import Mapping
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
32
|
_config_directory = str(Path(__file__).resolve().parent / "config")
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
sys.path[:] = [
|
|
34
|
+
each_existing_entry
|
|
35
|
+
for each_existing_entry in sys.path
|
|
36
|
+
if each_existing_entry != _config_directory
|
|
37
|
+
]
|
|
38
|
+
sys.path[:0] = [_config_directory]
|
|
35
39
|
|
|
36
40
|
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
37
41
|
ALL_CLI_MODEL_ID_BY_TIER,
|
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
|
+
});
|