* fix(opencode): use commands/ directory (plural) to match OpenCode docs
OpenCode documentation (https://opencode.ai/docs/commands/) uses
.opencode/commands/ (plural) as the canonical command directory.
The OpenCode runtime supports both .opencode/command/ and
.opencode/commands/ via a {command,commands} glob, but the
singular form was the original convention and is now outdated.
Update the OpenCode integration to write to .opencode/commands/
instead of .opencode/command/, aligning with the documented
standard and the OpenSpec fix (Fission-AI/OpenSpec#748).
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
Assisted-by: OpenCode (claude-opus-4-6)
* feat(registrar): add legacy_dir fallback for backward-compatible directory migration
Add _resolve_agent_dir() to CommandRegistrar that checks a
legacy_dir fallback when the canonical directory does not exist.
When legacy_dir is found, a deprecation warning directs users to
run "specify integration upgrade" to migrate.
The OpenCode integration declares legacy_dir: ".opencode/command"
so that extension and preset registration, as well as command
cleanup, continue working for projects that have not yet migrated
to .opencode/commands/.
The legacy_dir mechanism is opt-in: integrations that do not
declare it get no fallback and no behavioral change.
Add end-to-end test verifying that "specify integration upgrade
opencode" migrates commands from legacy .opencode/command/ to
canonical .opencode/commands/ and removes stale files.
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
Assisted-by: OpenCode (claude-opus-4-6)
* fix(registrar): address PR review feedback on legacy_dir handling
- Fix deprecation warning formatting: quote paths and remove trailing
'/.' that produced confusing '.opencode/commands/.' output
- Eliminate duplicate warnings: pass pre-resolved directory to
register_commands() via _resolved_dir parameter so
_resolve_agent_dir() is only called once per agent
- Fix unregister_commands() to clean both canonical and legacy dirs
when both exist, preventing orphaned command files after upgrade
- Add test_unregister_cleans_legacy_when_both_dirs_exist regression
test and tighten warning count assertion to exactly 1
Assisted-by: OpenCode (claude-opus-4-6)
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
---------
Signed-off-by: Marcus Burghardt <maburgha@redhat.com>
* fix(integration): refresh shared infra on integration switch
* fix(integration): address Copilot review on switch shared-infra refresh
- Clarify install_shared_infra docstring: force overwrites regular files
but always preserves symlinks (safe-destination check refuses to follow).
- Print refresh_hint only for preserved_user_files; skipped_files keeps
the generic remediation. Avoids misleading guidance when files were
merely skipped (not detected as customized).
- Catch ValueError from the safe-destination check and bucket the path
under a new symlinked_files warning instead of aborting the switch.
- Restore templates/constitution-template.md to upstream (drop accidental
leading blank lines).
* fix(integration): narrow symlink bucketing to dedicated exception
Address Copilot feedback on shared_infra.py:305 — _safe_dest_or_bucket
caught any ValueError as 'symlinked', which masked genuine safety errors
(path escape, parent-not-a-directory).
- Introduce SymlinkedSharedPathError(ValueError) raised only by the
symlink-specific branches in _ensure_safe_shared_*().
- _safe_dest_or_bucket() now catches only SymlinkedSharedPathError;
other ValueErrors propagate so the operation aborts with the real
cause instead of being silently bucketed.
- Wrap top-level dest_scripts/dest_variant/dest_templates mkdir calls
in the same bucket helper so a symlinked .specify/scripts or
.specify/templates is preserved with a warning rather than aborting
the switch (matches the documented 'preserve customizations' behavior).
- Update tests to expect the new bucket+warn behavior for leaf-level
symlinked destinations.
* fix(integration): tailor shared-infra warnings and rename preflight test
Address Copilot review on PR #2375:
- skipped_files hint now uses refresh_hint when refresh_managed=True
so integration switch suggests --refresh-shared-infra instead of the
generic init/upgrade flags.
- symlinked-files warning header says "path(s)" rather than "file(s)"
since symlinked directories (e.g. .specify/scripts/bash) are also
bucketed there.
- Rename test_shared_infra_install_preflights_before_writing to
test_shared_infra_install_buckets_unsafe_destinations_and_continues
to match the new bucket-and-continue semantics.
* test: rename symlink bucketing tests to reflect bucket-and-continue behavior
The two file-bucketing tests at line 300/320 were named *_refuses_*, but
the new behavior buckets symlinked file destinations with a warning while
safe destinations in the same install still complete. Rename to
*_buckets_* and update docstrings to match.
The remaining *_refuses_* tests (line 342/362/381) genuinely raise on
symlinked dirs/manifests and keep their names.
---------
Co-authored-by: Quratulain-bilal <quratulain.bilal@users.noreply.github.com>
* fix: migrate extension commands on integration switch
When switching integrations (e.g. kimi → opencode), extension commands
were not re-registered for the new agent, leaving the new agent without
extension support and orphaning files in the old agent's directory.
Changes:
- Add ExtensionManager.unregister_agent_artifacts() to clean up old
agent extension files and registry entries during switch
- Add ExtensionManager.register_enabled_extensions_for_agent() to
re-register all enabled extensions for the new agent
- Wire both into integration_switch() after uninstall/install phases
- Handle skills mode (Copilot --skills) correctly
- Add tests for kimi→opencode→claude migration, Copilot skills mode,
and disabled extension handling
Fixes extension commands not appearing after integration switch.
* Update src/specify_cli/extensions.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Initial plan
* Add specify integration subcommand (list, install, uninstall, switch)
Implements the `specify integration` subcommand group for managing
integrations in existing projects after initial setup:
- `specify integration list` — shows available integrations and installed status
- `specify integration install <key>` — installs an integration into existing project
- `specify integration uninstall [key]` — hash-safe removal preserving modified files
- `specify integration switch <target>` — uninstalls current, installs target
Follows the established `specify <noun> <verb>` CLI pattern used by
extensions and presets. Shared infrastructure (scripts, templates) is
preserved during uninstall and switch operations.
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/1cca6c84-3e12-465d-88b8-a646d3504f63
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Address review feedback: extract helper, fix return type annotation
- Extract _update_init_options_for_integration() to deduplicate init-options
update logic between install and switch commands
- Fix _parse_integration_options return type to dict[str, Any] | None
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/1cca6c84-3e12-465d-88b8-a646d3504f63
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Potential fix for pull request finding 'Unused import'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Address review feedback: validate script type, handle --flag=value, fix metadata cleanup
- Add _normalize_script_type() to validate script type against SCRIPT_TYPE_CHOICES
- Handle --name=value syntax in _parse_integration_options()
- Clear init-options.json keys in no-manifest uninstall early-return path
- Clear stale metadata between switch teardown and install phases
- Add 5 tests covering the new edge cases
* Block --force with different integration, persist script type in init-options
- --force on install now rejects overwriting a different integration; users must
use 'specify integration switch' instead
- _update_init_options_for_integration() now accepts and persists script_type
- Fix misleading test docstring for switch metadata test
- Add test_force_blocked_with_different_integration
* Remove --force from integration install, ensure shared infra on install/switch
- Remove --force parameter entirely from integration install; users must
uninstall before reinstalling to prevent orphaned files
- Auto-install shared infrastructure (.specify/scripts/, .specify/templates/)
when missing during install or switch
- Add test for shared infra creation on bare project install
* Remove redundant installed_key != key check
The == key case already returns above, so the != key guard is always true
at this point. Simplify to just 'if installed_key:'.
* Run shared infra unconditionally, defer metadata removal in switch
- Call _install_shared_infra() unconditionally on install and switch since it
merges without overwriting existing files
- Remove premature metadata cleanup between switch phases; metadata is now
only updated after successful Phase 2 install
* Add install rollback, graceful manifest errors, clear switch metadata
- Attempt teardown rollback on install/switch failure to avoid orphaned files
- Catch ValueError/FileNotFoundError on IntegrationManifest.load() in uninstall
with user-friendly recovery guidance
- Clear metadata immediately after switch teardown so failed Phase 2 doesn't
leave stale references to the removed integration
* Log rollback failures instead of silently suppressing them
* Handle corrupt manifest in switch, distinguish unknown vs missing manifest
- Wrap IntegrationManifest.load() in switch with ValueError/FileNotFoundError
handling, matching the pattern used in uninstall
- Split else branch to report 'unknown integration' vs 'no manifest' separately
* Clean up metadata on rollback, broaden init-options match in uninstall
- Remove integration.json in install/switch rollback paths so failed installs
don't leave stale metadata
- Match on both 'integration' and 'ai' keys when clearing init-options.json
during uninstall to handle partially-written metadata
* Fix recovery guidance for unreadable manifests, fix type annotations
- Recovery instructions now guide users through delete manifest → uninstall →
reinstall workflow that actually works
- Type annotations for optional CLI parameters changed from str to str | None
* Allow manifest-only uninstall for unknown/removed integrations
- Uninstall no longer requires the integration to be in the registry; falls back
to manifest.uninstall() directly when get_integration() returns None
- Switch Phase 1 similarly uses manifest-only uninstall for unknown integrations
instead of skipping teardown, preventing orphaned files
* Fail fast on corrupt integration.json, validate integration options
- _read_integration_json() now exits with an actionable error when
integration.json exists but is corrupt/unreadable
- _parse_integration_options() rejects unknown options, validates flag usage,
and requires values for non-flag options
* Validate integration.json is a dict, fail fast on missing manifest in switch
- _read_integration_json() validates parsed JSON is a dict, not a list/string
- Switch fails fast with recovery guidance when manifest is missing instead
of silently skipping teardown and risking co-existing integration files
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>