feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver#3186
feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver#3186PascalThuet wants to merge 6 commits into
Conversation
63a6aa0 to
163d525
Compare
|
One design point worth a call before merge: how
The tension: uniform behavior and shell parity can't both hold. Making the first group refuse symlinks would diverge from the shell contract #2892 established; making the second group follow them would weaken the bundler's write confinement. So I kept each surface aligned with its existing cwd-path stance and documented the split in If you'd rather have one rule, I can make it uniformly strict (refuse a symlinked |
|
Let's go with what you have — keep the split. I want to reframe why, though, because "shell parity" undersells it. The rule I'd like documented isn't "match the shell," it's:
Each surface's override path should mirror its existing cwd-path stance, which is exactly what you've done:
So the difference between the two groups isn't an inconsistency to apologize for; it's the same per-surface symlink policy we already ship, just relocated. Going uniformly strict would either make a surface's override path stricter than its own cwd path (a new inconsistency), or change the cwd behavior of the read surfaces — which is scope creep and would break a legitimate pattern (a shared One small ask before merge: please reword the |
…iant Per maintainer feedback on github#3186: SPECIFY_INIT_DIR relocates where the project is, not how a surface treats symlinks. Each surface keeps its cwd-path stance (write surfaces refuse a symlinked .specify, read/config surfaces follow it), so the split is one policy relocated, not an inconsistency.
|
Done. Reworded the |
The shell resolver honors SPECIFY_INIT_DIR (github#2892), but the Python CLI did not: it resolved the project as Path.cwd() + a .specify/ check and never read the override. So setup-plan.sh respected it while `specify integration install` ignored it, and you still had to cd into the member project. Route project resolution through a shared _resolve_init_dir_override() that applies the shell resolver's validation rules (relative to cwd, must exist and contain .specify/, hard error, no fallback, same error strings). It's wired into _require_specify_project() — the chokepoint for every project-scoped subcommand (integration/extension/workflow/preset/...) — and the `workflow run <file>` standalone path, which re-applies its symlinked-.specify guard on the override branch too. init is unchanged: it creates .specify/, so the must-pre-exist rule doesn't apply. The resolver canonicalizes symlinks via Path.resolve() while the shell keeps the logical path; they agree for non-symlinked paths (documented in the resolver). Tests in tests/test_init_dir_cli.py mirror the strict cases from test_init_dir.py through the CLI; conftest now strips SPECIFY_* for the whole suite so a stray export can't perturb the now-env-reading resolver. Docs note the CLI applies the same rules. Discussion: github#2834 (Disclosure: I used an AI coding agent to audit the call sites and resolver, draft the change, and run an adversarial code review; reviewed by me.)
Assisted-by: Codex (model: GPT-5, autonomous)
…ide path find_project_root refuses a symlinked .specify (following it could read/write outside the tree, and a test pins that), but the SPECIFY_INIT_DIR override added for bundle commands returned early and skipped that guard: _resolve_init_dir_override validates .specify with is_dir(), which follows symlinks. So `specify bundle` accepted via the override a layout the cwd path rejects. Re-check the override result with the same guard, plus a regression test. (Disclosure: found via an AI code review and fixed with an AI coding agent; reviewed by me.)
Treat an explicit symlinked SPECIFY_INIT_DIR project as a hard bundle error instead of returning no project, which could initialize the current directory. Align the docs with the actual unset resolver behavior. Assisted-by: Codex (model: GPT-5, autonomous)
A symlinked .specify is followed by integration/extension/workflow (matching the shell resolver) but refused by bundle and workflow run <file> (write confinement). Document the asymmetry so it reads as intentional. (Disclosure: AI-assisted; reviewed by me.)
…iant Per maintainer feedback on github#3186: SPECIFY_INIT_DIR relocates where the project is, not how a surface treats symlinks. Each surface keeps its cwd-path stance (write surfaces refuse a symlinked .specify, read/config surfaces follow it), so the split is one policy relocated, not an inconsistency.
9b2ba6f to
97c4720
Compare
Description
Follow-up to #2892, per the go-ahead on #2834.
SPECIFY_INIT_DIRonly worked in the shell scripts; the Python CLI ignored it, so you still had tocdinto the member project to runspecify integration/extension/workflow. This makes the CLI's project resolver honor it too, with the same validation rules as the shell: relative to cwd, must exist and contain.specify/, hard error, no fallback.initis unchanged (it creates.specify/).No
--projectflag, per your note that the env var is the exception, not the rule.Testing
specify --helpNew tests in
tests/test_init_dir_cli.pycover targeting from outside the project, path normalization, and the no-fallback error cases.AI Disclosure
AI agent used to audit the call sites, draft the change, and review the diff; reviewed by me.