diff --git a/docs/index.md b/docs/index.md index fcc1835..4729164 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ Spec-Driven Development **flips the script** on traditional software development - [Installation Guide](installation.md) - [Quick Start Guide](quickstart.md) + - [Local Development](local-development.md) ## Core Philosophy diff --git a/docs/local-development.md b/docs/local-development.md new file mode 100644 index 0000000..90bf7d3 --- /dev/null +++ b/docs/local-development.md @@ -0,0 +1,131 @@ +# Local Development Guide + +This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first. + +## 1. Clone and Switch Branches + +```bash +git clone https://github.com/github/spec-kit.git +cd spec-kit +# Work on a feature branch +git checkout -b your-feature-branch +``` + +## 2. Run the CLI Directly (Fastest Feedback) + +You can execute the CLI via the module entrypoint without installing anything: + +```bash +# From repo root +python -m src.specify_cli --help +python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools +``` + +If you prefer invoking the script file style (uses shebang): + +```bash +python src/specify_cli/__init__.py init demo-project +``` + +## 3. Use Editable Install (Isolated Environment) + +Create an isolated environment using `uv` so dependencies resolve exactly like end users get them: + +```bash +# Create & activate virtual env (uv auto-manages .venv) +uv venv +source .venv/bin/activate # or on Windows: .venv\\Scripts\\activate + +# Install project in editable mode +uv pip install -e . + +# Now 'specify' entrypoint is available +specify --help +``` + +Re-running after code edits requires no reinstall because of editable mode. + +## 4. Invoke with uvx Directly From Git (Current Branch) + +`uvx` can run from a local path (or a Git ref) to simulate user flows: + +```bash +uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools +``` + +You can also point uvx at a specific branch without merging: + +```bash +# Push your working branch first +git push origin your-feature-branch +uvx --from git+https://github.com/github/spec-kit.git@your-feature-branch specify init demo-branch-test +``` + +## 5. Testing Script Permission Logic +After running an `init`, check that shell scripts are executable on POSIX systems: +```bash +ls -l scripts | grep .sh +# Expect owner execute bit (e.g. -rwxr-xr-x) +``` +On Windows this step is a no-op. + +## 6. Run Lint / Basic Checks (Add Your Own) +Currently no enforced lint config is bundled, but you can quickly sanity check importability: +```bash +python -c "import specify_cli; print('Import OK')" +``` + +## 7. Build a Wheel Locally (Optional) +Validate packaging before publishing: +```bash +uv build +ls dist/ +``` +Install the built artifact into a fresh throwaway environment if needed. + +## 8. Using a Temporary Workspace +When testing `init --here` in a dirty directory, create a temp workspace: +```bash +mkdir /tmp/spec-test && cd /tmp/spec-test +python -m src.specify_cli init --here --ai claude --ignore-agent-tools # if repo copied here +``` +Or copy only the modified CLI portion if you want a lighter sandbox. + +## 9. Debug Network / TLS Skips +If you need to bypass TLS validation while experimenting: +```bash +specify check --skip-tls +specify init demo --skip-tls --ai gemini --ignore-agent-tools +``` +(Use only for local experimentation.) + +## 10. Rapid Edit Loop Summary +| Action | Command | +|--------|---------| +| Run CLI directly | `python -m src.specify_cli --help` | +| Editable install | `uv pip install -e .` then `specify ...` | +| Local uvx run | `uvx --from . specify ...` | +| Git branch uvx | `uvx --from git+URL@branch specify ...` | +| Build wheel | `uv build` | + +## 11. Cleaning Up +Remove build artifacts / virtual env quickly: +```bash +rm -rf .venv dist build *.egg-info +``` + +## 12. Common Issues +| Symptom | Fix | +|---------|-----| +| `ModuleNotFoundError: typer` | Run `uv pip install -e .` | +| Scripts not executable (Linux) | Re-run init (logic adds bits) or `chmod +x scripts/*.sh` | +| Git step skipped | You passed `--no-git` or Git not installed | +| TLS errors on corporate network | Try `--skip-tls` (not for production) | + +## 13. Next Steps +- Update docs and run through Quick Start using your modified CLI +- Open a PR when satisfied +- (Optional) Tag a release once changes land in `main` + +--- +Feel free to expand this guide with additional local workflows (debugging, profiling, test automation) as the project matures. diff --git a/docs/toc.yml b/docs/toc.yml index c12e149..ecabd18 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,10 +1,17 @@ +# Home page - name: Home href: index.md -- name: Installation - href: installation.md -- name: Quick Start - href: quickstart.md -- name: Contributing - href: CONTRIBUTING.md -- name: Support - href: SUPPORT.md + +# Getting started section +- name: Getting Started + items: + - name: Installation + href: installation.md + - name: Quick Start + href: quickstart.md + +# Development workflows +- name: Development + items: + - name: Local Development + href: local-development.md diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 704d354..1e1004c 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -635,6 +635,67 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr return project_path +def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None: + """Ensure POSIX .sh scripts in the project scripts directory have execute bits (no-op on Windows).""" + if os.name == "nt": + return # Windows: skip silently + scripts_dir = project_path / "scripts" + if not scripts_dir.is_dir(): + return + failures: list[str] = [] + updated = 0 + for script in scripts_dir.glob("*.sh"): + try: + # Skip symlinks + if script.is_symlink(): + continue + # Must be a regular file + if not script.is_file(): + continue + # Quick shebang check + try: + with script.open("rb") as f: + first_two = f.read(2) + if first_two != b"#!": + continue + except Exception: + continue + st = script.stat() + mode = st.st_mode + # If already any execute bit set, skip + if mode & 0o111: + continue + # Only add execute bits that correspond to existing read bits + new_mode = mode + if mode & 0o400: # owner read + new_mode |= 0o100 + if mode & 0o040: # group read + new_mode |= 0o010 + if mode & 0o004: # other read + new_mode |= 0o001 + # Fallback: ensure at least owner execute + if not (new_mode & 0o100): + new_mode |= 0o100 + os.chmod(script, new_mode) + updated += 1 + except Exception as e: + failures.append(f"{script.name}: {e}") + if tracker: + detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "") + tracker.add("chmod", "Set script permissions") + if failures: + tracker.error("chmod", detail) + else: + tracker.complete("chmod", detail) + else: + if updated: + console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]") + if failures: + console.print("[yellow]Some scripts could not be updated:[/yellow]") + for f in failures: + console.print(f" - {f}") + + @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), @@ -761,6 +822,7 @@ def init( ("extract", "Extract template"), ("zip-list", "Archive contents"), ("extracted-summary", "Extraction summary"), + ("chmod", "Ensure scripts executable"), ("cleanup", "Cleanup"), ("git", "Initialize git repository"), ("final", "Finalize") @@ -778,6 +840,9 @@ def init( download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client) + # Ensure scripts are executable (POSIX) + ensure_executable_scripts(project_path, tracker=tracker) + # Git step if not no_git: tracker.start("git")