T-002 — init: idempotent + --force / --out
Goal
Make autorepro init create a default devcontainer.json, idempotently. It must not overwrite an existing file unless --force is provided, and it must support writing the file to an alternate path via --out. Print clear messages and return correct exit codes.
Scope (concise implementation)
1) env.py
default_devcontainer() -> dict
- Returns:
name: "autorepro-dev"
features:
ghcr.io/devcontainers/features/python:1 = {"version": "3.11"}
ghcr.io/devcontainers/features/node:1 = {"version": "20"}
ghcr.io/devcontainers/features/go:1 = {"version": "1.22"}
postCreateCommand:
python -m venv .venv && . .venv/bin/activate && python -m pip install -e . && python -m pip install pytest
write_devcontainer(repo_dir: str | Path, *, force: bool = False, out: str | Path | None = None) -> Path
- Compute
target:
- If
out is provided → use it as an absolute or relative file path.
- Else →
<repo_dir>/devcontainer.json.
- Ensure parent dirs:
target.parent.mkdir(parents=True, exist_ok=True).
- Build
content = json.dumps(default_devcontainer(), indent=2).
- Idempotent / atomic write:
- If
target exists and force == False → do not write; just return target.
- If writing: write to a temporary file in the same directory, then
replace() into target (best-effort atomic on the platform).
- Always return
Path(target).
- Note: no printing here; printing is the CLI’s responsibility.
2) cli.py
- Add subcommand:
init
- Flags:
--out PATH
--force (bool)
- Behavior:
- Set
repo_dir = "." and forward flags to write_devcontainer.
- Light validation:
- If
--out points to an existing directory (not a file), treat as misuse → print a clear message and return exit code 2.
- Output:
- If not written due to existing file and no
--force:
devcontainer.json already exists at <path>.
Use --force to overwrite or --out <path> to write elsewhere.
Exit 0.
- If written the first time:
Wrote devcontainer to <path>
Exit 0.
- If overwritten with
--force:
Overwrote devcontainer at <path>
Exit 0.
- Unexpected filesystem errors → print a short message and return exit code 1.
- Reminder:
main() stays in the current style (parse once + dispatch), and SystemExit is mapped to an int return as previously fixed.
Acceptance Criteria (Definition of Done)
- Running
autorepro init in an empty directory creates a valid JSON devcontainer.json and prints its path.
- Re-running without
--force does not change the file and prints “already exists …” (exit 0).
- Running with
--force overwrites and prints “Overwrote …”.
--out dev/devcontainer.json writes to that path and creates missing parent directories.
- All tests pass on CI (Ubuntu, Python 3.11).
Test Plan (tests/test_init.py)
- Create new
- In
tmp_path, run CLI: init.
- Assert
devcontainer.json exists.
- Load JSON and assert keys (
features, postCreateCommand).
- Idempotent (no
--force)
- Run
init twice.
- Compare
mtime or contents before/after to confirm no change.
- Assert “already exists” text.
- Force
- Record
mtime/content; run init --force; assert mtime changed (or content re-written) and “Overwrote …” is printed.
- Out path
init --out dev/devcontainer.json
- File is written to
tmp_path/"dev/devcontainer.json"; directory dev/ is created automatically.
- Invalid
--out (points to a directory)
- Create a directory
foo/; run init --out foo
- Return exit 2 with a clear message.
- Exit codes
- All successful cases (create/skip/overwrite) return 0.
- Misuse (
--out is a directory) returns 2.
- Optional: simulate I/O error returns 1.
Implementation note: run CLI tests via capsys or by calling main() with monkeypatch.setenv / sys.argv. Optionally unit-test env.write_devcontainer separately (without CLI).
README Examples
$ autorepro init
Wrote devcontainer to ./devcontainer.json
$ autorepro init
devcontainer.json already exists at ./devcontainer.json
Use --force to overwrite or --out <path> to write elsewhere.
$ autorepro init --force
Overwrote devcontainer at ./devcontainer.json
$ autorepro init --out dev/devcontainer.json
Wrote devcontainer to dev/devcontainer.json
T-002 —
init: idempotent +--force/--outGoal
Make
autorepro initcreate a defaultdevcontainer.json, idempotently. It must not overwrite an existing file unless--forceis provided, and it must support writing the file to an alternate path via--out. Print clear messages and return correct exit codes.Scope (concise implementation)
1)
env.pydefault_devcontainer() -> dictname: "autorepro-dev"features:ghcr.io/devcontainers/features/python:1 = {"version": "3.11"}ghcr.io/devcontainers/features/node:1 = {"version": "20"}ghcr.io/devcontainers/features/go:1 = {"version": "1.22"}postCreateCommand:write_devcontainer(repo_dir: str | Path, *, force: bool = False, out: str | Path | None = None) -> Pathtarget:outis provided → use it as an absolute or relative file path.<repo_dir>/devcontainer.json.target.parent.mkdir(parents=True, exist_ok=True).content = json.dumps(default_devcontainer(), indent=2).targetexists andforce == False→ do not write; just returntarget.replace()intotarget(best-effort atomic on the platform).Path(target).2)
cli.pyinit--out PATH--force(bool)repo_dir = "."and forward flags towrite_devcontainer.--outpoints to an existing directory (not a file), treat as misuse → print a clear message and return exit code 2.--force:--force:main()stays in the current style (parse once + dispatch), andSystemExitis mapped to anintreturn as previously fixed.Acceptance Criteria (Definition of Done)
autorepro initin an empty directory creates a valid JSONdevcontainer.jsonand prints its path.--forcedoes not change the file and prints “already exists …” (exit 0).--forceoverwrites and prints “Overwrote …”.--out dev/devcontainer.jsonwrites to that path and creates missing parent directories.Test Plan (
tests/test_init.py)tmp_path, run CLI:init.devcontainer.jsonexists.features,postCreateCommand).--force)inittwice.mtimeor contents before/after to confirm no change.mtime/content; runinit --force; assertmtimechanged (or content re-written) and “Overwrote …” is printed.init --out dev/devcontainer.jsontmp_path/"dev/devcontainer.json"; directorydev/is created automatically.--out(points to a directory)foo/; runinit --out foo--outis a directory) returns 2.Implementation note: run CLI tests via
capsysor by callingmain()withmonkeypatch.setenv/sys.argv. Optionally unit-testenv.write_devcontainerseparately (without CLI).README Examples