claude-dev-env 8.36.1 → 8.36.3

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.
@@ -212,7 +212,7 @@ def default_credentials_path() -> Path:
212
212
 
213
213
  ::
214
214
 
215
- CLAUDE_CONFIG_DIR=C:/profiles/mel -> C:/profiles/mel/.credentials.json
215
+ CLAUDE_CONFIG_DIR=C:/profiles/work -> C:/profiles/work/.credentials.json
216
216
  (variable unset or empty) -> ~/.claude/.credentials.json
217
217
 
218
218
  A profile-isolated session keeps its credential under the config-dir
@@ -664,12 +664,12 @@ def test_does_not_flag_identifier_bound_only_in_a_test_module() -> None:
664
664
  "--- a/tests/test_config.py\n"
665
665
  "+++ b/tests/test_config.py\n"
666
666
  "@@ -0,0 +1,1 @@\n"
667
- "+jon_pc_json = load_fixture()\n"
667
+ "+office_pc_json = load_fixture()\n"
668
668
  "diff --git a/README.md b/README.md\n"
669
669
  "--- a/README.md\n"
670
670
  "+++ b/README.md\n"
671
671
  "@@ -0,0 +1,1 @@\n"
672
- "+The jon pc export runs first.\n"
672
+ "+The office pc export runs first.\n"
673
673
  )
674
674
  assert sweep_diff(diff) == []
675
675
 
@@ -680,16 +680,16 @@ def test_still_flags_identifier_bound_in_a_production_module() -> None:
680
680
  "--- a/api/config.py\n"
681
681
  "+++ b/api/config.py\n"
682
682
  "@@ -0,0 +1,1 @@\n"
683
- "+jon_pc_json = load_config()\n"
683
+ "+office_pc_json = load_config()\n"
684
684
  "diff --git a/README.md b/README.md\n"
685
685
  "--- a/README.md\n"
686
686
  "+++ b/README.md\n"
687
687
  "@@ -0,0 +1,1 @@\n"
688
- "+The jon pc export runs first.\n"
688
+ "+The office pc export runs first.\n"
689
689
  )
690
690
  findings = sweep_diff(diff)
691
691
  assert len(findings) == 1
692
- assert "jon pc export" in findings[0]
692
+ assert "office pc export" in findings[0]
693
693
 
694
694
 
695
695
  def test_does_not_flag_window_whose_leading_tokens_are_a_known_identifier() -> None:
@@ -8,6 +8,7 @@ import {
8
8
  writeFileSync,
9
9
  symlinkSync,
10
10
  readFileSync,
11
+ readlinkSync,
11
12
  readdirSync,
12
13
  existsSync,
13
14
  copyFileSync,
@@ -16,6 +17,7 @@ import {
16
17
  import { tmpdir } from 'node:os';
17
18
  import { join, dirname } from 'node:path';
18
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { FAULT_PHASES, INSTALL_FAULT_ENV } from './install-transaction.mjs';
19
21
 
20
22
  import {
21
23
  collectPackageSourceConflicts,
@@ -50,6 +52,7 @@ import {
50
52
  commandNamesOwnedHookScript,
51
53
  hookCommandComparisonSpelling,
52
54
  detectPython,
55
+ pythonFileAndPrefixArguments,
53
56
  retainNewestRunBackupOnly,
54
57
  } from './install.mjs';
55
58
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
@@ -2594,7 +2597,12 @@ test('a stale registration keeps working because the retired hook leaves an iner
2594
2597
  'a retired path no registration names gets no stand-in',
2595
2598
  );
2596
2599
 
2597
- const standInRun = spawnSync(detectPython(), [standInPath], { encoding: 'utf8' });
2600
+ const pythonCommand = pythonFileAndPrefixArguments(detectPython());
2601
+ const standInRun = spawnSync(
2602
+ pythonCommand.file,
2603
+ [...pythonCommand.prefixArguments, standInPath],
2604
+ { encoding: 'utf8' },
2605
+ );
2598
2606
 
2599
2607
  assert.equal(standInRun.status, 0, 'the stand-in exits 0, so a stale registration allows the tool');
2600
2608
  assert.equal(standInRun.stdout, '', 'the stand-in writes nothing to stdout');
@@ -3006,6 +3014,40 @@ test('pruneRetiredHookEntriesFromSettings warns and returns 0 when the host conf
3006
3014
  const PSTACK_TEST_PACKAGE_ROOT = dirname(fileURLToPath(new URL('./install.mjs', import.meta.url)));
3007
3015
  const PSTACK_TEST_INSTALLER_PATH = fileURLToPath(new URL('./install.mjs', import.meta.url));
3008
3016
 
3017
+ test('a failed update preserves an existing skills lookup pointer', () => {
3018
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-install-pointer-'));
3019
+ try {
3020
+ writeFileSync(join(homeDirectory, '.gitconfig'), '');
3021
+ const environment = {
3022
+ ...process.env,
3023
+ HOME: homeDirectory,
3024
+ USERPROFILE: homeDirectory,
3025
+ CLAUDE_CONFIG_DIR: '',
3026
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
3027
+ CDE_INSTALL_PSTACK: '0',
3028
+ CDE_INSTALL_USAGE_WRAPUP: '0',
3029
+ };
3030
+ const argumentsForCoreInstall = [PSTACK_TEST_INSTALLER_PATH, '--only', 'core'];
3031
+ const firstRun = spawnSync(process.execPath, argumentsForCoreInstall, {
3032
+ encoding: 'utf8',
3033
+ env: environment,
3034
+ });
3035
+ assert.equal(firstRun.status, 0, firstRun.stdout + firstRun.stderr);
3036
+ const pointerPath = join(homeDirectory, '.claude', 'skills');
3037
+ const targetPath = readlinkSync(pointerPath);
3038
+
3039
+ const failedRun = spawnSync(process.execPath, argumentsForCoreInstall, {
3040
+ encoding: 'utf8',
3041
+ env: { ...environment, [INSTALL_FAULT_ENV]: FAULT_PHASES.AFTER_FILE_STAGING },
3042
+ });
3043
+ assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
3044
+ assert.equal(lstatSync(pointerPath).isSymbolicLink(), true);
3045
+ assert.equal(readlinkSync(pointerPath), targetPath);
3046
+ } finally {
3047
+ rmSync(homeDirectory, { recursive: true, force: true });
3048
+ }
3049
+ });
3050
+
3009
3051
  function writeRecordingHostCommand(directory, name, logPath) {
3010
3052
  mkdirSync(directory, { recursive: true });
3011
3053
  if (process.platform === 'win32') {
@@ -15,6 +15,8 @@ the cache directory redirected to a temporary directory.
15
15
  """
16
16
 
17
17
  import importlib.util
18
+ import io
19
+ import json
18
20
  import os
19
21
  import sys
20
22
  from pathlib import Path
@@ -53,6 +55,42 @@ def _load_validator() -> ModuleType:
53
55
  return module
54
56
 
55
57
 
58
+ def test_type_error_block_uses_hook_reply_without_notification(
59
+ tmp_path_factory: pytest.TempPathFactory,
60
+ monkeypatch: pytest.MonkeyPatch,
61
+ capsys: pytest.CaptureFixture[str],
62
+ ) -> None:
63
+ validator = _load_validator()
64
+ project_root = tmp_path_factory.mktemp("blocked_project")
65
+ target_file = project_root / "typed_module.py"
66
+ target_file.write_text(TYPE_ERROR_MODULE_SOURCE, encoding="utf-8")
67
+ all_touched_names: list[str] = []
68
+
69
+ class RecordingNotificationModule(ModuleType):
70
+ def __getattr__(self, attribute_name: str) -> object:
71
+ if not attribute_name.startswith("__"):
72
+ all_touched_names.append(attribute_name)
73
+ return lambda *_arguments, **_options: False
74
+
75
+ monkeypatch.setitem(sys.modules, "notification_utils", RecordingNotificationModule("notification_utils"))
76
+ monkeypatch.setattr(validator, "discover_project_root", lambda _target: project_root)
77
+ monkeypatch.setattr(
78
+ validator,
79
+ "run_mypy",
80
+ lambda _target, _root: (1, f"{target_file}:1: error: Incompatible types\n"),
81
+ )
82
+ monkeypatch.setattr(validator, "log_hook_block", lambda **_details: None)
83
+ monkeypatch.setattr(
84
+ sys, "stdin", io.StringIO(json.dumps({"tool_input": {"file_path": str(target_file)}}))
85
+ )
86
+
87
+ with pytest.raises(SystemExit):
88
+ validator.main()
89
+
90
+ assert json.loads(capsys.readouterr().out)["decision"] == "block"
91
+ assert all_touched_names == []
92
+
93
+
56
94
  @pytest.fixture(autouse=True)
57
95
  def isolate_cache_directory(
58
96
  tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch
@@ -182,6 +182,33 @@ def _load_auto_formatter_module() -> ModuleType:
182
182
  return auto_formatter_module
183
183
 
184
184
 
185
+ def test_successful_format_does_not_access_notifications(
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ ) -> None:
188
+ formatter = _load_auto_formatter_module()
189
+ all_touched_names: list[str] = []
190
+
191
+ class RecordingNotificationModule(ModuleType):
192
+ def __getattr__(self, attribute_name: str) -> object:
193
+ if not attribute_name.startswith("__"):
194
+ all_touched_names.append(attribute_name)
195
+ return lambda *_arguments, **_options: False
196
+
197
+ monkeypatch.setitem(sys.modules, "notification_utils", RecordingNotificationModule("notification_utils"))
198
+
199
+ def report_formatter_success(
200
+ command: list[str], _file_path: str, _timeout_seconds: int
201
+ ) -> tuple[subprocess.CompletedProcess[str], bool]:
202
+ return subprocess.CompletedProcess(command, 0, "", ""), False
203
+
204
+ monkeypatch.setattr(formatter, "_run_command", report_formatter_success)
205
+
206
+ formatter.run_eligible_formatter("formatted_module.py")
207
+ formatter._run_prettier("formatted_module.js")
208
+
209
+ assert all_touched_names == []
210
+
211
+
185
212
  def test_formatter_eligibility_requires_write_tool_and_untracked_source(
186
213
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
187
214
  ) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.36.1",
3
+ "version": "8.36.3",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@ def test_rejects_bad_payload_and_schedule() -> None:
26
26
 
27
27
  def test_rejects_unknown_and_private_paths() -> None:
28
28
  assert translate_capability("NoSuchSurface", {})["status"] == "unknown"
29
- assert translate_capability("SendMessage", {"message": "C:\\Users\\melan\\secret"})["status"] == "rejected"
29
+ assert translate_capability("SendMessage", {"message": "C:\\Users\\example\\secret"})["status"] == "rejected"
30
30
 
31
31
 
32
32
  @pytest.mark.parametrize("private_path", ["C:/private/file", "\\\\server\\share\\file", "/private/file", "~/secret", "$HOME/secret", "%USERPROFILE%\\secret", "safe/../secret"])