feat(init): support extension install during initialization#2180
feat(init): support extension install during initialization#2180officialasishkumar wants to merge 1 commit intogithub:mainfrom
Conversation
Add repeatable --extension support to specify init for bundled extension IDs, local extension directories, and archive URLs. Persist requested extensions in init options and install them before the default git-extension auto-install so explicit git opt-in does not double-install. Warn when legacy --ai and --no-git flags are used, and show a visible notice when the git extension is still auto-enabled by default ahead of the v1.0.0 opt-in change. Update README init option docs and add CLI regression coverage for init-time extension installation and deprecation messaging.
There was a problem hiding this comment.
Pull request overview
Adds support for installing extensions during specify init, including a transition path away from legacy init flags and toward explicit extension opt-in.
Changes:
- Introduces repeatable
specify init --extension <value>for bundled IDs, local extension directories, and archive URLs, and persists selected extensions in.specify/init-options.json. - Ensures init-time extension installs occur before the default git-extension auto-install, and adds user-visible notices/deprecation warnings (
--ai,--no-git). - Expands CLI integration tests and updates README init documentation/examples.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/specify_cli/__init__.py |
Adds init-time extension installation flow (name/path/URL), persistence of init options, and deprecation/notice messaging. |
tests/integrations/test_cli.py |
Adds integration tests covering local extension install, repeatable --extension, explicit --extension git with --no-git, and new warnings/notices. |
README.md |
Updates init option docs to include --extension and deprecations; adjusts headings and examples. |
Comments suppressed due to low confidence (1)
README.md:424
- The examples section mixes the new recommended
--integrationflag with several--aiexamples (e.g. qodercli/windsurf/kiro-cli/etc.), even though--aiis now documented as deprecated above. To avoid encouraging deprecated usage, update these remaining examples to use--integration <key>(or explicitly label--aiexamples as legacy/deprecated).
# Initialize with Cursor support
specify init my-project --integration cursor-agent
# Initialize with Qoder support
specify init my-project --ai qodercli
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if member.issym() or member.islnk(): | ||
| raise ValidationError( | ||
| f"Unsafe link in TAR archive: {member.name}" |
There was a problem hiding this comment.
TAR extraction validation currently only rejects symlinks/hardlinks, but still allows other non-regular member types (e.g., character/block devices, FIFOs). That can be a security risk if a crafted archive is installed (especially on systems running with elevated privileges). Consider explicitly allowing only directories and regular files (and/or using Python’s tarfile extraction filter where available), rejecting all other member types before calling extractall().
| if member.issym() or member.islnk(): | |
| raise ValidationError( | |
| f"Unsafe link in TAR archive: {member.name}" | |
| if not (member.isdir() or member.isreg()): | |
| raise ValidationError( | |
| f"Unsupported member type in TAR archive: {member.name}" |
| try: | ||
| with urllib.request.urlopen(source_url, timeout=60) as response: | ||
| archive_path.write_bytes(response.read()) |
There was a problem hiding this comment.
Downloading the extension archive uses response.read() and writes the entire payload at once. This can lead to unbounded memory usage (and potentially hangs) if the URL serves a very large file. Prefer streaming the response to disk in chunks and enforcing a reasonable maximum size (and optionally honoring Content-Length when present).
| try: | |
| with urllib.request.urlopen(source_url, timeout=60) as response: | |
| archive_path.write_bytes(response.read()) | |
| max_archive_size = 100 * 1024 * 1024 | |
| chunk_size = 1024 * 1024 | |
| try: | |
| with urllib.request.urlopen(source_url, timeout=60) as response: | |
| content_length = response.headers.get("Content-Length") | |
| if content_length is not None: | |
| try: | |
| expected_size = int(content_length) | |
| except ValueError: | |
| expected_size = None | |
| else: | |
| if expected_size > max_archive_size: | |
| raise ExtensionError( | |
| f"Extension archive from {source_url} exceeds the " | |
| f"maximum allowed size of {max_archive_size} bytes" | |
| ) | |
| total_downloaded = 0 | |
| with archive_path.open("wb") as archive_file: | |
| while True: | |
| chunk = response.read(chunk_size) | |
| if not chunk: | |
| break | |
| total_downloaded += len(chunk) | |
| if total_downloaded > max_archive_size: | |
| raise ExtensionError( | |
| f"Extension archive from {source_url} exceeds the " | |
| f"maximum allowed size of {max_archive_size} bytes" | |
| ) | |
| archive_file.write(chunk) |
| @@ -333,7 +334,7 @@ Community projects that extend, visualize, or build on Spec Kit: | |||
|
|
|||
| After running `specify init`, your AI coding agent will have access to these slash commands for structured development. If you pass `--ai <agent> --ai-skills`, Spec Kit installs agent skills instead of slash-command prompt files; `--ai-skills` requires `--ai`. | |||
There was a problem hiding this comment.
This paragraph still documents the legacy --ai ... --ai-skills behavior as the primary path, but the PR deprecates --ai and --ai-skills in favor of --integration (with skills selection handled by the integration). The docs should be updated here to describe --integration as the recommended option and clarify that --ai* flags are deprecated.
| After running `specify init`, your AI coding agent will have access to these slash commands for structured development. If you pass `--ai <agent> --ai-skills`, Spec Kit installs agent skills instead of slash-command prompt files; `--ai-skills` requires `--ai`. | |
| After running `specify init`, your AI coding agent will have access to these slash commands for structured development. The recommended way to configure agent behavior is with `--integration <name>`, which determines whether Spec Kit installs slash-command prompt files or integration-specific skills. The legacy `--ai` and `--ai-skills` flags are deprecated. |
Description
Closes #2165
Closes #2166
Closes #2167
Closes #2169
Adds the init-time extension transition path:
specify init --extension <value>support for bundled extension IDs, local extension directories, and extension archive URLs..specify/init-options.jsonand installs them before the default git-extension auto-install, so explicit--extension gitdoes not double-install.--aiand--no-gitwhile preserving current behavior.--extension gitorspecify extension add git.Testing
.venv/bin/specify --help(uvis not installed in this environment).venv/bin/python -m pytest -q--extension git, and default git auto-install)Additional checks:
.venv/bin/ruff check src/npx --yes markdownlint-cli2 README.mdgit diff --checkAI Disclosure
I used AI assistance for codebase analysis, implementation support, test updates, and PR drafting. I reviewed the resulting changes and validated them locally with the commands listed above.