paranoid-atlas-cli 0.1.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.
- paranoid_atlas_cli-0.1.1/.github/workflows/publish-pypi.yml +43 -0
- paranoid_atlas_cli-0.1.1/.gitignore +5 -0
- paranoid_atlas_cli-0.1.1/PKG-INFO +45 -0
- paranoid_atlas_cli-0.1.1/README.md +24 -0
- paranoid_atlas_cli-0.1.1/pyproject.toml +40 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/__init__.py +3 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/cli.py +29 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/commands/__init__.py +0 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/commands/init.py +97 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/BACKLOG.md +7 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/CLAUDE.md +36 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/STATUS.md +15 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/archived.md +3 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/atlas.toml +3 -0
- paranoid_atlas_cli-0.1.1/src/atlas_cli/templates/gitignore +5 -0
- paranoid_atlas_cli-0.1.1/tests/test_askingtheversionshould.py +11 -0
- paranoid_atlas_cli-0.1.1/tests/test_initializinganatlasshould.py +182 -0
- paranoid_atlas_cli-0.1.1/tests/test_runningwithoutacommandshould.py +6 -0
- paranoid_atlas_cli-0.1.1/uv.lock +79 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish paranoid-atlas-cli to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.11"
|
|
23
|
+
|
|
24
|
+
- name: Check the tag matches the package version
|
|
25
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
26
|
+
run: |
|
|
27
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
28
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
29
|
+
echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $version"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade pip build twine
|
|
36
|
+
python -m build
|
|
37
|
+
twine check dist/*
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
41
|
+
with:
|
|
42
|
+
username: __token__
|
|
43
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: paranoid-atlas-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The Atlas method made executable: create and keep Atlases in order
|
|
5
|
+
Project-URL: Homepage, https://atlas.paranoid.software
|
|
6
|
+
Project-URL: Repository, https://github.com/paranoid-software/atlas-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/paranoid-software/atlas-cli/issues
|
|
8
|
+
Author: Paranoid Software
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,atlas,claude,cli,spec-driven
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# atlas-cli
|
|
23
|
+
|
|
24
|
+
The [Atlas](https://atlas.paranoid.software) method made executable: create an Atlas and keep
|
|
25
|
+
it in order while several people and agents work in it.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install paranoid-atlas-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Develop
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync
|
|
37
|
+
uv run atlas --version
|
|
38
|
+
uv run pytest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Release
|
|
42
|
+
|
|
43
|
+
Bump `version` in `pyproject.toml`, commit, then push a matching tag (`v0.1.1` for `0.1.1`):
|
|
44
|
+
the `Publish paranoid-atlas-cli to PyPI` workflow builds and publishes it. A tag that doesn't match the
|
|
45
|
+
version fails the workflow.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# atlas-cli
|
|
2
|
+
|
|
3
|
+
The [Atlas](https://atlas.paranoid.software) method made executable: create an Atlas and keep
|
|
4
|
+
it in order while several people and agents work in it.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv tool install paranoid-atlas-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Develop
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync
|
|
16
|
+
uv run atlas --version
|
|
17
|
+
uv run pytest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Release
|
|
21
|
+
|
|
22
|
+
Bump `version` in `pyproject.toml`, commit, then push a matching tag (`v0.1.1` for `0.1.1`):
|
|
23
|
+
the `Publish paranoid-atlas-cli to PyPI` workflow builds and publishes it. A tag that doesn't match the
|
|
24
|
+
version fails the workflow.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "paranoid-atlas-cli"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "The Atlas method made executable: create and keep Atlases in order"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Paranoid Software" },
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
keywords = ["atlas", "cli", "agents", "spec-driven", "claude"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Build Tools",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://atlas.paranoid.software"
|
|
30
|
+
Repository = "https://github.com/paranoid-software/atlas-cli"
|
|
31
|
+
Issues = "https://github.com/paranoid-software/atlas-cli/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
atlas = "atlas_cli.cli:main"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = ["pytest>=8"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/atlas_cli"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from atlas_cli import __version__
|
|
5
|
+
from atlas_cli.commands import init
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="atlas",
|
|
11
|
+
description="Create and keep Atlases in order — https://atlas.paranoid.software",
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument("--version", action="version", version=f"atlas {__version__}")
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
|
|
15
|
+
init.register(subparsers)
|
|
16
|
+
return parser
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = _build_parser()
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
if args.command is None:
|
|
23
|
+
parser.print_help()
|
|
24
|
+
return 0
|
|
25
|
+
return args.handler(args)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from datetime import date
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from string import Template
|
|
7
|
+
|
|
8
|
+
ATLAS_FILES = {
|
|
9
|
+
"CLAUDE.md": "CLAUDE.md",
|
|
10
|
+
"STATUS.md": "STATUS.md",
|
|
11
|
+
"BACKLOG.md": "BACKLOG.md",
|
|
12
|
+
"_archived/README.md": "archived.md",
|
|
13
|
+
"_settings/atlas.toml": "atlas.toml",
|
|
14
|
+
}
|
|
15
|
+
CLAUDE_SETTINGS = ".claude/settings.local.json"
|
|
16
|
+
VSCODE_SETTINGS = ".vscode/settings.json"
|
|
17
|
+
GITIGNORE = ".gitignore"
|
|
18
|
+
ATLAS_MARKERS = ("_settings/atlas.toml", "CLAUDE.md", "STATUS.md")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register(subparsers) -> None:
|
|
22
|
+
parser = subparsers.add_parser("init", help="create an Atlas in the current folder, named after it")
|
|
23
|
+
parser.set_defaults(handler=run)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(args: argparse.Namespace) -> int:
|
|
27
|
+
folder = Path.cwd()
|
|
28
|
+
found = [marker for marker in ATLAS_MARKERS if (folder / marker).exists()]
|
|
29
|
+
if found:
|
|
30
|
+
print(f"This folder is already an Atlas (found {', '.join(found)}); nothing was changed.")
|
|
31
|
+
if "_settings/atlas.toml" not in found:
|
|
32
|
+
print("It predates _settings/ — bring it up to date with `atlas migrate`.")
|
|
33
|
+
return 1
|
|
34
|
+
try:
|
|
35
|
+
plan = _plan_base_set(folder, date.today())
|
|
36
|
+
except ValueError as error:
|
|
37
|
+
print(f"{error}; nothing was changed.")
|
|
38
|
+
return 1
|
|
39
|
+
for path, (action, content) in plan.items():
|
|
40
|
+
if content is not None:
|
|
41
|
+
_write(folder / path, content)
|
|
42
|
+
print(f"{action} {path}")
|
|
43
|
+
if plan[GITIGNORE][0] == "kept":
|
|
44
|
+
print(f"Check that the existing {GITIGNORE} keeps only the Atlas's own files (see 07-sharing-an-atlas).")
|
|
45
|
+
print("Next: add each source with `atlas link <name> <path>`.")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _plan_base_set(folder: Path, today: date) -> dict[str, tuple[str, str | None]]:
|
|
50
|
+
plan = {
|
|
51
|
+
path: ("created", _render_template(template, name=folder.name, today=today.isoformat()))
|
|
52
|
+
for path, template in ATLAS_FILES.items()
|
|
53
|
+
}
|
|
54
|
+
plan[CLAUDE_SETTINGS] = _plan_json(folder / CLAUDE_SETTINGS, _complete_claude_settings)
|
|
55
|
+
plan[VSCODE_SETTINGS] = _plan_json(folder / VSCODE_SETTINGS, _complete_vscode_settings)
|
|
56
|
+
if (folder / GITIGNORE).exists():
|
|
57
|
+
plan[GITIGNORE] = ("kept", None)
|
|
58
|
+
else:
|
|
59
|
+
plan[GITIGNORE] = ("created", _render_template("gitignore"))
|
|
60
|
+
return plan
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _plan_json(target: Path, complete) -> tuple[str, str]:
|
|
64
|
+
if not target.exists():
|
|
65
|
+
return "created", _dump_json(complete({}))
|
|
66
|
+
try:
|
|
67
|
+
settings = json.loads(target.read_text(encoding="utf-8"))
|
|
68
|
+
except json.JSONDecodeError as error:
|
|
69
|
+
raise ValueError(f"Cannot read {target.name} in {target.parent.name}/ ({error.msg})") from error
|
|
70
|
+
return "updated", _dump_json(complete(settings))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _complete_claude_settings(settings: dict) -> dict:
|
|
74
|
+
permissions = settings.setdefault("permissions", {})
|
|
75
|
+
permissions.setdefault("allow", [])
|
|
76
|
+
permissions.setdefault("additionalDirectories", [])
|
|
77
|
+
return settings
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _complete_vscode_settings(settings: dict) -> dict:
|
|
81
|
+
settings["git.autoRepositoryDetection"] = True
|
|
82
|
+
settings.setdefault("git.scanRepositories", [])
|
|
83
|
+
return settings
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _dump_json(settings: dict) -> str:
|
|
87
|
+
return json.dumps(settings, indent=2, ensure_ascii=False) + "\n"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _render_template(template: str, **values: str) -> str:
|
|
91
|
+
text = files("atlas_cli.templates").joinpath(template).read_text(encoding="utf-8")
|
|
92
|
+
return Template(text).safe_substitute(values)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _write(target: Path, content: str) -> None:
|
|
96
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
target.write_text(content, encoding="utf-8", newline="\n")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# $name Atlas — agent instructions
|
|
2
|
+
|
|
3
|
+
This is the **$name Atlas** — the context of the $name project. Project orientation +
|
|
4
|
+
Atlas-specific glue only. Universal rules and behavior live in the memory store; autonomous
|
|
5
|
+
file-memory is off. **Current state & next steps live in [STATUS.md](STATUS.md) — read it
|
|
6
|
+
first.**
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. What lives here — sources and their purposes
|
|
11
|
+
|
|
12
|
+
Each source is a symlink; where it lives is up to each machine and is never recorded here.
|
|
13
|
+
|
|
14
|
+
_(no sources yet — add each one with `atlas link <name> <path>`, then its block below)_
|
|
15
|
+
|
|
16
|
+
Editing a file under a symlink writes to the source itself. Changes across sources are
|
|
17
|
+
flagged explicitly.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. How I work in this Atlas
|
|
22
|
+
- **Before editing, identify the target source** — most changes belong to exactly one;
|
|
23
|
+
changes across sources are called out explicitly.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 3. $name — product / architecture
|
|
28
|
+
_(no domain conventions yet)_
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 5. Conventions we're establishing (Atlas-level, not in the memory store)
|
|
33
|
+
_Rules that apply to this Atlas and that we've decided not (yet) to lift into the memory
|
|
34
|
+
store. Keep it short; promote to the memory store only after a dedicated session, and only
|
|
35
|
+
if they generalize._
|
|
36
|
+
- _(empty — fill in as conventions get decided.)_
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# $name — STATUS (living)
|
|
2
|
+
|
|
3
|
+
Where this project is now. Standing decisions + orientation live in CLAUDE.md; the
|
|
4
|
+
BACKLOG / SPEC_ / _archived artifacts are the record. Refresh with `/atlas-sync`.
|
|
5
|
+
|
|
6
|
+
_Last updated: ${today}_
|
|
7
|
+
|
|
8
|
+
## What $name is (one line)
|
|
9
|
+
_(one-line summary)_
|
|
10
|
+
|
|
11
|
+
## Active
|
|
12
|
+
_(one block per SPEC in progress / in review — rewritten to the current state, never appended)_
|
|
13
|
+
|
|
14
|
+
## Recently closed
|
|
15
|
+
_(short rolling window — last N — pointing into _archived/; the full history lives there)_
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from atlas_cli import __version__
|
|
4
|
+
from atlas_cli.cli import main
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_print_the_installed_version(capsys):
|
|
8
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
9
|
+
main(["--version"])
|
|
10
|
+
assert exit_info.value.code == 0
|
|
11
|
+
assert capsys.readouterr().out.strip() == f"atlas {__version__}"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tomllib
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from atlas_cli.cli import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _files_under(folder: Path) -> set[str]:
|
|
10
|
+
return {str(path.relative_to(folder)) for path in folder.rglob("*") if path.is_file()}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_create_the_base_set_and_nothing_else(tmp_path, monkeypatch):
|
|
14
|
+
monkeypatch.chdir(tmp_path)
|
|
15
|
+
|
|
16
|
+
assert main(["init"]) == 0
|
|
17
|
+
|
|
18
|
+
assert _files_under(tmp_path) == {
|
|
19
|
+
"CLAUDE.md",
|
|
20
|
+
"STATUS.md",
|
|
21
|
+
"BACKLOG.md",
|
|
22
|
+
"_archived/README.md",
|
|
23
|
+
"_settings/atlas.toml",
|
|
24
|
+
".claude/settings.local.json",
|
|
25
|
+
".vscode/settings.json",
|
|
26
|
+
".gitignore",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_start_with_no_sources_and_no_plugins(tmp_path, monkeypatch):
|
|
31
|
+
monkeypatch.chdir(tmp_path)
|
|
32
|
+
|
|
33
|
+
main(["init"])
|
|
34
|
+
|
|
35
|
+
settings = tomllib.loads((tmp_path / "_settings/atlas.toml").read_text(encoding="utf-8"))
|
|
36
|
+
assert settings == {"version": 1, "plugins": {}}
|
|
37
|
+
claude = json.loads((tmp_path / ".claude/settings.local.json").read_text(encoding="utf-8"))
|
|
38
|
+
assert claude == {"permissions": {"allow": [], "additionalDirectories": []}}
|
|
39
|
+
vscode = json.loads((tmp_path / ".vscode/settings.json").read_text(encoding="utf-8"))
|
|
40
|
+
assert vscode == {"git.autoRepositoryDetection": True, "git.scanRepositories": []}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_ignore_everything_but_the_atlas_files(tmp_path, monkeypatch):
|
|
44
|
+
monkeypatch.chdir(tmp_path)
|
|
45
|
+
|
|
46
|
+
main(["init"])
|
|
47
|
+
|
|
48
|
+
assert (tmp_path / ".gitignore").read_text(encoding="utf-8").splitlines() == [
|
|
49
|
+
"/*",
|
|
50
|
+
"!/*.md",
|
|
51
|
+
"!/_*/",
|
|
52
|
+
"!/.vscode/",
|
|
53
|
+
"!/.gitignore",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_name_the_atlas_after_its_folder(tmp_path, monkeypatch):
|
|
58
|
+
folder = tmp_path / "log-daemon"
|
|
59
|
+
folder.mkdir()
|
|
60
|
+
monkeypatch.chdir(folder)
|
|
61
|
+
|
|
62
|
+
main(["init"])
|
|
63
|
+
|
|
64
|
+
assert (folder / "CLAUDE.md").read_text(encoding="utf-8").startswith("# log-daemon Atlas")
|
|
65
|
+
assert (folder / "STATUS.md").read_text(encoding="utf-8").startswith("# log-daemon — STATUS")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_date_the_status_with_today(tmp_path, monkeypatch):
|
|
69
|
+
monkeypatch.chdir(tmp_path)
|
|
70
|
+
|
|
71
|
+
main(["init"])
|
|
72
|
+
|
|
73
|
+
assert f"_Last updated: {date.today().isoformat()}_" in (tmp_path / "STATUS.md").read_text(encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_write_the_same_files_on_every_run(tmp_path, monkeypatch):
|
|
77
|
+
first, second = tmp_path / "acme", tmp_path / "other" / "acme"
|
|
78
|
+
for folder in (first, second):
|
|
79
|
+
folder.mkdir(parents=True)
|
|
80
|
+
monkeypatch.chdir(folder)
|
|
81
|
+
main(["init"])
|
|
82
|
+
|
|
83
|
+
for path in _files_under(first):
|
|
84
|
+
assert (first / path).read_bytes() == (second / path).read_bytes(), path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_write_utf8_with_unix_line_endings(tmp_path, monkeypatch):
|
|
88
|
+
folder = tmp_path / "señal"
|
|
89
|
+
folder.mkdir()
|
|
90
|
+
monkeypatch.chdir(folder)
|
|
91
|
+
|
|
92
|
+
main(["init"])
|
|
93
|
+
|
|
94
|
+
for path in _files_under(folder):
|
|
95
|
+
assert b"\r\n" not in (folder / path).read_bytes(), path
|
|
96
|
+
assert (folder / "CLAUDE.md").read_bytes().startswith("# señal Atlas".encode("utf-8"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_keep_what_an_existing_claude_settings_already_allows(tmp_path, monkeypatch):
|
|
100
|
+
(tmp_path / ".claude").mkdir()
|
|
101
|
+
(tmp_path / ".claude/settings.local.json").write_text(
|
|
102
|
+
'{"permissions": {"allow": ["Bash(ls:*)"]}}', encoding="utf-8"
|
|
103
|
+
)
|
|
104
|
+
monkeypatch.chdir(tmp_path)
|
|
105
|
+
|
|
106
|
+
assert main(["init"]) == 0
|
|
107
|
+
|
|
108
|
+
claude = json.loads((tmp_path / ".claude/settings.local.json").read_text(encoding="utf-8"))
|
|
109
|
+
assert claude == {"permissions": {"allow": ["Bash(ls:*)"], "additionalDirectories": []}}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_turn_on_repository_detection_in_existing_vscode_settings(tmp_path, monkeypatch):
|
|
113
|
+
(tmp_path / ".vscode").mkdir()
|
|
114
|
+
(tmp_path / ".vscode/settings.json").write_text(
|
|
115
|
+
'{"editor.tabSize": 4, "git.autoRepositoryDetection": "openEditors"}', encoding="utf-8"
|
|
116
|
+
)
|
|
117
|
+
monkeypatch.chdir(tmp_path)
|
|
118
|
+
|
|
119
|
+
assert main(["init"]) == 0
|
|
120
|
+
|
|
121
|
+
vscode = json.loads((tmp_path / ".vscode/settings.json").read_text(encoding="utf-8"))
|
|
122
|
+
assert vscode == {
|
|
123
|
+
"editor.tabSize": 4,
|
|
124
|
+
"git.autoRepositoryDetection": True,
|
|
125
|
+
"git.scanRepositories": [],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_keep_an_existing_gitignore_and_ask_to_check_it(tmp_path, monkeypatch, capsys):
|
|
130
|
+
(tmp_path / ".gitignore").write_text("node_modules/\n", encoding="utf-8")
|
|
131
|
+
monkeypatch.chdir(tmp_path)
|
|
132
|
+
|
|
133
|
+
assert main(["init"]) == 0
|
|
134
|
+
|
|
135
|
+
assert (tmp_path / ".gitignore").read_text(encoding="utf-8") == "node_modules/\n"
|
|
136
|
+
assert "Check that the existing .gitignore" in capsys.readouterr().out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_refuse_unreadable_claude_settings_and_change_nothing(tmp_path, monkeypatch, capsys):
|
|
140
|
+
(tmp_path / ".claude").mkdir()
|
|
141
|
+
(tmp_path / ".claude/settings.local.json").write_text("{ not json", encoding="utf-8")
|
|
142
|
+
monkeypatch.chdir(tmp_path)
|
|
143
|
+
|
|
144
|
+
assert main(["init"]) == 1
|
|
145
|
+
|
|
146
|
+
assert _files_under(tmp_path) == {".claude/settings.local.json"}
|
|
147
|
+
assert "nothing was changed" in capsys.readouterr().out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_refuse_a_folder_that_is_already_an_atlas(tmp_path, monkeypatch, capsys):
|
|
151
|
+
monkeypatch.chdir(tmp_path)
|
|
152
|
+
main(["init"])
|
|
153
|
+
(tmp_path / "STATUS.md").write_text("work in progress", encoding="utf-8")
|
|
154
|
+
capsys.readouterr()
|
|
155
|
+
|
|
156
|
+
assert main(["init"]) == 1
|
|
157
|
+
|
|
158
|
+
assert (tmp_path / "STATUS.md").read_text(encoding="utf-8") == "work in progress"
|
|
159
|
+
assert "already an Atlas" in capsys.readouterr().out
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_suggest_migrate_for_an_atlas_without_settings(tmp_path, monkeypatch, capsys):
|
|
163
|
+
monkeypatch.chdir(tmp_path)
|
|
164
|
+
(tmp_path / "CLAUDE.md").write_text("an Atlas from before _settings/", encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
assert main(["init"]) == 1
|
|
167
|
+
|
|
168
|
+
assert not (tmp_path / "_settings").exists()
|
|
169
|
+
assert "atlas migrate" in capsys.readouterr().out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_leave_existing_sources_untouched(tmp_path, monkeypatch):
|
|
173
|
+
source = tmp_path / "elsewhere" / "log-daemon"
|
|
174
|
+
source.mkdir(parents=True)
|
|
175
|
+
atlas = tmp_path / "acme"
|
|
176
|
+
atlas.mkdir()
|
|
177
|
+
(atlas / "log-daemon").symlink_to(source)
|
|
178
|
+
monkeypatch.chdir(atlas)
|
|
179
|
+
|
|
180
|
+
assert main(["init"]) == 0
|
|
181
|
+
|
|
182
|
+
assert (atlas / "log-daemon").is_symlink()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "colorama"
|
|
7
|
+
version = "0.4.6"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "iniconfig"
|
|
16
|
+
version = "2.3.0"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
19
|
+
wheels = [
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "packaging"
|
|
25
|
+
version = "26.3"
|
|
26
|
+
source = { registry = "https://pypi.org/simple" }
|
|
27
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
|
28
|
+
wheels = [
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "paranoid-atlas-cli"
|
|
34
|
+
version = "0.1.1"
|
|
35
|
+
source = { editable = "." }
|
|
36
|
+
|
|
37
|
+
[package.dev-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
{ name = "pytest" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[package.metadata]
|
|
43
|
+
|
|
44
|
+
[package.metadata.requires-dev]
|
|
45
|
+
dev = [{ name = "pytest", specifier = ">=8" }]
|
|
46
|
+
|
|
47
|
+
[[package]]
|
|
48
|
+
name = "pluggy"
|
|
49
|
+
version = "1.6.0"
|
|
50
|
+
source = { registry = "https://pypi.org/simple" }
|
|
51
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
52
|
+
wheels = [
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[[package]]
|
|
57
|
+
name = "pygments"
|
|
58
|
+
version = "2.21.0"
|
|
59
|
+
source = { registry = "https://pypi.org/simple" }
|
|
60
|
+
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
|
61
|
+
wheels = [
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "pytest"
|
|
67
|
+
version = "9.1.1"
|
|
68
|
+
source = { registry = "https://pypi.org/simple" }
|
|
69
|
+
dependencies = [
|
|
70
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
71
|
+
{ name = "iniconfig" },
|
|
72
|
+
{ name = "packaging" },
|
|
73
|
+
{ name = "pluggy" },
|
|
74
|
+
{ name = "pygments" },
|
|
75
|
+
]
|
|
76
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
77
|
+
wheels = [
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
79
|
+
]
|