feat(cli): embed core pack in wheel for offline/air-gapped deployment (#1803)

* feat(cli): embed core pack in wheel + offline-first init (#1711, #1752)

Bundle templates, commands, and scripts inside the specify-cli wheel so
that `specify init` works without any network access by default.

Changes:
- pyproject.toml: add hatchling force-include for core_pack assets; bump
  version to 0.2.1
- __init__.py: add _locate_core_pack(), _generate_agent_commands() (Python
  port of generate_commands() shell function), and scaffold_from_core_pack();
  modify init() to scaffold from bundled assets by default; add --from-github
  flag to opt back in to the GitHub download path
- release.yml: build wheel during CI release job
- create-github-release.sh: attach .whl as a release asset
- docs/installation.md: add Enterprise/Air-Gapped Installation section
- README.md: add Option 3 enterprise install with accurate offline story

Closes #1711
Addresses #1752

* fix(tests): update kiro alias test for offline-first scaffold path

* feat(cli): invoke bundled release script at runtime for offline scaffold

- Embed release scripts (bash + PowerShell) in wheel via pyproject.toml
- Replace Python _generate_agent_commands() with subprocess invocation of
  the canonical create-release-packages.sh, guaranteeing byte-for-byte
  parity between 'specify init --offline' and GitHub release ZIPs
- Fix macOS bash 3.2 compat in release script: replace cp --parents,
  local -n (nameref), and mapfile with POSIX-safe alternatives
- Fix _TOML_AGENTS: remove qwen (uses markdown per release script)
- Rename --from-github to --offline (opt-in to bundled assets)
- Add _locate_release_script() for cross-platform script discovery
- Update tests: remove bash 4+/GNU coreutils requirements, handle
  Kimi directory-per-skill layout, 576 tests passing
- Update CHANGELOG and docs/installation.md

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(offline): error out if --offline fails instead of falling back to network

- _locate_core_pack() docstring now accurately describes that it only
  finds wheel-bundled core_pack/; source-checkout fallback lives in callers
- init() --offline + no bundled assets now exits with a clear error
  (previously printed a warning and silently fell back to GitHub download)
- init() scaffold failure under --offline now exits with an error
  instead of retrying via download_and_extract_template

Addresses reviewer comment: https://github.com/github/spec-kit/pull/1803

* fix(offline): address PR review comments

- fix(shell): harden validate_subset against glob injection in case patterns
- fix(shell): make GENRELEASES_DIR overridable via env var for test isolation
- fix(cli): probe pwsh then powershell on Windows instead of hardcoding pwsh
- fix(cli): remove unreachable fallback branch when --offline fails
- fix(cli): improve --offline error message with common failure causes
- fix(release): move wheel build step after create-release-packages.sh
- fix(docs): add --offline to installation.md air-gapped example
- fix(tests): remove unused genreleases_dir param from _run_release_script
- fix(tests): rewrite parity test to run one agent at a time with isolated
  temp dirs, preventing cross-agent interference from rm -rf

* fix(offline): address second round of review comments

- fix(shell): replace case-pattern membership with explicit loop + == check
  for unambiguous glob-safety in validate_subset()
- fix(cli): require pwsh (PowerShell 7) only; drop powershell (PS5) fallback
  since the bundled script uses #requires -Version 7.0
- fix(cli): add bash and zip preflight checks in scaffold_from_core_pack()
  with clear error messages if either is missing
- fix(build): list individual template files in pyproject.toml force-include
  to avoid duplicating templates/commands/ in the wheel

* fix(offline): address third round of review comments

- Add 120s timeout to subprocess.run in scaffold_from_core_pack to prevent
  indefinite hangs during offline scaffolding
- Add test_pyproject_force_include_covers_all_templates to catch missing
  template files in wheel bundling
- Tighten kiro alias test to assert specific scaffold path (download vs offline)

* fix(offline): address Copilot review round 4

- fix(offline): use handle_vscode_settings() merge for --here --offline
  to prevent data loss on existing .vscode/settings.json
- fix(release): glob wheel filename in create-github-release.sh instead
  of hardcoding version, preventing upload failures on version mismatch
- docs(release): add comment noting pyproject.toml version is synced by
  release-trigger.yml before the tag is pushed

* fix(offline): address review round 5 + offline bundle ZIP

- fix(offline): pwsh-only, no powershell.exe fallback; clarify error message
- fix(offline): tighten _has_bundled to check scripts dir for source checkouts
- feat(release): build specify-bundle-v*.zip with all deps at release time
- feat(release): attach offline bundle ZIP to GitHub release assets
- docs: simplify air-gapped install to single ZIP download from releases
- docs: add Windows PowerShell 7+ (pwsh) requirement note

* fix(tests): session-scoped scaffold cache + timeout + dead code removal

- Add timeout=300 and returncode check to _run_release_script() to fail
  fast with clear output on script hangs or failures
- Remove unused import specify_cli, _SOURCE_TEMPLATES, bundled_project fixture
- Add session-scoped scaffolded_sh/scaffolded_ps fixtures that scaffold
  once per agent and reuse the output directory across all invariant tests
- Reduces test_core_pack_scaffold runtime from ~175s to ~51s (3.4x faster)
- Parity tests still scaffold independently for isolation

* fix(offline): remove wheel from release, update air-gapped docs to use pip download

* fix(tests): handle codex skills layout and iflow agent in scaffold tests

Codex now uses create_skills() with hyphenated separator (speckit-plan/SKILL.md)
instead of generate_commands(). Update _SKILL_AGENTS, _expected_ext, and
_list_command_files to handle both codex ('-') and kimi ('.') skill agents.
Also picks up iflow as a new testable agent automatically via AGENT_CONFIG.

* fix(offline): require wheel core_pack for --offline, remove source-checkout fallback

--offline now strictly requires _locate_core_pack() to find the wheel's
bundled core_pack/ directory. Source-checkout fallbacks are no longer
accepted at the init() level — if core_pack/ is missing, the CLI errors
out with a clear message pointing to the installation docs.

scaffold_from_core_pack() retains its internal source-checkout fallbacks
so parity tests can call it directly from a source checkout.

* fix(offline): remove stale [Unreleased] CHANGELOG section, scope httpx.Client to download path

- Remove entire [Unreleased] section — CHANGELOG is auto-generated at release
- Move httpx.Client into use_github branch with context manager so --offline
  path doesn't allocate an unused network client

* fix(offline): remove dead --from-github flag, fix typer.Exit handling, add page templates validation

- Remove unused --from-github CLI option and docstring example
- Add (typer.Exit, SystemExit) re-raise before broad except Exception
  to prevent duplicate error panel on offline scaffold failure
- Validate page templates directory exists in scaffold_from_core_pack()
  to fail fast on incomplete wheel installs
- Fix ruff lint: remove unused shutil import, remove f-prefix on
  strings without placeholders in test_core_pack_scaffold.py

* docs(offline): add v0.6.0 deprecation notice with rationale

- Help text: note bundled assets become default in v0.6.0
- Docstring: explain why GitHub download is being retired (no network
  dependency, no proxy/firewall issues, guaranteed version match)
- Runtime nudge: when bundled assets are available but user takes the
  GitHub download path, suggest --offline with rationale
- docs/installation.md: add deprecation notice with full rationale

* fix(offline): allow --offline in source checkouts, fix CHANGELOG truncation

- Simplify use_github logic: use_github = not offline (let
  scaffold_from_core_pack handle fallback to source-checkout paths)
- Remove hard-fail when core_pack/ is absent — scaffold_from_core_pack
  already falls back to repo-root templates/scripts/commands
- Fix truncated 'skill…' → 'skills' in CHANGELOG.md

* fix(offline): sandbox GENRELEASES_DIR and clean up on failure

- Pin GENRELEASES_DIR to temp dir in scaffold_from_core_pack() so a
  user-exported value cannot redirect output or cause rm -rf outside
  the sandbox
- Clean up partial project directory on --offline scaffold failure
  (same behavior as the GitHub-download failure path)

* fix(tests): use shutil.which for bash discovery, add ps parity tests

- _find_bash() now tries shutil.which('bash') first so non-standard
  install locations (Nix, custom CI images) are found
- Parametrize parity test over both 'sh' and 'ps' script types to
  ensure PowerShell variant stays byte-for-byte identical to release
  script output (353 scaffold tests, 810 total)

* fix(tests): parse pyproject.toml with tomllib, remove unused fixture

- Use tomllib to parse force-include keys from the actual TOML table
  instead of raw substring search (avoids false positives)
- Remove unused source_template_stems fixture from
  test_scaffold_command_dir_location

* fix: guard GENRELEASES_DIR against unsafe values, update docstring

- Add safety check in create-release-packages.sh: reject empty, '/',
  '.', '..' values for GENRELEASES_DIR before rm -rf
- Strip trailing slash to avoid path surprises
- Update scaffold_from_core_pack() docstring to accurately describe
  all failure modes (not just 'assets not found')

* fix: harden GENRELEASES_DIR guard, cache parity tests, safe iterdir

- Reject '..' path segments in GENRELEASES_DIR to prevent traversal
- Session-cache both scaffold and release-script results in parity
  tests — runtime drops from ~74s to ~45s (40% faster)
- Guard cmd_dir.iterdir() in assertion message against missing dirs

* fix(tests): exclude YAML frontmatter source metadata from path rewrite check

The codex and kimi SKILL.md files have 'source: templates/commands/...'
in their YAML frontmatter — this is provenance metadata, not a runtime
path that needs rewriting. Strip frontmatter before checking for bare
scripts/ and templates/ paths.

* fix(offline): surface scaffold failure detail in error output

When --offline scaffold fails, look up the tracker's 'scaffold' step
detail and print it alongside the generic error message so users see
the specific root cause (e.g. missing zip/pwsh, script stderr).

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Manfred Riem
2026-03-20 14:46:48 -05:00
committed by GitHub
parent a7606c0f14
commit bf33980426
9 changed files with 1082 additions and 104 deletions

View File

@@ -12,18 +12,22 @@
### Initialize a New Project
The easiest way to get started is to initialize a new project:
The easiest way to get started is to initialize a new project. Pin a specific release tag for stability (check [Releases](https://github.com/github/spec-kit/releases) for the latest):
```bash
# Install from a specific stable release (recommended — replace vX.Y.Z with the latest tag)
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJECT_NAME>
# Or install latest from main (may include unreleased changes)
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
```
Or initialize in the current directory:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init .
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init .
# or use the --here flag
uvx --from git+https://github.com/github/spec-kit.git specify init --here
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here
```
### Specify AI Agent
@@ -31,11 +35,11 @@ uvx --from git+https://github.com/github/spec-kit.git specify init --here
You can proactively specify your AI agent during initialization:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai claude
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai gemini
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai codebuddy
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai pi
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai gemini
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai codebuddy
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai pi
```
### Specify Script Type (Shell vs PowerShell)
@@ -51,8 +55,8 @@ Auto behavior:
Force a specific script type:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script sh
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script ps
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --script sh
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --script ps
```
### Ignore Agent Tools Check
@@ -60,7 +64,7 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <project_name
If you prefer to get the templates without checking for the right tools:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai claude --ignore-agent-tools
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude --ignore-agent-tools
```
## Verification
@@ -75,6 +79,52 @@ The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
## Troubleshooting
### Enterprise / Air-Gapped Installation
If your environment blocks access to PyPI (you see 403 errors when running `uv tool install` or `pip install`), you can create a portable wheel bundle on a connected machine and transfer it to the air-gapped target.
**Step 1: Build the wheel on a connected machine (same OS and Python version as the target)**
```bash
# Clone the repository
git clone https://github.com/github/spec-kit.git
cd spec-kit
# Build the wheel
pip install build
python -m build --wheel --outdir dist/
# Download the wheel and all its runtime dependencies
pip download -d dist/ dist/specify_cli-*.whl
```
> **Important:** `pip download` resolves platform-specific wheels (e.g., PyYAML includes native extensions). You must run this step on a machine with the **same OS and Python version** as the air-gapped target. If you need to support multiple platforms, repeat this step on each target OS (Linux, macOS, Windows) and Python version.
**Step 2: Transfer the `dist/` directory to the air-gapped machine**
Copy the entire `dist/` directory (which contains the `specify-cli` wheel and all dependency wheels) to the target machine via USB, network share, or other approved transfer method.
**Step 3: Install on the air-gapped machine**
```bash
pip install --no-index --find-links=./dist specify-cli
```
**Step 4: Initialize a project (no network required)**
```bash
# Initialize a project — no GitHub access needed
specify init my-project --ai claude --offline
```
The `--offline` flag tells the CLI to use the templates, commands, and scripts bundled inside the wheel instead of downloading from GitHub.
> **Deprecation notice:** Starting with v0.6.0, `specify init` will use bundled assets by default and the `--offline` flag will be removed. The GitHub download path will be retired because bundled assets eliminate the need for network access, avoid proxy/firewall issues, and guarantee that templates always match the installed CLI version. No action will be needed — `specify init` will simply work without network access out of the box.
> **Note:** Python 3.11+ is required.
> **Windows note:** Offline scaffolding requires PowerShell 7+ (`pwsh`), not Windows PowerShell 5.x (`powershell.exe`). Install from https://aka.ms/powershell.
### Git Credential Manager on Linux
If you're having issues with Git authentication on Linux, you can install Git Credential Manager:

View File

@@ -8,7 +8,7 @@
| What to Upgrade | Command | When to Use |
|----------------|---------|-------------|
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git` | Get latest CLI features without touching project files |
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | Get latest CLI features without touching project files |
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
@@ -20,16 +20,18 @@ The CLI tool (`specify`) is separate from your project files. Upgrade it to get
### If you installed with `uv tool install`
Upgrade to a specific release (check [Releases](https://github.com/github/spec-kit/releases) for the latest tag):
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z
```
### If you use one-shot `uvx` commands
No upgrade needed—`uvx` always fetches the latest version. Just run your commands as normal:
Specify the desired release tag:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai copilot
```
### Verify the upgrade