Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ jobs:
chmod +x .github/workflows/scripts/create-release-packages.sh
.github/workflows/scripts/create-release-packages.sh ${{ steps.version.outputs.tag }}

- name: Build Python wheel
if: steps.check_release.outputs.exists == 'false'
run: |
pip install build
python -m build --wheel --outdir .genreleases/

- name: Generate release notes
if: steps.check_release.outputs.exists == 'false'
id: release_notes
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/scripts/create-github-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ VERSION="$1"
VERSION_NO_V=${VERSION#v}

gh release create "$VERSION" \
.genreleases/specify_cli-"$VERSION_NO_V"-py3-none-any.whl \
.genreleases/spec-kit-template-copilot-sh-"$VERSION".zip \
.genreleases/spec-kit-template-copilot-ps-"$VERSION".zip \
.genreleases/spec-kit-template-claude-sh-"$VERSION".zip \
Expand Down
30 changes: 16 additions & 14 deletions .github/workflows/scripts/create-release-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ fi
echo "Building release packages for $NEW_VERSION"

# Create and use .genreleases directory for all build artifacts
GENRELEASES_DIR=".genreleases"
# Override via GENRELEASES_DIR env var (e.g. for tests writing to a temp dir)
GENRELEASES_DIR="${GENRELEASES_DIR:-.genreleases}"
mkdir -p "$GENRELEASES_DIR"
rm -rf "$GENRELEASES_DIR"/* || true

Expand Down Expand Up @@ -218,7 +219,7 @@ build_variant() {
esac
fi

[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" | while IFS= read -r f; do d="$SPEC_DIR/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done; echo "Copied templates -> .specify/templates"; }

case $agent in
claude)
Expand Down Expand Up @@ -303,34 +304,35 @@ build_variant() {
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic)
ALL_SCRIPTS=(sh ps)

norm_list() {
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
}

validate_subset() {
local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
local type=$1; shift
local allowed_str="$1"; shift
local invalid=0
for it in "${items[@]}"; do
for it in "$@"; do
local found=0
for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done
for a in $allowed_str; do
if [[ "$it" == "$a" ]]; then found=1; break; fi
done
if [[ $found -eq 0 ]]; then
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
echo "Error: unknown $type '$it' (allowed: $allowed_str)" >&2
invalid=1
fi
done
return $invalid
}

read_list() { tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i);out=1}}}END{printf("\n")}'; }

if [[ -n ${AGENTS:-} ]]; then
mapfile -t AGENT_LIST < <(printf '%s' "$AGENTS" | norm_list)
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
read -ra AGENT_LIST <<< "$(printf '%s' "$AGENTS" | read_list)"
validate_subset agent "${ALL_AGENTS[*]}" "${AGENT_LIST[@]}" || exit 1
else
AGENT_LIST=("${ALL_AGENTS[@]}")
fi

if [[ -n ${SCRIPTS:-} ]]; then
mapfile -t SCRIPT_LIST < <(printf '%s' "$SCRIPTS" | norm_list)
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
read -ra SCRIPT_LIST <<< "$(printf '%s' "$SCRIPTS" | read_list)"
validate_subset script "${ALL_SCRIPTS[*]}" "${SCRIPT_LIST[@]}" || exit 1
else
SCRIPT_LIST=("${ALL_SCRIPTS[@]}")
fi
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- feat(cli): embed core templates/commands/scripts in wheel for air-gapped deployment; `specify init --offline` uses bundled assets without network access (#1711)
- feat(cli): add `--offline` flag to `specify init` to scaffold from bundled assets instead of downloading from GitHub (for air-gapped/enterprise environments)
- feat(cli): embed release scripts (bash + PowerShell) in wheel and invoke at runtime for guaranteed parity with GitHub release ZIPs
- feat(release): build and publish `specify_cli-*.whl` Python wheel as a release asset for enterprise/offline installation (#1752)
- feat(presets): Pluggable preset system with preset catalog and template resolver
- Preset manifest (`preset.yml`) with validation for artifact, command, and script types
- `PresetManifest`, `PresetRegistry`, `PresetManager`, `PresetCatalog`, `PresetResolver` classes in `src/specify_cli/presets.py`
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai c
- Better tool management with `uv tool list`, `uv tool upgrade`, `uv tool uninstall`
- Cleaner shell configuration

#### Option 3: Enterprise / Air-Gapped Installation

If your environment blocks PyPI access, download the pre-built `specify_cli-*.whl` wheel from the [releases page](https://github.com/github/spec-kit/releases/latest) and install it directly:

```bash
pip install specify_cli-*.whl
specify init my-project --ai claude --offline # runs without contacting api.github.com
```

The wheel bundles all templates, commands, and scripts, so `specify init` can run without any network access after install when you pass `--offline`. See the [Enterprise / Air-Gapped Installation](./docs/installation.md#enterprise--air-gapped-installation) section for fully offline (no-PyPI) instructions.

### 2. Establish project principles

Launch your AI assistant in the project directory. The `/speckit.*` commands are available in the assistant.
Expand Down
45 changes: 45 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,51 @@ 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 install Specify using the pre-built wheel from the GitHub releases page.

**Step 1: Download the wheel**

Go to the [Spec Kit releases page](https://github.com/github/spec-kit/releases/latest) and download the `specify_cli-*.whl` file.

**Step 2: Install the wheel**

```bash
pip install specify_cli-*.whl
```

**Step 3: Initialize a project (no network required)**

```bash
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 — no connection to `api.github.com` needed.

**If you also need runtime dependencies offline** (fully air-gapped machines with no access to any PyPI), use a connected machine with the same OS and Python version to download them first:

```bash
# On a connected machine (same OS and Python version as the target):
pip download -d vendor specify_cli-*.whl

# Transfer the wheel and vendor/ directory to the target machine

# On the target machine:
pip install --no-index --find-links=./vendor specify_cli-*.whl
```

> **Note:** Python 3.11+ is required. The wheel is a pure-Python artifact, so it works on any platform without recompilation.

**Using bundled assets (offline / air-gapped):**

If you want to scaffold from the templates bundled inside the specify-cli
package instead of downloading from GitHub, use:

```bash
specify init my-project --ai claude --offline
```

### Git Credential Manager on Linux

If you're having issues with Git authentication on Linux, you can install Git Credential Manager:
Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"]

[tool.hatch.build.targets.wheel.force-include]
# Bundle core assets so `specify init` works without network access (air-gapped / enterprise)
# Page templates (exclude commands/ — bundled separately below to avoid duplication)
"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md"
"templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md"
"templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-template.md"
"templates/plan-template.md" = "specify_cli/core_pack/templates/plan-template.md"
"templates/spec-template.md" = "specify_cli/core_pack/templates/spec-template.md"
"templates/tasks-template.md" = "specify_cli/core_pack/templates/tasks-template.md"
"templates/vscode-settings.json" = "specify_cli/core_pack/templates/vscode-settings.json"
# Command templates
"templates/commands" = "specify_cli/core_pack/commands"
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
".github/workflows/scripts/create-release-packages.sh" = "specify_cli/core_pack/release_scripts/create-release-packages.sh"
".github/workflows/scripts/create-release-packages.ps1" = "specify_cli/core_pack/release_scripts/create-release-packages.ps1"
Comment on lines +29 to +44

Comment on lines +29 to +45
[project.optional-dependencies]
test = [
"pytest>=7.0",
Expand Down
Loading
Loading