Skip to content
Open
5 changes: 5 additions & 0 deletions docs/install/one-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ If you plan to use Spec Kit regularly, a persistent installation is recommended:
- Tool stays installed and available in PATH
- No re-download on every invocation
- Better tool management with `uv tool list`, `uv tool upgrade`, `uv tool uninstall`
- Required for [presets](../community/presets.md) that provide a **script** template: the generated
script resolves its composition by invoking `specify` at run time, so it needs the CLI (or an
importable `specify_cli`) to still be present after `init` finishes. The `uvx` flow above discards
its environment as soon as the command exits, so a script-providing preset installed this way will
fail the first time its script runs. Use a persistent installation if you plan to use such a preset.

See the main [Installation Guide](../installation.md) for persistent installation instructions.
6 changes: 6 additions & 0 deletions src/specify_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Allow ``python -m specify_cli`` to run the CLI."""

from specify_cli import main

if __name__ == "__main__":
main()
20 changes: 20 additions & 0 deletions src/specify_cli/command_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,26 @@ def init(

ensure_executable_scripts(project_path, tracker=tracker)

# install_shared_infra above may have just overwritten
# .specify/scripts/bash/<name>.sh with the bundled core,
# clobbering any generated continuation dispatcher for a
# script an already-enabled preset provides (e.g. on
# `specify init --force` against an existing project).
# Restore those dispatchers before any *new* --preset
# install below runs its own reconciliation.
try:
from .presets import PresetManager as _ExistingPresetManager

_ExistingPresetManager(project_path).reconcile_all_script_chains()
Comment on lines +831 to +834
except Exception as exc:
_print_cli_warning(
"reconcile script presets after",
"init",
str(project_path),
exc,
continuing="Run 'specify preset script-chain <name>' to diagnose.",
)

if preset:
try:
from .presets import PresetCatalog, PresetError, PresetManager
Expand Down
22 changes: 22 additions & 0 deletions src/specify_cli/integrations/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,28 @@ def _set_default_integration(
f"Failed to refresh shared infrastructure for '{key}': {exc}"
) from exc

# _install_shared_infra above may have just overwritten
# .specify/scripts/bash/<name>.sh with the bundled core when
# refresh_templates_force is True, clobbering any generated
# continuation dispatcher for a script an already-enabled preset
# provides. Restore those dispatchers now, mirroring the same call
# in command_init.py and command_upgrade.py.
if refresh_templates_force:
try:
Comment on lines +348 to +355
from ..presets import PresetManager as _ExistingPresetManager

_ExistingPresetManager(project_root).reconcile_all_script_chains()
except Exception as exc:
from .. import _print_cli_warning

_print_cli_warning(
"reconcile script presets after",
"integration use/switch",
str(project_root),
exc,
continuing="Run 'specify preset script-chain <name>' to diagnose.",
)

_write_integration_json(project_root, key, installed_keys, settings)
_update_init_options_for_integration(
project_root, integration, script_type=resolved_script, parsed_options=parsed_options
Expand Down
22 changes: 22 additions & 0 deletions src/specify_cli/integrations/command_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,28 @@ def integration_switch(
from .. import ensure_executable_scripts
ensure_executable_scripts(project_root)

# The forced refresh above may have just overwritten
# .specify/scripts/bash/<name>.sh with the bundled core, clobbering any
# generated continuation dispatcher for a script an already-enabled
# preset provides. Restore those dispatchers now: this call happens
# before _set_default_integration below, whose own reconciliation
# (_helpers.py) is gated on refresh_templates_force and would not fire
# here since this phase's force comes from --refresh-shared-infra, not
# from that helper's own parameter.
if refresh_shared_infra:
try:
from ..presets import PresetManager as _ExistingPresetManager

_ExistingPresetManager(project_root).reconcile_all_script_chains()
except Exception as exc:
_print_cli_warning(
"reconcile script presets after",
"integration switch --refresh-shared-infra",
str(project_root),
exc,
continuing="Run 'specify preset script-chain <name>' to diagnose.",
)

# Phase 2: Install target integration
console.print(f"Installing integration: [cyan]{target}[/cyan]")
manifest = IntegrationManifest(
Expand Down
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/command_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,27 @@ def integration_upgrade(
from .. import ensure_executable_scripts
ensure_executable_scripts(project_root)

# _install_shared_infra_or_exit above may have just overwritten
# .specify/scripts/bash/<name>.sh with the bundled core when force=True,
# clobbering any generated continuation dispatcher for a script an
# already-enabled preset provides. Restore those dispatchers now,
# mirroring the same call in command_init.py.
if force:
try:
from ..presets import PresetManager as _ExistingPresetManager

_ExistingPresetManager(project_root).reconcile_all_script_chains()
Comment on lines +224 to +228
except Exception as exc:
from .. import _print_cli_warning

_print_cli_warning(
"reconcile script presets after",
"integration upgrade",
str(project_root),
exc,
continuing="Run 'specify preset script-chain <name>' to diagnose.",
)

# Phase 1: Install new files (overwrites existing; old-only files remain)
console.print(f"Upgrading integration: [cyan]{key}[/cyan]")
new_manifest = IntegrationManifest(key, project_root, version=_get_speckit_version())
Expand Down
Loading