codex-workspace-bootstrap 0.5.2__tar.gz → 0.6.1__tar.gz
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.
- {codex_workspace_bootstrap-0.5.2/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.6.1}/PKG-INFO +11 -9
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/README.md +10 -8
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/pyproject.toml +1 -1
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/__init__.py +1 -1
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/agents.py +101 -22
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/audit.py +91 -14
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/cli.py +3 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/doctor.py +5 -1
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/fixes.py +1 -1
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/instructions.py +312 -72
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/preflight.py +28 -6
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/sarif.py +4 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +11 -9
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +1 -0
- codex_workspace_bootstrap-0.6.1/tests/test_audit.py +208 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/tests/test_cli.py +112 -3
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/tests/test_doctor.py +17 -0
- codex_workspace_bootstrap-0.6.1/tests/test_instruction_integrity.py +813 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/tests/test_preflight.py +63 -0
- codex_workspace_bootstrap-0.6.1/tests/test_release_consistency.py +49 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/tests/test_sarif.py +22 -0
- codex_workspace_bootstrap-0.5.2/tests/test_audit.py +0 -78
- codex_workspace_bootstrap-0.5.2/tests/test_instruction_integrity.py +0 -381
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/LICENSE +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/setup.cfg +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap/__main__.py +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
- {codex_workspace_bootstrap-0.5.2 → codex_workspace_bootstrap-0.6.1}/tests/test_public_repo_patterns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-workspace-bootstrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them.
|
|
5
5
|
Author: kohli217
|
|
6
6
|
License: MIT
|
|
@@ -124,7 +124,7 @@ The goal is not a vanity score. The result is one of:
|
|
|
124
124
|
- license
|
|
125
125
|
- `.gitignore`
|
|
126
126
|
- common project manifests
|
|
127
|
-
- local Git / Python / Node.js /
|
|
127
|
+
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
128
128
|
|
|
129
129
|
### AI instruction coverage
|
|
130
130
|
|
|
@@ -142,7 +142,7 @@ It does not claim these files are correct merely because they exist. It tells yo
|
|
|
142
142
|
|
|
143
143
|
### Cross-agent instruction integrity
|
|
144
144
|
|
|
145
|
-
When multiple AI instruction files exist, `cwb` reads executable-looking commands and checks them against repository evidence. It understands nested `AGENTS.md` / `AGENTS.override.md`, nested Cursor `.cursor/rules`, and common path-specific frontmatter such as Copilot `applyTo` and rule `globs`. It can flag:
|
|
145
|
+
When multiple AI instruction files exist, `cwb` reads executable-looking commands and checks them against repository evidence. It understands nested `AGENTS.md` / `AGENTS.override.md`, nested `CLAUDE.md`, Gemini CLI hierarchical context files (including project `context.fileName` overrides), nested Cursor `.cursor/rules`, and common path-specific frontmatter such as Copilot `applyTo` and rule `globs`. It can flag:
|
|
146
146
|
|
|
147
147
|
- package-manager mismatches against `packageManager` and lockfiles;
|
|
148
148
|
- cross-agent package-manager drift;
|
|
@@ -248,18 +248,18 @@ cwb doctor .
|
|
|
248
248
|
cwb init-agents .
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
Generation is evidence-based: the tool cross-checks project manifests, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
251
|
+
Generation is evidence-based: the tool cross-checks project manifests, package-manager evidence, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
252
252
|
|
|
253
253
|
## GitHub Action
|
|
254
254
|
|
|
255
255
|
The reusable Action is published on GitHub Marketplace.
|
|
256
256
|
|
|
257
257
|
```yaml
|
|
258
|
-
- uses: actions/checkout@
|
|
259
|
-
- uses: actions/setup-python@
|
|
258
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
259
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
260
260
|
with:
|
|
261
261
|
python-version: "3.13"
|
|
262
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
262
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.6.1
|
|
263
263
|
with:
|
|
264
264
|
path: .
|
|
265
265
|
strict: "true"
|
|
@@ -291,7 +291,9 @@ The intent is to complement those tools, not replace them.
|
|
|
291
291
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
292
292
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
293
293
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
294
|
-
-
|
|
294
|
+
- Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
|
|
295
|
+
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
296
|
+
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
295
297
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
296
298
|
|
|
297
299
|
See [SECURITY.md](SECURITY.md).
|
|
@@ -323,7 +325,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
323
325
|
Pinned GitHub release artifact:
|
|
324
326
|
|
|
325
327
|
```powershell
|
|
326
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
328
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.6.1/codex_workspace_bootstrap-0.6.1-py3-none-any.whl"
|
|
327
329
|
```
|
|
328
330
|
|
|
329
331
|
## Development
|
|
@@ -96,7 +96,7 @@ The goal is not a vanity score. The result is one of:
|
|
|
96
96
|
- license
|
|
97
97
|
- `.gitignore`
|
|
98
98
|
- common project manifests
|
|
99
|
-
- local Git / Python / Node.js /
|
|
99
|
+
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
100
100
|
|
|
101
101
|
### AI instruction coverage
|
|
102
102
|
|
|
@@ -114,7 +114,7 @@ It does not claim these files are correct merely because they exist. It tells yo
|
|
|
114
114
|
|
|
115
115
|
### Cross-agent instruction integrity
|
|
116
116
|
|
|
117
|
-
When multiple AI instruction files exist, `cwb` reads executable-looking commands and checks them against repository evidence. It understands nested `AGENTS.md` / `AGENTS.override.md`, nested Cursor `.cursor/rules`, and common path-specific frontmatter such as Copilot `applyTo` and rule `globs`. It can flag:
|
|
117
|
+
When multiple AI instruction files exist, `cwb` reads executable-looking commands and checks them against repository evidence. It understands nested `AGENTS.md` / `AGENTS.override.md`, nested `CLAUDE.md`, Gemini CLI hierarchical context files (including project `context.fileName` overrides), nested Cursor `.cursor/rules`, and common path-specific frontmatter such as Copilot `applyTo` and rule `globs`. It can flag:
|
|
118
118
|
|
|
119
119
|
- package-manager mismatches against `packageManager` and lockfiles;
|
|
120
120
|
- cross-agent package-manager drift;
|
|
@@ -220,18 +220,18 @@ cwb doctor .
|
|
|
220
220
|
cwb init-agents .
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
Generation is evidence-based: the tool cross-checks project manifests, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
223
|
+
Generation is evidence-based: the tool cross-checks project manifests, package-manager evidence, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
224
224
|
|
|
225
225
|
## GitHub Action
|
|
226
226
|
|
|
227
227
|
The reusable Action is published on GitHub Marketplace.
|
|
228
228
|
|
|
229
229
|
```yaml
|
|
230
|
-
- uses: actions/checkout@
|
|
231
|
-
- uses: actions/setup-python@
|
|
230
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
231
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
232
232
|
with:
|
|
233
233
|
python-version: "3.13"
|
|
234
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
234
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.6.1
|
|
235
235
|
with:
|
|
236
236
|
path: .
|
|
237
237
|
strict: "true"
|
|
@@ -263,7 +263,9 @@ The intent is to complement those tools, not replace them.
|
|
|
263
263
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
264
264
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
265
265
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
266
|
-
-
|
|
266
|
+
- Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
|
|
267
|
+
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
268
|
+
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
267
269
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
268
270
|
|
|
269
271
|
See [SECURITY.md](SECURITY.md).
|
|
@@ -295,7 +297,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
295
297
|
Pinned GitHub release artifact:
|
|
296
298
|
|
|
297
299
|
```powershell
|
|
298
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
300
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.6.1/codex_workspace_bootstrap-0.6.1-py3-none-any.whl"
|
|
299
301
|
```
|
|
300
302
|
|
|
301
303
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-workspace-bootstrap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -18,18 +18,22 @@ class ValidationCommand:
|
|
|
18
18
|
review_required: bool = False
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _regular_file(path: Path) -> bool:
|
|
22
|
+
return path.is_file() and not path.is_symlink()
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
def detect_project_signals(root: Path) -> list[str]:
|
|
22
26
|
signals: list[str] = []
|
|
23
|
-
if any((root / marker)
|
|
27
|
+
if any(_regular_file(root / marker) for marker in PYTHON_MARKERS):
|
|
24
28
|
signals.append("Python")
|
|
25
|
-
if (root / NODE_MARKER)
|
|
29
|
+
if _regular_file(root / NODE_MARKER):
|
|
26
30
|
signals.append("Node.js")
|
|
27
31
|
return signals
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def _node_script_names(root: Path) -> set[str]:
|
|
31
35
|
package_json = root / NODE_MARKER
|
|
32
|
-
if not package_json.
|
|
36
|
+
if not package_json.is_file() or package_json.is_symlink():
|
|
33
37
|
return set()
|
|
34
38
|
|
|
35
39
|
try:
|
|
@@ -44,10 +48,48 @@ def _node_script_names(root: Path) -> set[str]:
|
|
|
44
48
|
return {name for name, value in scripts.items() if isinstance(name, str) and isinstance(value, str)}
|
|
45
49
|
|
|
46
50
|
|
|
51
|
+
def detect_node_package_managers(root: Path) -> set[str]:
|
|
52
|
+
managers: set[str] = set()
|
|
53
|
+
package_json = root / NODE_MARKER
|
|
54
|
+
|
|
55
|
+
if package_json.is_file() and not package_json.is_symlink():
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
58
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
59
|
+
data = {}
|
|
60
|
+
if isinstance(data, dict):
|
|
61
|
+
declared = data.get("packageManager")
|
|
62
|
+
if isinstance(declared, str) and declared:
|
|
63
|
+
manager = declared.split("@", 1)[0].strip().lower()
|
|
64
|
+
if manager in {"npm", "pnpm", "yarn", "bun"}:
|
|
65
|
+
managers.add(manager)
|
|
66
|
+
|
|
67
|
+
lockfiles = {
|
|
68
|
+
"package-lock.json": "npm",
|
|
69
|
+
"npm-shrinkwrap.json": "npm",
|
|
70
|
+
"pnpm-lock.yaml": "pnpm",
|
|
71
|
+
"yarn.lock": "yarn",
|
|
72
|
+
"bun.lock": "bun",
|
|
73
|
+
"bun.lockb": "bun",
|
|
74
|
+
}
|
|
75
|
+
for filename, manager in lockfiles.items():
|
|
76
|
+
candidate = root / filename
|
|
77
|
+
if candidate.is_file() and not candidate.is_symlink():
|
|
78
|
+
managers.add(manager)
|
|
79
|
+
|
|
80
|
+
return managers
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _package_script_command(manager: str, script: str) -> str:
|
|
84
|
+
if manager == "npm" and script == "test":
|
|
85
|
+
return "npm test"
|
|
86
|
+
return f"{manager} run {script}"
|
|
87
|
+
|
|
88
|
+
|
|
47
89
|
def _readme_text(root: Path) -> str:
|
|
48
90
|
for name in README_NAMES:
|
|
49
91
|
path = root / name
|
|
50
|
-
if path.is_file():
|
|
92
|
+
if path.is_file() and not path.is_symlink():
|
|
51
93
|
try:
|
|
52
94
|
return path.read_text(encoding="utf-8")
|
|
53
95
|
except (OSError, UnicodeDecodeError):
|
|
@@ -64,10 +106,10 @@ def _documented_commands(root: Path) -> set[str]:
|
|
|
64
106
|
"python -m pytest",
|
|
65
107
|
"pytest",
|
|
66
108
|
"python -m unittest",
|
|
67
|
-
"npm test",
|
|
68
|
-
"npm run lint",
|
|
69
|
-
"npm run build",
|
|
70
109
|
}
|
|
110
|
+
for manager in ("npm", "pnpm", "yarn", "bun"):
|
|
111
|
+
for script in ("test", "lint", "build"):
|
|
112
|
+
candidates.add(_package_script_command(manager, script))
|
|
71
113
|
found: set[str] = set()
|
|
72
114
|
for command in candidates:
|
|
73
115
|
pattern = rf"(?m)(^|[\s$>]){re.escape(command)}(?=$|\s)"
|
|
@@ -78,7 +120,7 @@ def _documented_commands(root: Path) -> set[str]:
|
|
|
78
120
|
|
|
79
121
|
def _pyproject_has_pytest(root: Path) -> bool:
|
|
80
122
|
path = root / "pyproject.toml"
|
|
81
|
-
if not path.is_file():
|
|
123
|
+
if not path.is_file() or path.is_symlink():
|
|
82
124
|
return False
|
|
83
125
|
|
|
84
126
|
try:
|
|
@@ -116,8 +158,8 @@ def validation_plan(root: Path) -> list[ValidationCommand]:
|
|
|
116
158
|
if "Python" in signals:
|
|
117
159
|
pytest_evidence = (
|
|
118
160
|
_pyproject_has_pytest(root)
|
|
119
|
-
or (root / "pytest.ini")
|
|
120
|
-
or (root / "conftest.py")
|
|
161
|
+
or _regular_file(root / "pytest.ini")
|
|
162
|
+
or _regular_file(root / "conftest.py")
|
|
121
163
|
)
|
|
122
164
|
if "python -m pytest" in documented or "pytest" in documented:
|
|
123
165
|
plan.append(ValidationCommand("python -m pytest", "documented in README"))
|
|
@@ -136,19 +178,56 @@ def validation_plan(root: Path) -> list[ValidationCommand]:
|
|
|
136
178
|
|
|
137
179
|
if "Node.js" in signals:
|
|
138
180
|
scripts = _node_script_names(root)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
managers = detect_node_package_managers(root)
|
|
182
|
+
manager = next(iter(managers)) if len(managers) == 1 else None
|
|
183
|
+
|
|
184
|
+
if manager:
|
|
185
|
+
if "test" in scripts:
|
|
186
|
+
plan.append(
|
|
187
|
+
ValidationCommand(
|
|
188
|
+
_package_script_command(manager, "test"),
|
|
189
|
+
f"package.json defines scripts.test and repository evidence selects {manager}",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
if "lint" in scripts:
|
|
193
|
+
plan.append(
|
|
194
|
+
ValidationCommand(
|
|
195
|
+
_package_script_command(manager, "lint"),
|
|
196
|
+
f"package.json defines scripts.lint and repository evidence selects {manager}",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
build_command = _package_script_command(manager, "build")
|
|
200
|
+
if "build" in scripts and build_command in documented:
|
|
201
|
+
plan.append(
|
|
202
|
+
ValidationCommand(
|
|
203
|
+
build_command,
|
|
204
|
+
f"package.json defines scripts.build, repository evidence selects {manager}, and README documents it",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
elif len(managers) > 1:
|
|
208
|
+
# Conflicting package-manager evidence is handled by preflight.
|
|
209
|
+
# Do not manufacture a manager-specific validation command here.
|
|
210
|
+
pass
|
|
211
|
+
else:
|
|
212
|
+
# package.json alone does not prove npm. Preserve the historical
|
|
213
|
+
# npm suggestions only as review-required hints until the project
|
|
214
|
+
# supplies a lockfile, packageManager field, or documented command.
|
|
215
|
+
if "test" in scripts:
|
|
216
|
+
plan.append(
|
|
217
|
+
ValidationCommand(
|
|
218
|
+
"npm test",
|
|
219
|
+
"package.json defines scripts.test, but the package manager is not confirmed",
|
|
220
|
+
review_required=True,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
if "lint" in scripts:
|
|
224
|
+
plan.append(
|
|
225
|
+
ValidationCommand(
|
|
226
|
+
"npm run lint",
|
|
227
|
+
"package.json defines scripts.lint, but the package manager is not confirmed",
|
|
228
|
+
review_required=True,
|
|
229
|
+
)
|
|
150
230
|
)
|
|
151
|
-
)
|
|
152
231
|
|
|
153
232
|
plan.extend(
|
|
154
233
|
[
|
|
@@ -2,10 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import asdict, dataclass
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
from typing import Iterable
|
|
8
9
|
|
|
10
|
+
from .agents import detect_node_package_managers
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
@dataclass(frozen=True)
|
|
11
14
|
class Check:
|
|
@@ -22,7 +25,6 @@ COMMON_TOOLS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
22
25
|
("git", ("git", "--version")),
|
|
23
26
|
("python", ("python", "--version")),
|
|
24
27
|
("node", ("node", "--version")),
|
|
25
|
-
("npm", ("npm", "--version")),
|
|
26
28
|
("powershell", ("powershell", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()")),
|
|
27
29
|
("wsl", ("wsl", "--status")),
|
|
28
30
|
("codex", ("codex", "--version")),
|
|
@@ -49,6 +51,13 @@ RISK_FILENAMES = {
|
|
|
49
51
|
|
|
50
52
|
RISK_SUFFIXES = (".pem", ".p12", ".pfx", ".key")
|
|
51
53
|
|
|
54
|
+
NODE_PACKAGE_MANAGER_COMMANDS: dict[str, tuple[str, ...]] = {
|
|
55
|
+
"npm": ("npm", "--version"),
|
|
56
|
+
"pnpm": ("pnpm", "--version"),
|
|
57
|
+
"yarn": ("yarn", "--version"),
|
|
58
|
+
"bun": ("bun", "--version"),
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
|
|
53
62
|
def _tool_check(label: str, command: tuple[str, ...]) -> Check:
|
|
54
63
|
executable = shutil.which(command[0])
|
|
@@ -73,12 +82,30 @@ def _tool_check(label: str, command: tuple[str, ...]) -> Check:
|
|
|
73
82
|
|
|
74
83
|
|
|
75
84
|
def _iter_project_files(root: Path) -> Iterable[Path]:
|
|
76
|
-
excluded = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
excluded = {
|
|
86
|
+
".git",
|
|
87
|
+
".venv",
|
|
88
|
+
"venv",
|
|
89
|
+
"node_modules",
|
|
90
|
+
"__pycache__",
|
|
91
|
+
".pytest_cache",
|
|
92
|
+
".mypy_cache",
|
|
93
|
+
".ruff_cache",
|
|
94
|
+
".tox",
|
|
95
|
+
".nox",
|
|
96
|
+
"dist",
|
|
97
|
+
"build",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for current, dirnames, filenames in os.walk(root, followlinks=False):
|
|
101
|
+
current_path = Path(current)
|
|
102
|
+
dirnames[:] = [
|
|
103
|
+
name
|
|
104
|
+
for name in dirnames
|
|
105
|
+
if name not in excluded and not (current_path / name).is_symlink()
|
|
106
|
+
]
|
|
107
|
+
for filename in filenames:
|
|
108
|
+
yield current_path / filename
|
|
82
109
|
|
|
83
110
|
|
|
84
111
|
def _git_matches(root: Path, args: tuple[str, ...]) -> bool:
|
|
@@ -132,7 +159,14 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
132
159
|
)
|
|
133
160
|
)
|
|
134
161
|
|
|
135
|
-
readme = next(
|
|
162
|
+
readme = next(
|
|
163
|
+
(
|
|
164
|
+
p
|
|
165
|
+
for p in root.iterdir()
|
|
166
|
+
if p.is_file() and not p.is_symlink() and p.name.lower().startswith("readme")
|
|
167
|
+
),
|
|
168
|
+
None,
|
|
169
|
+
)
|
|
136
170
|
checks.append(
|
|
137
171
|
Check(
|
|
138
172
|
"readme",
|
|
@@ -141,7 +175,14 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
141
175
|
)
|
|
142
176
|
)
|
|
143
177
|
|
|
144
|
-
license_file = next(
|
|
178
|
+
license_file = next(
|
|
179
|
+
(
|
|
180
|
+
p
|
|
181
|
+
for p in root.iterdir()
|
|
182
|
+
if p.is_file() and not p.is_symlink() and p.name.lower().startswith("license")
|
|
183
|
+
),
|
|
184
|
+
None,
|
|
185
|
+
)
|
|
145
186
|
checks.append(
|
|
146
187
|
Check(
|
|
147
188
|
"license",
|
|
@@ -153,20 +194,32 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
153
194
|
checks.append(
|
|
154
195
|
Check(
|
|
155
196
|
"gitignore",
|
|
156
|
-
"pass"
|
|
157
|
-
|
|
197
|
+
"pass"
|
|
198
|
+
if (root / ".gitignore").is_file() and not (root / ".gitignore").is_symlink()
|
|
199
|
+
else "warn",
|
|
200
|
+
".gitignore detected"
|
|
201
|
+
if (root / ".gitignore").is_file() and not (root / ".gitignore").is_symlink()
|
|
202
|
+
else ".gitignore not found",
|
|
158
203
|
)
|
|
159
204
|
)
|
|
160
205
|
|
|
161
206
|
checks.append(
|
|
162
207
|
Check(
|
|
163
208
|
"agents",
|
|
164
|
-
"pass"
|
|
165
|
-
|
|
209
|
+
"pass"
|
|
210
|
+
if (root / "AGENTS.md").is_file() and not (root / "AGENTS.md").is_symlink()
|
|
211
|
+
else "warn",
|
|
212
|
+
"AGENTS.md detected"
|
|
213
|
+
if (root / "AGENTS.md").is_file() and not (root / "AGENTS.md").is_symlink()
|
|
214
|
+
else "AGENTS.md not found; run init-agents",
|
|
166
215
|
)
|
|
167
216
|
)
|
|
168
217
|
|
|
169
|
-
manifests = [
|
|
218
|
+
manifests = [
|
|
219
|
+
name
|
|
220
|
+
for name in MANIFESTS
|
|
221
|
+
if (root / name).is_file() and not (root / name).is_symlink()
|
|
222
|
+
]
|
|
170
223
|
checks.append(
|
|
171
224
|
Check(
|
|
172
225
|
"project-manifest",
|
|
@@ -178,6 +231,30 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
178
231
|
for label, command in COMMON_TOOLS:
|
|
179
232
|
checks.append(_tool_check(label, command))
|
|
180
233
|
|
|
234
|
+
if (root / "package.json").is_file() and not (root / "package.json").is_symlink():
|
|
235
|
+
managers = detect_node_package_managers(root)
|
|
236
|
+
if managers:
|
|
237
|
+
if len(managers) > 1:
|
|
238
|
+
checks.append(
|
|
239
|
+
Check(
|
|
240
|
+
"package-manager-evidence",
|
|
241
|
+
"warn",
|
|
242
|
+
"Conflicting Node.js package-manager evidence detected: "
|
|
243
|
+
+ ", ".join(sorted(managers)),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
for manager in sorted(managers):
|
|
247
|
+
command = NODE_PACKAGE_MANAGER_COMMANDS[manager]
|
|
248
|
+
checks.append(_tool_check(manager, command))
|
|
249
|
+
else:
|
|
250
|
+
checks.append(
|
|
251
|
+
Check(
|
|
252
|
+
"package-manager-evidence",
|
|
253
|
+
"warn",
|
|
254
|
+
"Node.js project detected, but packageManager/lockfile evidence does not confirm npm, pnpm, yarn, or bun",
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
181
258
|
risky: list[str] = []
|
|
182
259
|
for path in _iter_project_files(root):
|
|
183
260
|
name = path.name.lower()
|
|
@@ -270,6 +270,9 @@ def _run_init_agents(path: str, force: bool) -> int:
|
|
|
270
270
|
return 2
|
|
271
271
|
|
|
272
272
|
target = root / "AGENTS.md"
|
|
273
|
+
if target.is_symlink():
|
|
274
|
+
print(f"error: refusing to write through symlink: {target}", file=sys.stderr)
|
|
275
|
+
return 1
|
|
273
276
|
if target.exists() and not force:
|
|
274
277
|
print(f"error: {target} already exists; use --force to overwrite", file=sys.stderr)
|
|
275
278
|
return 1
|
|
@@ -23,7 +23,11 @@ GUIDANCE: dict[str, str] = {
|
|
|
23
23
|
"git": "Verify Git is installed and available on PATH.",
|
|
24
24
|
"python": "Verify Python or the Windows Python launcher is installed and available on PATH.",
|
|
25
25
|
"node": "Verify Node.js is installed and available on PATH when the project requires it.",
|
|
26
|
-
"npm": "Verify npm is available
|
|
26
|
+
"npm": "Verify npm is installed and available on PATH for this repository.",
|
|
27
|
+
"pnpm": "Verify pnpm is installed and available on PATH for this repository.",
|
|
28
|
+
"yarn": "Verify Yarn is installed and available on PATH for this repository.",
|
|
29
|
+
"bun": "Verify Bun is installed and available on PATH for this repository.",
|
|
30
|
+
"package-manager-evidence": "Confirm the intended Node.js package manager in package.json packageManager or with the repository lockfile.",
|
|
27
31
|
"powershell": "Verify Windows PowerShell or PowerShell is available on PATH.",
|
|
28
32
|
"wsl": "Check WSL status only when the repository workflow requires Linux tooling.",
|
|
29
33
|
"codex": "Verify the Codex CLI is installed and available on PATH when local Codex workflows are intended.",
|
|
@@ -64,7 +64,7 @@ def apply_fix_plan(root: Path, plan: list[FixPlanItem]) -> list[str]:
|
|
|
64
64
|
continue
|
|
65
65
|
if item.kind == "create-agents":
|
|
66
66
|
target = root / "AGENTS.md"
|
|
67
|
-
if target.exists():
|
|
67
|
+
if target.exists() or target.is_symlink():
|
|
68
68
|
continue
|
|
69
69
|
target.write_text(generate_agents(root), encoding="utf-8")
|
|
70
70
|
applied.append(str(target))
|