Skip to content

Conversation

@alberti42
Copy link
Contributor

Feature: Add Native One-Time Installation for Local Plugins

Motivation and Problem

Currently, zinit treats local plugins (specified by an absolute path) differently from remote plugins. They lack a one-time "installation" phase, which means they are simply sourced "in-place" on every shell start.

This prevents the automatic, declarative setup of features like completions, which normally run during the atclone phase for remote plugins. As a result, users are forced to write complex atload hooks with manual flag files to simulate this one-time setup, making the configuration for local plugins unnecessarily verbose and complex.

For example, installing completions for a local plugin requires a workaround like this:

# The OLD way: a complex, manual workaround
zinit ice atload"
  # Define a flag file in the current (plugin) directory.
  local flag_file='./.installed'

  # If the flag doesn't exist, run the one-time install.
  if [[ ! -f \$flag_file ]]; then
    # We are already inside the plugin's directory, so we use '.'
    zinit creinstall -q .
    
    # Create the flag file to prevent this from running again.
    touch \$flag_file
  fi
"
zinit light /path/to/my/local/plugin

This is not intuitive and goes against zinit's goal of clean, declarative syntax.

Solution and Implementation

This pull request introduces a more integrated approach by treating a local plugin as a "pre-cloned" repository. It bridges the gap between remote and local plugin handling.

The implementation involves the following changes:

  1. Detection in .zinit-load() (zinit.zsh): The main load function is patched to detect when a new, unmanaged local plugin is being loaded. It then calls the main installation function, passing a special local type. A filter is included to prevent this logic from incorrectly triggering on zinit's own internal use of local paths (e.g., loading from $ZINIT[BIN_DIR]).

  2. Symlinking in .zinit-setup-plugin-dir() (zinit-install.zsh): The main installation function is patched with a new branch to handle the local type. Instead of cloning or downloading, it creates a symlink from the user's local plugin directory into zinit's managed $ZINIT[PLUGINS_DIR].

  3. Unified Post-Install Hooks: After the symlink is created, the function proceeds to run all the standard post-installation logic that is normally reserved for remote plugins. This includes the automatic scan for completions triggered by the completions ice, running make hooks, etc.

  4. Improved Logging: The installation message has been updated to say Installing local plugin... instead of the confusing Downloading %//... when this new logic is triggered.

Benefits and User Experience

This change significantly improves the user experience for managing local plugins.

  • Declarative Syntax: Users can now use the simple, declarative completions ice for local plugins, just as they would for remote ones.
  • Consistency: Local plugins now behave almost identically to remote plugins during their initial setup.
  • Simplified Configuration: The need for manual atload workarounds is eliminated.
  • Full Feature Parity: Other installation-time hooks (make, compile, etc.) will now also work as expected for local plugins on their first run.

With this patch, the complex workaround from before becomes a simple, clean, and idiomatic zinit command:

# The NEW way: clean and declarative
zinit ice id-as:'local/my-local-plugin' completions
zinit light /path/to/my/local/plugin

A Note on Update Tracking

A key design consideration is update tracking. Since zinit does not own the source of the local plugin (it only links to it), it cannot manage its updates via zinit update. The responsibility for keeping the local plugin's source directory up-to-date remains with the user. This is an expected and reasonable trade-off for the flexibility of managing plugins from one's own dotfiles or local projects.

AI Assistance Disclaimer

This pull request, including the implementation of the patch and the descriptive text, was developed with the assistance of an AI model (Google's Gemini 2.5). The process was guided by a human developer who provided the specific logic, identified the target locations for surgical code changes, and directed the content and structure of this note. The final code and functionality have been thoroughly tested and validated by humans on macOS (Apple Silicon) and Linux (Ubuntu 22.04) to ensure correctness and stability.

@alberti42
Copy link
Contributor Author

Hi all,

Thanks for taking a look at this. Based on further testing and refinement, I've pushed two new commits to this PR that significantly improve the initial proposal.

Here’s a summary of the updates:

  1. Automatic id-as for Local Plugins: The patch now provides a sensible default id-as for local plugins. If a user loads a plugin from /path/to/my-plugin without specifying an id-as, it will automatically be identified as local/my-plugin. This prevents failures and makes the syntax for loading local plugins much cleaner and more intuitive.

  2. Unified Symlink Behavior with the link Ice: Instead of introducing a new symlink ice, this version now uses the existing link ice for consistency with how local snippets are handled. The default behavior for a local plugin is now to create a self-contained copy (cp -R). If the user wants to symlink it for live development, they can simply add the ice 'link' modifier.

These changes are in two separate, logical commits.

For a complete overview and for any new reviewers, I have updated the main pull request description to reflect the full scope of the feature. I'm pasting the final, consolidated version below for convenience.


Feature: Add Native One-Time Installation for Local Plugins

Motivation and Problem

Currently, zinit treats local plugins (specified by an absolute path) differently from remote plugins. They lack a one-time "installation" phase, meaning they are simply sourced "in-place" on every shell start.

This prevents the automatic, declarative setup of features like completions, which normally run during the atclone phase for remote plugins. As a result, users are forced to write complex atload hooks with manual flag files to simulate this one-time setup, making the configuration for local plugins unnecessarily verbose and complex.

Solution and Implementation

This pull request introduces a more integrated approach by treating a local plugin as a "pre-cloned" repository. This is achieved by patching zinit's core loading and installation functions:

  1. Detection in .zinit-load(): The main load function now detects when a new, unmanaged local plugin is being loaded for the first time. It then calls the main installation function, which was previously reserved for remote plugins. A filter is included to prevent this logic from incorrectly triggering on zinit's own internal use of local paths.

  2. Copy/Link in .zinit-setup-plugin-dir(): The main installation function now has a new branch for local plugins. By default, it copies the plugin's directory into zinit's managed $ZINIT[PLUGINS_DIR], creating a self-contained snapshot.

  3. Unified Post-Install Hooks: After the copy/link operation, the function proceeds to run all standard post-installation logic. This allows local plugins to use declarative ice-mods like completions, make, and compile on their first run, just like remote plugins.

  4. Improved Logging: The installation message has been updated to be context-aware, printing Installing local plugin... or Symlinking local plugin... instead of the confusing Downloading %//....

Usability Improvements

This patch also includes two significant usability enhancements:

Automatic id-as for Local Plugins

Loading a local plugin without an explicit id-as no longer fails or creates a messy identifier. If a user loads a plugin from /path/to/my-plugin, zinit will now automatically assign it a clean, conventional ID of local/my-plugin. This allows for a much simpler syntax:

# No longer requires a manual id-as
zinit ice completions
zinit light /path/to/my-local-plugin
Unified Behavior with link Ice

To provide flexibility, the installation behavior can be controlled with the existing link ice-mod, creating consistent behavior between local plugins and local snippets:

  • Default Behavior (cp -R): Creates a self-contained copy of the plugin. This is safe and robust.
  • With ice 'link' (ln -s): Creates a symlink to the original directory. This is ideal for local development, as changes to the source are immediately reflected in the shell.

A Note on Update Tracking

Since zinit does not own the source of the local plugin, it cannot manage its updates via zinit update. The responsibility for keeping the local plugin's source directory up-to-date remains with the user. This is an expected and reasonable trade-off.

AI Assistance Disclaimer

This pull request, including the implementation of the patch and the descriptive text, was developed with the assistance of an AI model (Google's Gemini). The process was guided by a human developer who provided the specific logic, identified the target locations for surgical code changes, and directed the content and structure of this note. The final code and functionality have been thoroughly tested and validated by humans on macOS (Apple Silicon) and Linux (Ubuntu 22.04) to ensure correctness and stability.

@alberti42
Copy link
Contributor Author

alberti42 commented Dec 30, 2025

Hi all,

I've pushed one more commit a66b381 to this PR to address a cross-platform compatibility bug I discovered during testing.

Problem

When running zinit update on a local plugin or snippet that was installed with the link ice, the process would fail on macOS. The script attempts to use GNU-specific flags (--version and --relative-to) for the realpath command to create a relative symlink.

However, macOS ships with a BSD version of realpath that does not support these flags, causing the update process to print an error: realpath: illegal option -- -.

Solution

This new commit fixes the issue by adding a robust compatibility check. Before attempting to use the GNU realpath features, the script now safely checks if the realpath --version command succeeds.

  • On Linux (with GNU realpath), the check passes, and the script proceeds to create an efficient relative symlink as before.
  • On macOS (with BSD realpath), the check fails silently. The script then gracefully falls back to using an absolute path for the symlink, which works perfectly on all systems.

This ensures the zinit update command for linked local resources is now silent, correct, and fully functional on macOS without regressing the behavior on Linux systems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant