spritegen-cli 0.3.0__tar.gz → 0.4.0__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.
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.gitignore +3 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/PKG-INFO +1 -1
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0006-centralise-configuration-and-never-cache-it.md +2 -1
- spritegen_cli-0.4.0/docs/adr/0016-a-workspace-is-marked-by-the-file-that-configures-it.md +63 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/glossary.md +3 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/notes.md +1 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/pyproject.toml +1 -1
- spritegen_cli-0.4.0/specs/workspace-config/design.md +98 -0
- spritegen_cli-0.4.0/specs/workspace-config/requirements.md +50 -0
- spritegen_cli-0.4.0/specs/workspace-config/tasks.md +25 -0
- spritegen_cli-0.4.0/src/spritegen/__init__.py +27 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/cli.py +17 -2
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/report.py +25 -1
- spritegen_cli-0.4.0/src/spritegen/settings.py +420 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/skill/__init__.py +81 -5
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/workspace.py +28 -14
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/conftest.py +12 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_cli.py +25 -0
- spritegen_cli-0.4.0/tests/test_settings.py +462 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_skill.py +133 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_workspace.py +111 -1
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/uv.lock +217 -195
- spritegen_cli-0.3.0/src/spritegen/__init__.py +0 -3
- spritegen_cli-0.3.0/src/spritegen/settings.py +0 -127
- spritegen_cli-0.3.0/tests/test_settings.py +0 -165
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/agents/code-review.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/agents/security-review.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-adr.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-codewiki.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-glossary.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-init.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-plan-run.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-prd.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-stack.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-wiki.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/artifacts.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/autonomy.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/caveman.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/code-search.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/delivery.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/knowledge-base.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/methodology.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/notes.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/prior-art.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/project.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/routing.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/specs.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/tasks.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/verification.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/scc-manifest.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/adr/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/codewiki/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/glossary/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/init/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/plan-run/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/prd/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/stack/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/wiki/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.env.template +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.gitattributes +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.python-version +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/CLAUDE.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/README.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0001-asset-directory-and-no-path-arguments.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0002-transfer-movement-instead-of-generating-frames.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0003-append-only-jsonl-ledger.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0004-allow-list-every-downloaded-host.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0005-a-directory-per-artifact-kind.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0007-heavy-dependencies-are-optional-extras.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0008-local-backends-are-the-default.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0009-walk-the-redirect-chain-here.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0010-ci-on-three-operating-systems.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0011-keep-pixelfixer-out-of-the-distribution.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0012-publish-with-one-secret.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0013-require-python-3-13.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0014-every-run-is-a-version-and-the-state-names-the-chosen-one.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0015-record-the-call-before-the-files-it-writes.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/spending-money.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/the-stage-registry.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/the-workspace.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/stack.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/changelog.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/index.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/configuration.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/grid-and-palette-recovery.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/local-instead-of-paid.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/motion-transfer.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/paid-calls-and-the-ledger.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-asset-directory.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-generated-skill.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-pipeline.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/plans/code-health.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/plans/motion-optimisation.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/design.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/requirements.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/tasks.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/atlas.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/clip.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/drive.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/endpoints.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/fal.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/imaging.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/ledger.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/matting.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/migrate.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/prompts.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/rrdb.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/sheet.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/skill/files/SKILL.md +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/__init__.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/_common.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/anchor.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/board.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/catalog.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/matte.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/motion.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/pose.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/registry.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/video.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/upscale.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/helpers.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_matted.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_row.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.gif +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/anchor_crop.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_board.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_chroma.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_matted.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/walk_south_board.png +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/manifest.json +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_anchor.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_atlas.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_board_stage.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_clip.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_drive.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_fal.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_imaging.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_ledger.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_matte.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_matting.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_migrate.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_motion.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_parity.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_pose.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_prompts.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_sheet.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_show.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_stages.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_upscale.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_versions.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_video.py +0 -0
- {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/tests_fal_doubles.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# A workspace is marked by the file that configures it
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`adr:0006-centralise-configuration-and-never-cache-it` reads settings from the
|
|
10
|
+
environment, the nearest `.env`, and the declared default, in that order. Where the
|
|
11
|
+
assets are was a separate question, answered by walking up from the working directory
|
|
12
|
+
looking for a directory named `spritegen/assets`.
|
|
13
|
+
|
|
14
|
+
That walk had nothing to recognise. A directory of the right name is not a claim to be
|
|
15
|
+
one, so the search could not tell a workspace from a coincidence — and its ceiling
|
|
16
|
+
admitted the home directory as a candidate. Run in `~/spritegen1/test1`, `init`
|
|
17
|
+
reported a workspace at `~/spritegen/assets` and adopted it: one workspace became every
|
|
18
|
+
project's, and a second project on the same machine was not possible without giving it
|
|
19
|
+
a repository first.
|
|
20
|
+
|
|
21
|
+
The two questions turned out to be one. A project that has its own assets usually wants
|
|
22
|
+
its own cell size, its own matte backend, and — where more than one account is in play —
|
|
23
|
+
its own key.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
**A directory holding `.spritegen.json` is a workspace root.** Its assets are
|
|
28
|
+
`<root>/spritegen/assets`, and the nearest root at or above the working directory is
|
|
29
|
+
the one a command uses.
|
|
30
|
+
|
|
31
|
+
The same file carries the settings, as a JSON object whose names are the `Settings`
|
|
32
|
+
fields without the environment's prefix. It sits **between the environment and the
|
|
33
|
+
`.env`**: an exported variable still wins, which is what makes a one-off override work,
|
|
34
|
+
and a machine with one key in `~/.env` keeps working. A name in it that is not a setting
|
|
35
|
+
is refused rather than ignored.
|
|
36
|
+
|
|
37
|
+
**The home directory is a candidate only when it is where the command was typed.** A
|
|
38
|
+
marker there serves a command run in it and is never climbed into from below.
|
|
39
|
+
|
|
40
|
+
`init` creates the workspace where it was run, always. It writes the marker with no
|
|
41
|
+
credential in it, adds it to a `.gitignore` that exists, and warns when the marker is
|
|
42
|
+
already tracked by git.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
Several workspaces on one machine work, and they work without a repository: the marker
|
|
47
|
+
is the boundary that `.git` used to have to stand in for.
|
|
48
|
+
|
|
49
|
+
A credential can live in a file inside a project, which is why `init` writes it into
|
|
50
|
+
`.gitignore` and warns when git already has it. `allowed_hosts` is settable there too
|
|
51
|
+
(`adr:0004-allow-list-every-downloaded-host`) — the same reach a `.env` already had,
|
|
52
|
+
bounded by the same ceiling.
|
|
53
|
+
|
|
54
|
+
Two files now answer the same question, and that is the cost. `.env` is kept because
|
|
55
|
+
removing it would break every machine that has a key today; whether it eventually goes
|
|
56
|
+
is deliberately not decided here.
|
|
57
|
+
|
|
58
|
+
A workspace opened before this exists has no marker. It keeps working through the
|
|
59
|
+
unmarked fallback, which now holds the same line about the home directory — but it will
|
|
60
|
+
not be found from a sibling project, and `init` in its root is what gives it one.
|
|
61
|
+
|
|
62
|
+
This supersedes `adr:0006-centralise-configuration-and-never-cache-it` **on the list of
|
|
63
|
+
sources only**. One class, nothing cached, and the ceiling all stand.
|
|
@@ -9,6 +9,9 @@ directory `row/` to `sheet/as-is/` — retired a spelling whose word is still in
|
|
|
9
9
|
use elsewhere, and listing it would report every correct use as a finding. Add an
|
|
10
10
|
`Avoid:` the first time a genuinely dead, distinctive name turns up.
|
|
11
11
|
|
|
12
|
+
- **workspace** — a directory holding `.spritegen.json`, and the assets under `spritegen/assets` inside it. One machine carries several, and a command uses the nearest one at or above where it was typed.
|
|
13
|
+
- **workspace root** — the directory the marker is in, which is what `status` names and what `init` creates in. Not the assets directory, which sits inside it.
|
|
14
|
+
- **marker** — `.spritegen.json`: the file that says a directory is a workspace, and holds that workspace's settings. Avoid: config file, settings file
|
|
12
15
|
- **asset** — everything belonging to one character, under `assets/<name>/`. The unit `new`, `status`, `show` and `cost` all talk about, and the reason no stage takes an input or output path.
|
|
13
16
|
- **state file** — `state.json` inside an asset, the only record of progress. It holds the stages that have completed, never what should happen next.
|
|
14
17
|
- **stage** — one step of the pipeline, declared in `stages.STAGES` and implemented by the module of the same name. Most spend money; `board` does not.
|
|
@@ -44,3 +44,4 @@ over this file answers with the example above as well as with the notes. -->
|
|
|
44
44
|
- n-0004 2026-08-28 #gotcha @src/spritegen/sheet.py — the GIF preview writes one global palette plus one shared local palette whatever the frames were quantised with — Pillow unifies on save, so quantising per frame and quantising the strip produce the same file
|
|
45
45
|
- n-0005 2026-08-28 #gotcha @src/spritegen/upscale.py — the weight cache sidecar must stay a content hash — size and mtime are forgeable with the same write access a swap needs, so a stat-based fast path silently disables the check
|
|
46
46
|
- n-0006 2026-08-28 #gotcha @docs/codewiki/the-workspace.md — a codewiki citation that still resolves after a refactor is not still correct — the range shifts onto a neighbouring function and validate cannot tell, so check what each range opens on
|
|
47
|
+
- n-0007 2026-08-29 #gotcha @src/spritegen/__init__.py — the CLI version came from a hand-written __version__ in spritegen/__init__.py, a second copy of what pyproject.toml declares — it read 0.1.0 through three releases; it is importlib.metadata now and a test pins the two together
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Workspace configuration — design
|
|
2
|
+
|
|
3
|
+
## What changes, and where
|
|
4
|
+
|
|
5
|
+
Today a workspace is found by looking for a directory that happens to be named
|
|
6
|
+
`spritegen/assets`, walking up from the working directory. That has no marker, so the
|
|
7
|
+
walk cannot tell a workspace from a coincidence — and it reaches the home directory,
|
|
8
|
+
where one workspace becomes every project's. Run from `~/spritegen1/test1`, `init`
|
|
9
|
+
reported a workspace at `~/spritegen/assets`, which belongs to something else.
|
|
10
|
+
|
|
11
|
+
The change is a marker. `.spritegen.json` at the root of a workspace says *this is
|
|
12
|
+
one*, the way `package.json` does for npm and `pyproject.toml` for this repository. The
|
|
13
|
+
walk stops at the nearest one, so a second project one directory over is a second
|
|
14
|
+
workspace, and nothing has to be configured for that to be true.
|
|
15
|
+
|
|
16
|
+
The same file carries the settings, which is the other half: a credential and a cell
|
|
17
|
+
size belong to the project they are for, not to the shell that happened to run the
|
|
18
|
+
command.
|
|
19
|
+
|
|
20
|
+
- `settings.py` — a source between the environment and `.env`, and the search that
|
|
21
|
+
finds it.
|
|
22
|
+
- `workspace.py` — `assets_root` resolves through the marker rather than by looking for
|
|
23
|
+
a directory of the right name.
|
|
24
|
+
- `skill/__init__.py` — `cmd_init` creates in the working directory and writes the file.
|
|
25
|
+
|
|
26
|
+
## The order settings are read in
|
|
27
|
+
|
|
28
|
+
Highest wins:
|
|
29
|
+
|
|
30
|
+
1. **The environment.** `SPRITEGEN_CELL=200 spritegen board …` still overrides for one
|
|
31
|
+
run, which is `adr:0006`'s reason and does not change.
|
|
32
|
+
2. **`.spritegen.json`** at the workspace root — the new one, and where a project's own
|
|
33
|
+
answer belongs.
|
|
34
|
+
3. **The nearest `.env`** — kept, because a machine with one `FAL_KEY` for everything is
|
|
35
|
+
a real setup and breaking it buys nothing.
|
|
36
|
+
4. **The declared default.**
|
|
37
|
+
|
|
38
|
+
A name in the file that is not a setting is refused rather than ignored: a typo that
|
|
39
|
+
does nothing is worse than one that says so, and this is the file a `fal_key` is
|
|
40
|
+
misspelled in.
|
|
41
|
+
|
|
42
|
+
## The file
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"fal_key": "…",
|
|
47
|
+
"cell": 166,
|
|
48
|
+
"matte_backend": "local"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every field is a `Settings` field under its own name, without the `SPRITEGEN_` prefix
|
|
53
|
+
the environment uses — the prefix exists to keep a shared namespace apart, and a file
|
|
54
|
+
of this tool's own has no such namespace to share.
|
|
55
|
+
|
|
56
|
+
## The credential, and git
|
|
57
|
+
|
|
58
|
+
`init` writes the file with no credential in it, and adds it to `.gitignore` where one
|
|
59
|
+
exists. It cannot add it where there is no `.gitignore`: writing one is a decision
|
|
60
|
+
about a repository this tool does not own.
|
|
61
|
+
|
|
62
|
+
That leaves the case the guard is for — a `.spritegen.json` somebody committed before
|
|
63
|
+
putting a key in it. `git ls-files --error-unmatch` answers whether it is tracked in one
|
|
64
|
+
call and needs no repository when there is none, so a warning is cheap. It warns and
|
|
65
|
+
proceeds: refusing would make the tool unusable in exactly the situation somebody is
|
|
66
|
+
trying to fix.
|
|
67
|
+
|
|
68
|
+
## What a file may point at
|
|
69
|
+
|
|
70
|
+
A path out of this file is **relative to the workspace and resolved under it** — R4.4,
|
|
71
|
+
R4.5. The environment keeps the unrestricted form, and the difference is who decided:
|
|
72
|
+
exporting a variable takes a shell, while this file arrives by being cloned, extracted,
|
|
73
|
+
or synced onto a mounted drive, and is then read for no reason but the command having
|
|
74
|
+
been run in that directory.
|
|
75
|
+
|
|
76
|
+
The reachable case is worse than a redirected output directory. `Path.is_dir()` on
|
|
77
|
+
`\host\share` opens an SMB connection and authenticates, so `status` — free, and
|
|
78
|
+
touching no network — would hand a credential to whoever wrote the file. So rooted
|
|
79
|
+
counts, not only absolute: on Windows `/etc/x` carries no drive and is not
|
|
80
|
+
`is_absolute()`, and it leaves the workspace all the same.
|
|
81
|
+
|
|
82
|
+
**`allowed_hosts` becomes settable from a file inside the project**
|
|
83
|
+
(`adr:0004-allow-list-every-downloaded-host`). That is the same reach a `.env` already
|
|
84
|
+
had, and the ceiling in R1.5 is what bounds both — worth stating rather than
|
|
85
|
+
discovering.
|
|
86
|
+
|
|
87
|
+
## What this supersedes
|
|
88
|
+
|
|
89
|
+
`adr:0006-centralise-configuration-and-never-cache-it` names the environment, the
|
|
90
|
+
nearest `.env` and the default as the three sources, in that order. It gains a fourth
|
|
91
|
+
between the first two, and its other decisions — one class, nothing cached, the ceiling
|
|
92
|
+
— stand. A new record supersedes it on the source list alone.
|
|
93
|
+
|
|
94
|
+
## Not decided here
|
|
95
|
+
|
|
96
|
+
Whether `.env` is eventually dropped. Two files for one job is one too many, but
|
|
97
|
+
removing one while adding the other would break every machine that has a key today,
|
|
98
|
+
and nothing forces the choice now.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
autonomy: auto
|
|
3
|
+
ci: wait
|
|
4
|
+
branch: feat/workspace-config
|
|
5
|
+
delivery: in-review
|
|
6
|
+
pr: 24
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workspace configuration — requirements
|
|
10
|
+
|
|
11
|
+
A workspace is marked by a file it holds, so one machine can carry several and a
|
|
12
|
+
command run inside one never reaches into another.
|
|
13
|
+
|
|
14
|
+
## R1 — where the workspace is
|
|
15
|
+
|
|
16
|
+
- **R1.1** The system shall treat a directory holding `.spritegen.json` as a workspace root.
|
|
17
|
+
- **R1.2** When a command needs a workspace, the system shall use the nearest workspace root at or above the working directory.
|
|
18
|
+
- **R1.3** If no workspace root is at or above the working directory, then the system shall refuse and name `spritegen init`.
|
|
19
|
+
- **R1.4** While a workspace root is in use, the system shall keep the assets under `<root>/spritegen/assets`.
|
|
20
|
+
- **R1.5** The system shall stop the upward search at the first repository boundary or at the home directory.
|
|
21
|
+
|
|
22
|
+
## R2 — opening one
|
|
23
|
+
|
|
24
|
+
- **R2.1** When `init` runs, the system shall create the workspace in the working directory.
|
|
25
|
+
- **R2.2** When `init` creates a workspace, the system shall write `.spritegen.json` in that directory.
|
|
26
|
+
- **R2.3** Where a `.gitignore` is at the workspace root, the system shall add `.spritegen.json` to it.
|
|
27
|
+
- **R2.4** If a workspace root is already at or above the working directory, then the system shall name it and create the new one anyway.
|
|
28
|
+
- **R2.5** If `.spritegen.json` is already in the working directory, then the system shall leave its contents alone.
|
|
29
|
+
|
|
30
|
+
## R3 — what configuration is read
|
|
31
|
+
|
|
32
|
+
- **R3.1** The system shall read its settings from `.spritegen.json` at the workspace root.
|
|
33
|
+
- **R3.2** Where a setting is in the environment, the environment shall win over the file.
|
|
34
|
+
- **R3.3** Where a setting is in neither, the system shall use the `.env` the search finds, and then the default.
|
|
35
|
+
- **R3.4** If `.spritegen.json` does not parse, then the system shall refuse naming the file and the reason.
|
|
36
|
+
- **R3.5** If `.spritegen.json` holds a name that is not a setting, then the system shall refuse naming it.
|
|
37
|
+
- **R3.6** The system shall build its settings fresh on every read and cache nothing.
|
|
38
|
+
|
|
39
|
+
## R4 — the key in it
|
|
40
|
+
|
|
41
|
+
- **R4.1** Where `.spritegen.json` holds `fal_key`, the system shall use it as the credential.
|
|
42
|
+
- **R4.2** When `init` writes `.spritegen.json`, the system shall write no credential into it.
|
|
43
|
+
- **R4.3** If `.spritegen.json` is tracked by git, then the system shall warn that a credential in it is committed.
|
|
44
|
+
- **R4.4** (ADDED) Where `.spritegen.json` sets a path, the system shall resolve it under the workspace root.
|
|
45
|
+
- **R4.5** (ADDED) If `.spritegen.json` sets a path that is absolute, rooted or outside the workspace, then the system shall refuse it.
|
|
46
|
+
|
|
47
|
+
## R5 — saying which one
|
|
48
|
+
|
|
49
|
+
- **R5.1** When `status` runs, the system shall name the workspace root it is using.
|
|
50
|
+
- **R5.2** When `init` finishes, the system shall name the directory it created.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Workspace configuration — tasks
|
|
2
|
+
|
|
3
|
+
## Tasks
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 (TDD) Find the workspace root by the marker file, bounded by the ceiling — R1.1, R1.2, R1.5
|
|
6
|
+
- [x] 1.2 (Unit) Resolve the assets under the root the marker names, and refuse when there is none — R1.3, R1.4
|
|
7
|
+
_Depends 1.1_
|
|
8
|
+
- [x] 2.1 (TDD) Read `.spritegen.json` between the environment and `.env`, refusing a name that is not a setting — R3.1, R3.2, R3.3, R3.4, R3.5, R3.6
|
|
9
|
+
_Depends 1.1_
|
|
10
|
+
- [x] 2.2 (Unit) Take the credential from the file — R4.1
|
|
11
|
+
_Depends 2.1_
|
|
12
|
+
- [x] 3.1 (Unit) Create the workspace where `init` was run, and write the marker with no credential in it — R2.1, R2.2, R2.4, R2.5, R4.2, R5.2
|
|
13
|
+
_Depends 1.2_
|
|
14
|
+
- [x] 3.2 (Unit) Add the marker to a `.gitignore` that is there — R2.3
|
|
15
|
+
_Depends 3.1_
|
|
16
|
+
- [x] 3.3 (Unit) Warn when the marker is tracked by git — R4.3
|
|
17
|
+
_Depends 3.1_
|
|
18
|
+
- [x] 4.1 (Unit) Name the workspace root in `status` — R5.1
|
|
19
|
+
_Depends 1.2_
|
|
20
|
+
- [x] 4.2 (Unit) Record the decision as an ADR superseding `adr:0006` on its source list, and the term in the glossary — R3.1
|
|
21
|
+
_Depends 2.1_
|
|
22
|
+
- [x] 3.4 (Unit) Stop the skill search climbing into the home directory's .claude — R1.5
|
|
23
|
+
_Reason the same defect in the other search: the docstring says it is bounded and it is not, so init in a fresh project reported the global skill as already there and set that project up with none_
|
|
24
|
+
- [x] 2.3 (Unit) Bound a path the marker sets to the workspace it is in — R4.4, R4.5
|
|
25
|
+
_Reason security review: a hostile marker could point assets at a UNC path, and stat on a share authenticates, so status alone leaked a credential_
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Turn a handful of images into a game-ready character sheet, one stage at a time."""
|
|
2
|
+
|
|
3
|
+
_VERSION: str | None = None
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name: str) -> str:
|
|
7
|
+
"""`__version__`, read from the installed metadata the first time it is asked for.
|
|
8
|
+
|
|
9
|
+
Not a constant: it was one, written out by hand, and it read 0.1.0 through two
|
|
10
|
+
releases that had bumped `pyproject.toml` — the declaration is the only record now.
|
|
11
|
+
|
|
12
|
+
Resolved lazily because `importlib.metadata` reads dist-info off disk, and every
|
|
13
|
+
command builds the parser: `--help`, `status` and `cost` would each pay some thirty
|
|
14
|
+
milliseconds for a string only `--version` prints.
|
|
15
|
+
"""
|
|
16
|
+
global _VERSION
|
|
17
|
+
if name != "__version__":
|
|
18
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
19
|
+
if _VERSION is None:
|
|
20
|
+
from importlib.metadata import PackageNotFoundError
|
|
21
|
+
from importlib.metadata import version as installed
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
_VERSION = installed("spritegen-cli")
|
|
25
|
+
except PackageNotFoundError: # pragma: no cover - a checkout nobody installed
|
|
26
|
+
_VERSION = "0.0.0+source"
|
|
27
|
+
return _VERSION
|
|
@@ -22,7 +22,22 @@ import os
|
|
|
22
22
|
import sys
|
|
23
23
|
from collections.abc import Sequence
|
|
24
24
|
|
|
25
|
-
from . import
|
|
25
|
+
from . import stages
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _PrintVersion(argparse.Action):
|
|
29
|
+
"""`--version`, resolved when it is asked for rather than when the parser is built.
|
|
30
|
+
|
|
31
|
+
Every command builds the parser, and reading the installed metadata costs real
|
|
32
|
+
milliseconds — see the module `__getattr__` this defers to.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
36
|
+
from . import __version__
|
|
37
|
+
|
|
38
|
+
# stdout and exit 0, which is where argparse's own version action puts it.
|
|
39
|
+
print(f"spritegen {__version__}")
|
|
40
|
+
parser.exit()
|
|
26
41
|
|
|
27
42
|
|
|
28
43
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -31,7 +46,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
31
46
|
prog="spritegen",
|
|
32
47
|
description="Turn a handful of images into a character sheet, one stage at a time.",
|
|
33
48
|
)
|
|
34
|
-
parser.add_argument("--version", action=
|
|
49
|
+
parser.add_argument("--version", action=_PrintVersion, nargs=0, help="print the version")
|
|
35
50
|
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
36
51
|
|
|
37
52
|
new = sub.add_parser("new", help="open a new asset")
|
|
@@ -39,16 +39,39 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
39
39
|
console this tool cannot encode into drops the line rather than raising where
|
|
40
40
|
anyone would see it.
|
|
41
41
|
"""
|
|
42
|
+
from . import settings
|
|
43
|
+
|
|
42
44
|
root = assets_root()
|
|
43
45
|
as_json = getattr(args, "json", False)
|
|
46
|
+
# Which workspace answered, by name — R5.1. A machine carries several now, and
|
|
47
|
+
# "no assets here" is a different sentence from "the workspace you meant is
|
|
48
|
+
# elsewhere": the second one is what a command run in the wrong directory needs.
|
|
49
|
+
declared = settings.workspace_root()
|
|
44
50
|
|
|
45
51
|
if not root.is_dir():
|
|
46
52
|
if as_json:
|
|
47
|
-
print(
|
|
53
|
+
print(
|
|
54
|
+
json.dumps(
|
|
55
|
+
{
|
|
56
|
+
"root": str(root),
|
|
57
|
+
"workspace": False,
|
|
58
|
+
"declared_at": str(declared) if declared else None,
|
|
59
|
+
"assets": [],
|
|
60
|
+
},
|
|
61
|
+
indent=2,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
48
64
|
return 0
|
|
49
65
|
print(f"no spritegen workspace at {root}; run `spritegen init` to make one")
|
|
50
66
|
return 0
|
|
51
67
|
|
|
68
|
+
if not as_json:
|
|
69
|
+
# Only where a marker actually said so. `root.parent.parent` guessed the shape
|
|
70
|
+
# `<root>/spritegen/assets`, which is wrong for SPRITEGEN_ASSETS pointing
|
|
71
|
+
# anywhere and for the one-level legacy `assets/` — and a wrong directory printed
|
|
72
|
+
# under this heading reads as authoritative.
|
|
73
|
+
print(f"workspace {declared}" if declared else f"assets {root}")
|
|
74
|
+
|
|
52
75
|
# Resolved once, at the top of the command, and passed down from here: every asset,
|
|
53
76
|
# and every version inside each one, would otherwise rebuild it from the environment.
|
|
54
77
|
names = known(root)
|
|
@@ -58,6 +81,7 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
58
81
|
{
|
|
59
82
|
"root": str(root),
|
|
60
83
|
"workspace": True,
|
|
84
|
+
"declared_at": str(declared) if declared else None,
|
|
61
85
|
"assets": [inspect(name, root) for name in names],
|
|
62
86
|
},
|
|
63
87
|
indent=2,
|