ai-dev-cli-tools 0.5.0a1__py3-none-any.whl
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.
- ai_dev_cli_tools-0.5.0a1.dist-info/METADATA +240 -0
- ai_dev_cli_tools-0.5.0a1.dist-info/RECORD +60 -0
- ai_dev_cli_tools-0.5.0a1.dist-info/WHEEL +4 -0
- ai_dev_cli_tools-0.5.0a1.dist-info/entry_points.txt +2 -0
- ai_dev_cli_tools-0.5.0a1.dist-info/licenses/LICENSE +21 -0
- ai_dev_tools/__init__.py +3 -0
- ai_dev_tools/cache/__init__.py +11 -0
- ai_dev_tools/cache/graph.py +136 -0
- ai_dev_tools/cache/repository.py +169 -0
- ai_dev_tools/cache/validation.py +154 -0
- ai_dev_tools/cli.py +387 -0
- ai_dev_tools/completion.py +72 -0
- ai_dev_tools/config.py +223 -0
- ai_dev_tools/context/__init__.py +5 -0
- ai_dev_tools/context/builder.py +506 -0
- ai_dev_tools/context/incremental.py +107 -0
- ai_dev_tools/context/models.py +59 -0
- ai_dev_tools/context/profiles.py +49 -0
- ai_dev_tools/context/selection.py +270 -0
- ai_dev_tools/context/symbols.py +178 -0
- ai_dev_tools/detectors/__init__.py +1 -0
- ai_dev_tools/detectors/environment.py +125 -0
- ai_dev_tools/detectors/project.py +189 -0
- ai_dev_tools/detectors/repository_map.py +129 -0
- ai_dev_tools/detectors/runtime.py +190 -0
- ai_dev_tools/detectors/workspaces.py +228 -0
- ai_dev_tools/git/__init__.py +1 -0
- ai_dev_tools/git/inspect.py +219 -0
- ai_dev_tools/models/__init__.py +1 -0
- ai_dev_tools/models/report.py +95 -0
- ai_dev_tools/models/workspace.py +48 -0
- ai_dev_tools/parsers/__init__.py +1 -0
- ai_dev_tools/parsers/logs.py +372 -0
- ai_dev_tools/parsers/registry.py +60 -0
- ai_dev_tools/reporters/__init__.py +1 -0
- ai_dev_tools/reporters/progressive.py +161 -0
- ai_dev_tools/reporters/writer.py +74 -0
- ai_dev_tools/runners/__init__.py +1 -0
- ai_dev_tools/runners/baseline.py +190 -0
- ai_dev_tools/runners/bootstrap.py +191 -0
- ai_dev_tools/runners/bootstrap_models.py +64 -0
- ai_dev_tools/runners/bootstrap_strategies.py +444 -0
- ai_dev_tools/runners/cache.py +23 -0
- ai_dev_tools/runners/check.py +509 -0
- ai_dev_tools/runners/check_checkpoint.py +50 -0
- ai_dev_tools/runners/check_models.py +51 -0
- ai_dev_tools/runners/check_scheduler.py +94 -0
- ai_dev_tools/runners/check_selection.py +267 -0
- ai_dev_tools/runners/diagnostics.py +96 -0
- ai_dev_tools/runners/feedback.py +193 -0
- ai_dev_tools/runners/finish.py +105 -0
- ai_dev_tools/runners/focused.py +37 -0
- ai_dev_tools/runners/index.py +44 -0
- ai_dev_tools/runtime/__init__.py +3 -0
- ai_dev_tools/runtime/runner.py +380 -0
- ai_dev_tools/runtime/supervisor.py +145 -0
- ai_dev_tools/security/__init__.py +1 -0
- ai_dev_tools/security/secrets.py +58 -0
- ai_dev_tools/utils/__init__.py +1 -0
- ai_dev_tools/utils/subprocess.py +74 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-dev-cli-tools
|
|
3
|
+
Version: 0.5.0a1
|
|
4
|
+
Summary: Cross-platform CLI helpers that give AI coding agents concise, deterministic development reports.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MatthiasLew/ai-dev-cli-tools
|
|
6
|
+
Project-URL: Repository, https://github.com/MatthiasLew/ai-dev-cli-tools
|
|
7
|
+
Project-URL: Issues, https://github.com/MatthiasLew/ai-dev-cli-tools/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/MatthiasLew/ai-dev-cli-tools/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Mateusz Lewandowski
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,automation,cli,developer-tools
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: coverage[toml]>=7.6; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# AI Dev CLI Tools
|
|
31
|
+
|
|
32
|
+
Cross-platform Python CLI helpers for AI coding agents and humans who want concise, deterministic development reports instead of huge logs.
|
|
33
|
+
|
|
34
|
+
`ai-dev` runs repeatable project checks locally, stores full logs under `.ai/logs/`, and returns compact Markdown/JSON summaries under `.ai/reports/`.
|
|
35
|
+
|
|
36
|
+
## Problem
|
|
37
|
+
|
|
38
|
+
AI coding agents often spend tokens reading full test output, repository trees, dependency noise, repeated warnings, and raw Git diffs. This project follows one rule:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
Scripts do the work and collect data.
|
|
42
|
+
AI reads a short report and decides what to do next.
|
|
43
|
+
Full logs stay on disk until needed.
|
|
44
|
+
|
|
45
|
+
Generated state under `.ai/logs/`, `.ai/reports/`, `.ai/context/`, `.ai/cache/`,
|
|
46
|
+
`.ai/runtime/`, and `.ai/tmp/` is local and ignored by Git. Logs, reports, and context
|
|
47
|
+
artifacts may be removed when no longer needed; cache, runtime, and temporary state must always
|
|
48
|
+
be safe for `ai-dev` to recreate.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pip install --upgrade pipx
|
|
55
|
+
pipx install ai-dev-cli-tools==0.5.0a1
|
|
56
|
+
ai-dev --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a source checkout before the public alpha is published, use `pipx install .`. See
|
|
60
|
+
`docs/DISTRIBUTION.md` for the Trusted Publishing and upgrade policy.
|
|
61
|
+
|
|
62
|
+
For development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m pip install -e ".[dev]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Windows
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
.\ai.ps1 doctor
|
|
72
|
+
ai-dev scan --project "C:\path with spaces\project"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Linux and macOS
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
./ai.sh doctor
|
|
79
|
+
ai-dev check --mode fast --project "/path/with spaces/project"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
ai-dev doctor
|
|
86
|
+
ai-dev scan
|
|
87
|
+
ai-dev map --max-files 500 --max-depth 6
|
|
88
|
+
ai-dev check --mode fast
|
|
89
|
+
ai-dev check --mode changed # reports changed files and falls back safely when test mapping is uncertain
|
|
90
|
+
ai-dev check --mode full --jobs 4
|
|
91
|
+
ai-dev check --mode changed --policy feedback-first --resume
|
|
92
|
+
ai-dev index update
|
|
93
|
+
ai-dev cache status
|
|
94
|
+
ai-dev baseline create main
|
|
95
|
+
ai-dev baseline compare main
|
|
96
|
+
ai-dev explain issue:<id> --tail 100
|
|
97
|
+
ai-dev feedback --task "fix authentication timeout"
|
|
98
|
+
ai-dev session status
|
|
99
|
+
ai-dev diagnostics
|
|
100
|
+
ai-dev completion bash
|
|
101
|
+
ai-dev test affected
|
|
102
|
+
ai-dev logs summarize
|
|
103
|
+
ai-dev context build
|
|
104
|
+
ai-dev git status
|
|
105
|
+
ai-dev git inspect
|
|
106
|
+
ai-dev finish
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
All commands support `--project`, `--json`, `--quiet`, `--help`, and `--version` at the top level.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Bootstrap
|
|
113
|
+
|
|
114
|
+
`ai-dev bootstrap` prepares a detected project with conservative, project-local commands. Use `--explain` to see the plan without modifications, `--dry-run` to validate planning without executing modifying commands, and `--create-env` to allow copying `.env.example` to `.env` only when `.env` is missing.
|
|
115
|
+
|
|
116
|
+
Supported strategies include Python uv, Poetry, pip with `pyproject.toml`, pip with `requirements.txt`, Node npm/pnpm/Yarn, Maven wrapper or system Maven, Gradle wrapper or system Gradle, Cargo, and Composer.
|
|
117
|
+
|
|
118
|
+
See `docs/BOOTSTRAP.md` for safety rules and configuration.
|
|
119
|
+
|
|
120
|
+
## Managed application runtime
|
|
121
|
+
|
|
122
|
+
`ai-dev run` supports explain, dry-run, foreground, and supervised background modes.
|
|
123
|
+
`ai-dev stop` sends a token-authenticated request to the matching local supervisor and never
|
|
124
|
+
kills an arbitrary PID read from stale state. See `docs/RUNTIME.md`.
|
|
125
|
+
|
|
126
|
+
## Context Builder
|
|
127
|
+
|
|
128
|
+
`ai-dev context build` creates a bounded local context package for coding agents without calling any LLM, embedding API, or cloud service.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ai-dev context build --task "fix auth tests"
|
|
132
|
+
ai-dev context build --changed-only --max-chars 50000
|
|
133
|
+
ai-dev context build --incremental # emits only candidates changed since the last pack
|
|
134
|
+
ai-dev context build --profile review
|
|
135
|
+
ai-dev context build --include "src/**/*.py" --exclude "tests/fixtures/**"
|
|
136
|
+
ai-dev context build --explain --json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Artifacts are written to `.ai/context/context-latest.md` and `.ai/context/context-latest.json` by default. The builder includes detected technologies, git state, changed files, related tests, validation plan, recent commits, selected snippets, limited diffs, latest check errors, masked secret findings, and budget/truncation metadata. Large Python files use AST-aware symbol snippets with line ranges instead of blindly returning only the beginning of the file.
|
|
140
|
+
|
|
141
|
+
Incremental mode stores a schema-versioned manifest under `.ai/cache/` and reports changed versus reused files. Default limits are `--max-chars 50000`, `--max-files 30`, `--max-file-chars 8000`, and `--max-diff-chars 15000`. Secret-bearing and generated paths such as `.env`, private keys, caches, build output, `.ai/logs`, and `.ai/reports` are excluded from snippets.
|
|
142
|
+
## Reports and Logs
|
|
143
|
+
|
|
144
|
+
Validation results are cached by default using repository, command, workspace, runtime, and platform fingerprints; use `check --no-cache` to force execution. `check --resume` reuses only exact successful checkpoint fingerprints. `--policy feedback-first` runs cheaper waves first and cancels later expensive waves after a required failure; `complete` retains comprehensive execution. `index status/update/rebuild` manages the reusable repository index, while `cache status/prune/clear` provides bounded local cache maintenance. See `docs/CACHE_AND_INDEX.md`.
|
|
145
|
+
|
|
146
|
+
Short reports are written to `.ai/reports/` as Markdown and JSON. Full command output is written to `.ai/logs/` and ignored by Git. Check summaries include exit codes, durations, first failure hints, grouped repeated messages, test counts, and full log paths.
|
|
147
|
+
|
|
148
|
+
JSON reports use schema `1.1` with `schema_version`, `tool_version`, `command`, `status`, `exit_code`, timestamps, `project_root`, `summary`, `issues`, `artifacts`, and `metadata`.
|
|
149
|
+
|
|
150
|
+
`ai-dev feedback` combines Git changes, changed validation, incremental context, focused rerun hints, stage timings, and local session state into one compact agent protocol report.
|
|
151
|
+
|
|
152
|
+
Every expandable issue, check, file, snippet, diff, workspace, and artifact receives a stable local `evidence_id`. The report metadata lists references; `ai-dev explain <evidence-id> --tail 100` retrieves only that evidence. `ai-dev baseline create <name>` stores a compact local snapshot under `.ai/cache/baselines/`, and `baseline compare <name>` leads with new/resolved failures, issue codes, and status regressions.
|
|
153
|
+
|
|
154
|
+
## Auto Detection
|
|
155
|
+
|
|
156
|
+
`scan` detects Python, Node.js, Java, Rust, PHP, Docker, Make, CI workflows, package managers, scripts, entrypoints, tests, lint, formatters, type checkers, and `.env.example` variables.
|
|
157
|
+
|
|
158
|
+
Supported examples:
|
|
159
|
+
|
|
160
|
+
- Python: `pyproject.toml`, `requirements.txt`, `pytest`, `ruff`, `black`, `mypy`, `coverage`.
|
|
161
|
+
- Node.js: `package.json`, npm, pnpm, yarn, Jest, Vitest, ESLint, Prettier, TypeScript.
|
|
162
|
+
- Java: Maven, Gradle, tests, Checkstyle, SpotBugs, JaCoCo when configured.
|
|
163
|
+
- Rust: Cargo test, fmt, clippy.
|
|
164
|
+
- PHP: Composer, PHPUnit, PHPStan, PHP-CS-Fixer when configured.
|
|
165
|
+
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
Optional `.ai-dev-tools.toml`:
|
|
169
|
+
|
|
170
|
+
```toml
|
|
171
|
+
[project]
|
|
172
|
+
name = "example-project"
|
|
173
|
+
|
|
174
|
+
[commands]
|
|
175
|
+
test = "pytest"
|
|
176
|
+
lint = "ruff check ."
|
|
177
|
+
typecheck = "mypy src"
|
|
178
|
+
|
|
179
|
+
[ignore]
|
|
180
|
+
paths = ["node_modules", ".venv", "dist", "build"]
|
|
181
|
+
|
|
182
|
+
[reports]
|
|
183
|
+
directory = ".ai/reports"
|
|
184
|
+
logs_directory = ".ai/logs"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Configuration takes precedence over auto detection. Invalid or unknown configuration is reported through `config_warnings` instead of crashing normal scans.
|
|
188
|
+
## Local Validation
|
|
189
|
+
|
|
190
|
+
Before pushing a larger stage, run:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
python -m ruff check .
|
|
194
|
+
python -m mypy src tests scripts
|
|
195
|
+
python -m coverage run -m pytest
|
|
196
|
+
python -m coverage report --fail-under=90
|
|
197
|
+
python -m build
|
|
198
|
+
python scripts/validate_ci.py
|
|
199
|
+
python scripts/test_installed_package.py
|
|
200
|
+
git diff --check
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`python scripts/test_installed_package.py` rebuilds the wheel, installs only that wheel into a clean virtual environment in a path containing spaces, and verifies the installed `ai-dev` entrypoint without editable install or `PYTHONPATH`.
|
|
204
|
+
|
|
205
|
+
## Command Status
|
|
206
|
+
|
|
207
|
+
| Command | Status |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| doctor | implemented |
|
|
210
|
+
| scan | implemented |
|
|
211
|
+
| map | implemented |
|
|
212
|
+
| check | implemented |
|
|
213
|
+
| check --explain | implemented |
|
|
214
|
+
| test affected | implemented |
|
|
215
|
+
| index status/update/rebuild | implemented |
|
|
216
|
+
| cache status/prune/clear | implemented |
|
|
217
|
+
| logs summarize | implemented |
|
|
218
|
+
| context build | implemented |
|
|
219
|
+
| diagnostics | implemented |
|
|
220
|
+
| capabilities | implemented |
|
|
221
|
+
| git status | implemented |
|
|
222
|
+
| git inspect | implemented |
|
|
223
|
+
| finish | implemented |
|
|
224
|
+
| bootstrap | implemented |
|
|
225
|
+
| run | implemented |
|
|
226
|
+
| stop | implemented |
|
|
227
|
+
|
|
228
|
+
## Scope Notes
|
|
229
|
+
|
|
230
|
+
- Monorepo/workspace detection and per-subproject command routing: implemented.
|
|
231
|
+
- Per-subproject check and bootstrap working directories: implemented.
|
|
232
|
+
- Runtime version validation: partial.
|
|
233
|
+
- `context build` is implemented as a bounded local context pack builder.
|
|
234
|
+
- `bootstrap` is implemented as a conservative local setup planner/executor.
|
|
235
|
+
- Auto-commit, auto-push, and GUI remain out of scope or planned.
|
|
236
|
+
|
|
237
|
+
## Intentional Limits
|
|
238
|
+
Version 0.5.0a1 does not reset, clean, commit, push, merge, clone organizations, synchronize repositories, delete containers, publish releases, or remove user files.
|
|
239
|
+
|
|
240
|
+
Shell completion scripts are generated with `ai-dev completion bash|zsh|fish|powershell` and can be sourced or installed using the normal mechanism for the selected shell.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
ai_dev_tools/__init__.py,sha256=gcZy3Dv5W7LjZwt6A24kOUR1D9TCRyDsf_LFbf7unoA,49
|
|
2
|
+
ai_dev_tools/cli.py,sha256=ilgDBx9zc8V9mgwRe_08wzobrKvSoJQh1hFNLQ8pIwQ,14246
|
|
3
|
+
ai_dev_tools/completion.py,sha256=pZwxo4KoueUeDIK1C8013o_T0517nhf1hrzuSnw12DI,1929
|
|
4
|
+
ai_dev_tools/config.py,sha256=uZsEn7xe2XHHnknYmRg1qhg5FNgwBZm3L8zKPVPDezM,7998
|
|
5
|
+
ai_dev_tools/cache/__init__.py,sha256=cQ0ONFQ2MRhwnabuooeiiR9iKgujoWpxFTtgrgnOUn0,235
|
|
6
|
+
ai_dev_tools/cache/graph.py,sha256=WOpm_TLLgUOK4SCxhkt7llEvCKUx12joZgstTJzayVk,5296
|
|
7
|
+
ai_dev_tools/cache/repository.py,sha256=2t9_5vu5U0zaJ1UDhYh35joQPSiv2I8Yw0A9rzNEeoU,5506
|
|
8
|
+
ai_dev_tools/cache/validation.py,sha256=j8Vl9dAsgMc660zzs0yw5qG5j3DTQt6IgUJ2EsStSjA,4141
|
|
9
|
+
ai_dev_tools/context/__init__.py,sha256=QUXob-3eD2d0evw6eTUpy6yaaNrhiGO1-Ynx_y-D4l8,154
|
|
10
|
+
ai_dev_tools/context/builder.py,sha256=6Vb0EhEtifhWAetE2UN9yPkuRy7KOy_fCh0Lbbo6za8,18713
|
|
11
|
+
ai_dev_tools/context/incremental.py,sha256=VNhjdnQxvRKkJgSc-PT7zlBOGLHhYttS85gpft9aIpc,3520
|
|
12
|
+
ai_dev_tools/context/models.py,sha256=tfYnZWvAsUNQ9RsU1etzRz3gX5MAGZn0lxRDruxQj2Y,1452
|
|
13
|
+
ai_dev_tools/context/profiles.py,sha256=VteAM-YNqGHUpOQG3Aq2T60WuR_Ik2-2oOYSrrRACMA,1218
|
|
14
|
+
ai_dev_tools/context/selection.py,sha256=spu7CjZWJBR3VXQs3YBxPOxQit1H145IDz4BKmrQFJQ,9957
|
|
15
|
+
ai_dev_tools/context/symbols.py,sha256=NwEJ3_lBUPc7nZRSMLzfmbvG25gKm8P0IBy1KfeCJNE,5339
|
|
16
|
+
ai_dev_tools/detectors/__init__.py,sha256=FHCytKbvq-UnxBTHvMD3abeEVyAzG-OYRelp84GFfkU,41
|
|
17
|
+
ai_dev_tools/detectors/environment.py,sha256=98Wc7OUog-CKfyrxFr6yZlazeryhsL28tWM2_koLZos,4622
|
|
18
|
+
ai_dev_tools/detectors/project.py,sha256=iS3aykw0GoRItjLR1pdGBC-UuRe-blGviETf4fo0yC0,7213
|
|
19
|
+
ai_dev_tools/detectors/repository_map.py,sha256=4a7p1_6UcleFEDRWnc5cQiJuMqp3e1088sgEneKIwS8,4026
|
|
20
|
+
ai_dev_tools/detectors/runtime.py,sha256=QzVucMF_OSc8EGLUhpAh8HghC3UAtPT7NCn6U4gRRrY,6827
|
|
21
|
+
ai_dev_tools/detectors/workspaces.py,sha256=GnGMw7k9ST8sJRvc3PAs39cJtiMxTYKxFvOKA1DSjio,8404
|
|
22
|
+
ai_dev_tools/git/__init__.py,sha256=72rVU6ALOpCNljDnkRrbyCs4zfWwoSAaF4sRnQKWXPk,30
|
|
23
|
+
ai_dev_tools/git/inspect.py,sha256=bUsUMosmSM6p-CmbVkazDe0eX_oyr6qhT0yTcEP4F2E,7777
|
|
24
|
+
ai_dev_tools/models/__init__.py,sha256=zCWnVsKHBP8Myr5K4p1O3mh5u0h0Ee2cqNvGnHyFXJg,26
|
|
25
|
+
ai_dev_tools/models/report.py,sha256=evyUrZIN98Rv6rLEbS61t_nn5S7BjIF6fIVBRT4vMRg,2848
|
|
26
|
+
ai_dev_tools/models/workspace.py,sha256=KBHhn4FDH08OPm8fzqJbbW431B3gq21fmUrhH1FQ1Fk,1461
|
|
27
|
+
ai_dev_tools/parsers/__init__.py,sha256=CTeRzVPM4WA4KgU9q9fFxeKYruQsbCTkou9YOsQamxM,22
|
|
28
|
+
ai_dev_tools/parsers/logs.py,sha256=W4ieWdz9ixrGRjIVgnlEGzI5EuQxJMqS2qTvzqYwK1o,14121
|
|
29
|
+
ai_dev_tools/parsers/registry.py,sha256=Eye1NLCy2W6TF2OZ7tIOaFPj_tvPm7MyJRy_pyltuAg,2050
|
|
30
|
+
ai_dev_tools/reporters/__init__.py,sha256=JWR2WoPfAOilOoKesREl7dqZofYHmfn1Oask7pLUhbQ,22
|
|
31
|
+
ai_dev_tools/reporters/progressive.py,sha256=Z40kwEH5v4puyjT6AnU1a_fg0eChWnfBPodI08GtQe4,5599
|
|
32
|
+
ai_dev_tools/reporters/writer.py,sha256=yf6v-NSR9mxI3zSnlxUFl6N7cUK0vBnNWkg3gE5x-bM,2517
|
|
33
|
+
ai_dev_tools/runners/__init__.py,sha256=EMkZFuAQJdiVCAS8qsJmIj25XgzHvBoACqoKdKa9LG8,23
|
|
34
|
+
ai_dev_tools/runners/baseline.py,sha256=gNVcP1nWYTAh_6kCIJ2Fk8s9Ca_Q8oc1xhiMGpm6zyo,7285
|
|
35
|
+
ai_dev_tools/runners/bootstrap.py,sha256=xo7Xl9OFw-bnWdKYTA1i2UyBWwc5FoYuVuHZOvsFjuk,7762
|
|
36
|
+
ai_dev_tools/runners/bootstrap_models.py,sha256=ABdgCF9qUsp8lM2gWO1UUSNXgbd28stV4qQICZ0MW8U,1960
|
|
37
|
+
ai_dev_tools/runners/bootstrap_strategies.py,sha256=8tpNfiT_KRtZFxKbIQYJNU0fvRB3rJTHbSNLzE_9ET8,14115
|
|
38
|
+
ai_dev_tools/runners/cache.py,sha256=U_MBMBfJ6cSRPEl4GZZakXknONAWdd9rSgqNlnrnUhI,669
|
|
39
|
+
ai_dev_tools/runners/check.py,sha256=1sZlsPxY5E4iKpWwGJIj2I_2y8XT6M5GgAYQexYR5kQ,19316
|
|
40
|
+
ai_dev_tools/runners/check_checkpoint.py,sha256=7h7XzRjE10C6NqKePGYNDZWqB64ftLExzPW_1JcuVg0,1545
|
|
41
|
+
ai_dev_tools/runners/check_models.py,sha256=BrHTxq2yVSzpTxRuZ6jB7zOmbXFvVuVU7q0hwrPvy2g,1312
|
|
42
|
+
ai_dev_tools/runners/check_scheduler.py,sha256=tjMzKyqEFptgNK-CJjb7jtNXTvEjhjGBt3rrAE8I6sY,3165
|
|
43
|
+
ai_dev_tools/runners/check_selection.py,sha256=LlWmj4kK5bQUuZBl1LeUs8HGicSD7tInA2s3YCbOgJs,9650
|
|
44
|
+
ai_dev_tools/runners/diagnostics.py,sha256=7M1Zula_nUoBv3a8n9ioBsnHgM-qkoYVXydU8P4VjUY,3623
|
|
45
|
+
ai_dev_tools/runners/feedback.py,sha256=5Xo4OFaFMGf2UZ9SdHBOcCOdrztCJ1VPsEQZRSqRFQc,6642
|
|
46
|
+
ai_dev_tools/runners/finish.py,sha256=Za3SkW5o54NRQr_bo1WSKhrVfImv7TWVzszsDlFIceA,4102
|
|
47
|
+
ai_dev_tools/runners/focused.py,sha256=RierjyU6WChD4sSq53GsvFLcpQGrpsEZ9mOsaTzrSms,1347
|
|
48
|
+
ai_dev_tools/runners/index.py,sha256=GU581MlHrUtcYZu7P4Hy9Re24UfhJinQDYD0bFGF0dM,1467
|
|
49
|
+
ai_dev_tools/runtime/__init__.py,sha256=Pb-djv_Kcze8cKc5tiYh_3YeQzCENrBhKG00uYpCpWs,151
|
|
50
|
+
ai_dev_tools/runtime/runner.py,sha256=PbBIXfOQFR5hK1TgrD6dlQMiS9ZDcJDCyOQswqKYvN4,13689
|
|
51
|
+
ai_dev_tools/runtime/supervisor.py,sha256=ym7iQ92jF8hYUt4dSUo5sjeMwLAUDoxb6X0Xz70Aqj0,4713
|
|
52
|
+
ai_dev_tools/security/__init__.py,sha256=e6X0XtAGkfFSBGRxHZOHm7-ZMg8Pnypq3Z3IONu7Fho,25
|
|
53
|
+
ai_dev_tools/security/secrets.py,sha256=N97uz0qAGybvhzXSTf_D3hT5i_VmmesMavXXobME7sk,2151
|
|
54
|
+
ai_dev_tools/utils/__init__.py,sha256=vTzpzAhwZwhp9xdzSN95SWmf02qgw7Asc0ULXG4aEok,23
|
|
55
|
+
ai_dev_tools/utils/subprocess.py,sha256=Jjt2ezr8RM3fmbdrTCw13Qi8KYxgix-d7rXQkzwDHZ8,2266
|
|
56
|
+
ai_dev_cli_tools-0.5.0a1.dist-info/METADATA,sha256=d7f71_XPlQxJykInHceUIGD4eVgBX5phkVUAxTlToqM,10363
|
|
57
|
+
ai_dev_cli_tools-0.5.0a1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
58
|
+
ai_dev_cli_tools-0.5.0a1.dist-info/entry_points.txt,sha256=xKNwzY-s7fmCWs65waRZ5gzMpYlXl03S_P1btlxj3E4,49
|
|
59
|
+
ai_dev_cli_tools-0.5.0a1.dist-info/licenses/LICENSE,sha256=Nc0_ePANo7Sy4FWWgVlFx6mNbKnByDAcQA7jGAYJX0w,1076
|
|
60
|
+
ai_dev_cli_tools-0.5.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mateusz Lewandowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ai_dev_tools/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import posixpath
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_impact_graph(
|
|
9
|
+
root: Path,
|
|
10
|
+
paths: set[str],
|
|
11
|
+
*,
|
|
12
|
+
reused_paths: set[str] | None = None,
|
|
13
|
+
previous_edges: object = None,
|
|
14
|
+
) -> list[dict[str, str]]:
|
|
15
|
+
reused_paths = reused_paths or set()
|
|
16
|
+
old_by_source = _edges_by_source(previous_edges)
|
|
17
|
+
edges: set[tuple[str, str, str]] = set()
|
|
18
|
+
for relative in sorted(paths):
|
|
19
|
+
if relative in reused_paths and relative in old_by_source:
|
|
20
|
+
edges.update(old_by_source[relative])
|
|
21
|
+
continue
|
|
22
|
+
source = root / relative
|
|
23
|
+
for target, kind in _references(source, relative, paths):
|
|
24
|
+
edges.add((relative, target, kind))
|
|
25
|
+
for target in _candidate_tests(relative, paths):
|
|
26
|
+
edges.add((relative, target, "test"))
|
|
27
|
+
return [{"from": source, "to": target, "kind": kind} for source, target, kind in sorted(edges)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def related_tests(graph: object, changed_files: list[str]) -> list[str]:
|
|
31
|
+
changed = {path.replace("\\", "/") for path in changed_files}
|
|
32
|
+
selected: set[str] = set()
|
|
33
|
+
if not isinstance(graph, list):
|
|
34
|
+
return []
|
|
35
|
+
for edge in graph:
|
|
36
|
+
if (
|
|
37
|
+
isinstance(edge, dict)
|
|
38
|
+
and edge.get("kind") == "test"
|
|
39
|
+
and edge.get("from") in changed
|
|
40
|
+
and isinstance(edge.get("to"), str)
|
|
41
|
+
):
|
|
42
|
+
selected.add(str(edge["to"]))
|
|
43
|
+
return sorted(selected)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _references(source: Path, relative: str, paths: set[str]) -> list[tuple[str, str]]:
|
|
47
|
+
if source.suffix.lower() not in {".py", ".js", ".jsx", ".ts", ".tsx", ".rs"}:
|
|
48
|
+
return []
|
|
49
|
+
try:
|
|
50
|
+
text = source.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
except OSError:
|
|
52
|
+
return []
|
|
53
|
+
if source.suffix.lower() == ".py":
|
|
54
|
+
return _python_references(relative, text, paths)
|
|
55
|
+
if source.suffix.lower() in {".js", ".jsx", ".ts", ".tsx"}:
|
|
56
|
+
return _js_references(relative, text, paths)
|
|
57
|
+
return _rust_references(relative, text, paths)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _python_references(relative: str, text: str, paths: set[str]) -> list[tuple[str, str]]:
|
|
61
|
+
result: list[tuple[str, str]] = []
|
|
62
|
+
for match in re.finditer(
|
|
63
|
+
r"^\s*(?:from\s+([A-Za-z_][\w.]*)\s+import|import\s+([A-Za-z_][\w.]*))",
|
|
64
|
+
text,
|
|
65
|
+
re.MULTILINE,
|
|
66
|
+
):
|
|
67
|
+
module = match.group(1) or match.group(2)
|
|
68
|
+
module_path = module.replace(".", "/")
|
|
69
|
+
candidates = [
|
|
70
|
+
f"{module_path}.py",
|
|
71
|
+
f"src/{module_path}.py",
|
|
72
|
+
f"{module_path}/__init__.py",
|
|
73
|
+
f"src/{module_path}/__init__.py",
|
|
74
|
+
]
|
|
75
|
+
result.extend((candidate, "import") for candidate in candidates if candidate in paths)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _js_references(relative: str, text: str, paths: set[str]) -> list[tuple[str, str]]:
|
|
80
|
+
result: list[tuple[str, str]] = []
|
|
81
|
+
directory = posixpath.dirname(relative)
|
|
82
|
+
for match in re.finditer(r"(?:from\s+|require\()(['\"])(\.{1,2}/[^'\"]+)\1", text):
|
|
83
|
+
base = posixpath.normpath(posixpath.join(directory, match.group(2)))
|
|
84
|
+
candidates = [
|
|
85
|
+
base,
|
|
86
|
+
*[f"{base}{suffix}" for suffix in (".ts", ".tsx", ".js", ".jsx")],
|
|
87
|
+
*[f"{base}/index{suffix}" for suffix in (".ts", ".tsx", ".js", ".jsx")],
|
|
88
|
+
]
|
|
89
|
+
result.extend((candidate, "import") for candidate in candidates if candidate in paths)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _rust_references(relative: str, text: str, paths: set[str]) -> list[tuple[str, str]]:
|
|
94
|
+
directory = posixpath.dirname(relative)
|
|
95
|
+
result: list[tuple[str, str]] = []
|
|
96
|
+
for match in re.finditer(r"^\s*mod\s+([A-Za-z_][\w]*)\s*;", text, re.MULTILINE):
|
|
97
|
+
name = match.group(1)
|
|
98
|
+
for candidate in (f"{directory}/{name}.rs", f"{directory}/{name}/mod.rs"):
|
|
99
|
+
normalized = candidate.lstrip("/")
|
|
100
|
+
if normalized in paths:
|
|
101
|
+
result.append((normalized, "import"))
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _candidate_tests(relative: str, paths: set[str]) -> list[str]:
|
|
106
|
+
path = Path(relative)
|
|
107
|
+
stem = path.stem
|
|
108
|
+
candidates: list[str] = []
|
|
109
|
+
if path.suffix == ".py":
|
|
110
|
+
parts = list(path.parts)
|
|
111
|
+
source_parts = parts[1:] if parts and parts[0] in {"src", "lib"} else parts
|
|
112
|
+
if source_parts:
|
|
113
|
+
source_parts[-1] = f"test_{stem}.py"
|
|
114
|
+
candidates.append(Path("tests", *source_parts).as_posix())
|
|
115
|
+
candidates.append(Path("tests", f"test_{stem}.py").as_posix())
|
|
116
|
+
elif path.suffix.lower() in {".js", ".jsx", ".ts", ".tsx"}:
|
|
117
|
+
candidates.extend(
|
|
118
|
+
[
|
|
119
|
+
path.with_name(f"{stem}.test{path.suffix}").as_posix(),
|
|
120
|
+
path.with_name(f"{stem}.spec{path.suffix}").as_posix(),
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
return sorted({candidate for candidate in candidates if candidate in paths})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _edges_by_source(value: object) -> dict[str, set[tuple[str, str, str]]]:
|
|
127
|
+
result: dict[str, set[tuple[str, str, str]]] = {}
|
|
128
|
+
if not isinstance(value, list):
|
|
129
|
+
return result
|
|
130
|
+
for edge in value:
|
|
131
|
+
if not isinstance(edge, dict):
|
|
132
|
+
continue
|
|
133
|
+
source, target, kind = edge.get("from"), edge.get("to"), edge.get("kind")
|
|
134
|
+
if all(isinstance(item, str) for item in (source, target, kind)):
|
|
135
|
+
result.setdefault(str(source), set()).add((str(source), str(target), str(kind)))
|
|
136
|
+
return result
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
from ai_dev_tools.cache.graph import build_impact_graph
|
|
11
|
+
from ai_dev_tools.config import DEFAULT_IGNORES
|
|
12
|
+
|
|
13
|
+
INDEX_SCHEMA_VERSION = "1"
|
|
14
|
+
INDEX_RELATIVE_PATH = Path(".ai/cache/repository-index.json")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IndexEntry(TypedDict):
|
|
18
|
+
path: str
|
|
19
|
+
size: int
|
|
20
|
+
mtime_ns: int
|
|
21
|
+
sha256: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def update_repository_index(root: Path, *, rebuild: bool = False) -> dict[str, object]:
|
|
25
|
+
resolved_root = root.resolve()
|
|
26
|
+
index_path = resolved_root / INDEX_RELATIVE_PATH
|
|
27
|
+
previous = {} if rebuild else read_repository_index(resolved_root)
|
|
28
|
+
previous_entries = _entry_map(previous.get("entries"))
|
|
29
|
+
entries: list[IndexEntry] = []
|
|
30
|
+
reused = 0
|
|
31
|
+
hashed = 0
|
|
32
|
+
reused_paths: set[str] = set()
|
|
33
|
+
|
|
34
|
+
for path in _project_files(resolved_root):
|
|
35
|
+
relative = path.relative_to(resolved_root).as_posix()
|
|
36
|
+
stat = path.stat()
|
|
37
|
+
old = previous_entries.get(relative)
|
|
38
|
+
if old is not None and old["size"] == stat.st_size and old["mtime_ns"] == stat.st_mtime_ns:
|
|
39
|
+
digest = old["sha256"]
|
|
40
|
+
reused += 1
|
|
41
|
+
reused_paths.add(relative)
|
|
42
|
+
else:
|
|
43
|
+
digest = _sha256(path)
|
|
44
|
+
hashed += 1
|
|
45
|
+
entries.append(
|
|
46
|
+
{
|
|
47
|
+
"path": relative,
|
|
48
|
+
"size": stat.st_size,
|
|
49
|
+
"mtime_ns": stat.st_mtime_ns,
|
|
50
|
+
"sha256": digest,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
graph = build_impact_graph(
|
|
55
|
+
resolved_root,
|
|
56
|
+
{item["path"] for item in entries},
|
|
57
|
+
reused_paths=reused_paths,
|
|
58
|
+
previous_edges=previous.get("graph"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
payload: dict[str, object] = {
|
|
62
|
+
"schema_version": INDEX_SCHEMA_VERSION,
|
|
63
|
+
"project_root": str(resolved_root),
|
|
64
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
65
|
+
"entries": entries,
|
|
66
|
+
"graph": graph,
|
|
67
|
+
"summary": {
|
|
68
|
+
"files": len(entries),
|
|
69
|
+
"hashed": hashed,
|
|
70
|
+
"reused": reused,
|
|
71
|
+
"removed": len(set(previous_entries) - {item["path"] for item in entries}),
|
|
72
|
+
"graph_edges": len(graph),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
_write_json(index_path, payload)
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_repository_index(root: Path) -> dict[str, object]:
|
|
80
|
+
path = root.resolve() / INDEX_RELATIVE_PATH
|
|
81
|
+
try:
|
|
82
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
83
|
+
except (OSError, json.JSONDecodeError):
|
|
84
|
+
return {}
|
|
85
|
+
if not isinstance(value, dict) or value.get("schema_version") != INDEX_SCHEMA_VERSION:
|
|
86
|
+
return {}
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def repository_fingerprint(
|
|
91
|
+
entries: object, workspace: str = "", extra: tuple[str, ...] = ()
|
|
92
|
+
) -> str:
|
|
93
|
+
prefix = workspace.replace(chr(92), "/").strip("/")
|
|
94
|
+
selected = []
|
|
95
|
+
for entry in _entry_list(entries):
|
|
96
|
+
path = entry["path"]
|
|
97
|
+
if not prefix or path == prefix or path.startswith(prefix + "/"):
|
|
98
|
+
selected.append((path, entry["sha256"]))
|
|
99
|
+
payload = json.dumps(
|
|
100
|
+
{"workspace": prefix, "files": selected, "extra": extra},
|
|
101
|
+
sort_keys=True,
|
|
102
|
+
separators=(",", ":"),
|
|
103
|
+
)
|
|
104
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _project_files(root: Path) -> list[Path]:
|
|
108
|
+
ignored = {item.replace(chr(92), "/").strip("/") for item in DEFAULT_IGNORES}
|
|
109
|
+
ignored_names = {Path(item).name for item in ignored}
|
|
110
|
+
files: list[Path] = []
|
|
111
|
+
for current, directories, names in os.walk(root, followlinks=False):
|
|
112
|
+
current_path = Path(current)
|
|
113
|
+
directories[:] = sorted(
|
|
114
|
+
name
|
|
115
|
+
for name in directories
|
|
116
|
+
if name not in ignored_names
|
|
117
|
+
and not (current_path.name in {"test", "tests"} and name == "fixtures")
|
|
118
|
+
)
|
|
119
|
+
for name in sorted(names):
|
|
120
|
+
path = current_path / name
|
|
121
|
+
if path.is_symlink() or not path.is_file():
|
|
122
|
+
continue
|
|
123
|
+
relative = path.relative_to(root).as_posix()
|
|
124
|
+
if any(relative == item or relative.startswith(item + "/") for item in ignored):
|
|
125
|
+
continue
|
|
126
|
+
files.append(path)
|
|
127
|
+
return files
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _entry_map(value: object) -> dict[str, IndexEntry]:
|
|
131
|
+
return {entry["path"]: entry for entry in _entry_list(value)}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _entry_list(value: object) -> list[IndexEntry]:
|
|
135
|
+
if not isinstance(value, list):
|
|
136
|
+
return []
|
|
137
|
+
result: list[IndexEntry] = []
|
|
138
|
+
for item in value:
|
|
139
|
+
if (
|
|
140
|
+
isinstance(item, dict)
|
|
141
|
+
and isinstance(item.get("path"), str)
|
|
142
|
+
and isinstance(item.get("size"), int)
|
|
143
|
+
and isinstance(item.get("mtime_ns"), int)
|
|
144
|
+
and isinstance(item.get("sha256"), str)
|
|
145
|
+
):
|
|
146
|
+
result.append(
|
|
147
|
+
{
|
|
148
|
+
"path": item["path"],
|
|
149
|
+
"size": item["size"],
|
|
150
|
+
"mtime_ns": item["mtime_ns"],
|
|
151
|
+
"sha256": item["sha256"],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _sha256(path: Path) -> str:
|
|
158
|
+
digest = hashlib.sha256()
|
|
159
|
+
with path.open("rb") as handle:
|
|
160
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
161
|
+
digest.update(chunk)
|
|
162
|
+
return digest.hexdigest()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _write_json(path: Path, value: dict[str, object]) -> None:
|
|
166
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
168
|
+
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
169
|
+
os.replace(temporary, path)
|