bundle

package
v1.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package bundle implements the canonical nSelf paid plugin bundle catalog plus install/remove orchestration with license validation, version pinning, and atomic rollback on failure (S2.T03/T04/T22).

Bundle membership is authoritative here — the source of truth that mirrors .claude/docs/sport/F06-BUNDLE-INVENTORY.md. The cmd layer must consume this package; do NOT duplicate the bundle map elsewhere.

installer.go — S2.T03: `nself bundle install <bundle>` orchestrator.

Atomicity: pre-flight ALL licenses, then install each constituent plugin sequentially via plugin.Install. On any per-plugin failure we roll back by removing every plugin we successfully installed in THIS invocation. Plugins that were already on disk before the call are left untouched.

--dry-run prints the planned actions and exits without filesystem changes. --force re-installs even if the same version is already installed

(repair/upgrade path). License is ALWAYS validated regardless of
--force; --force NEVER bypasses the license check.

--strict fails if any plugin in the bundle is missing from the registry

(default: skip missing with a warning).

--channel selects stable/beta/canary for version resolution (T22).

remover.go — S2.T04: `nself bundle remove <bundle>` orchestrator.

Walks the bundle's plugin list and removes each one via plugin.Remove. By default plugin data volumes are dropped; --keep-data preserves them. --dry-run prints the planned actions without touching the filesystem.

Removal proceeds in REVERSE plugin order so that a plugin earlier in the list (often a dependency of later plugins, e.g. ai before claw) is removed last. plugin.Remove's reverse-dependency check is bypassed via force=true because the operator has explicitly asked to tear down the whole bundle.

resolver.go — S2.T22: cross-bundle plugin version MAX resolution.

When multiple bundles are installed simultaneously (e.g. `nself bundle install nself+` which installs all 6 paid bundles), the same plugin P may be required at different versions by different bundles. This file resolves those conflicts: MAX version wins across all bundles. Conflicts are informational — they are logged but never block the install.

Exit code 3 (downgrade): if an already-installed plugin version is HIGHER than all requested bundle versions, the caller skips that plugin to avoid a downgrade. This is surfaced as a Conflict with Resolved = "" to signal skip.

version_resolver.go — S2.T22: resolve bundle slugs + release channels into concrete plugin version pins. Reads plugins-pro/registry.json via the existing internal/plugin registry fetcher so the same fallback + cache chain (primary URL → GitHub raw → stale cache) applies.

Also provides cross-bundle MAX-resolution: when multiple bundles are installed and share plugins, the installed version is always the MAX (newest) of what each bundle requires — never a silent downgrade.

Index

Constants

This section is empty.

Variables

View Source
var DisplayOrder = []string{
	"nclaw", "nchat", "nfamily", "ntv", "clawde", "nsentry", "ntask", "nself-plus",
}

DisplayOrder is the canonical print order for bundle listings.

Functions

func Names

func Names() []string

Names returns every known bundle slug sorted alphabetically. Useful for error messages and shell completion.

func ResolveConflicts added in v1.1.1

func ResolveConflicts(requirements map[string][]string, strict bool) (map[string]string, error)

ResolveConflicts takes a map of plugin slug → list of version requirements from different bundles and returns the resolved map of plugin slug → max version. If strict is true, any plugin that has more than one distinct version requirement returns an error instead of resolving to the max.

A "conflict" in strict mode means two or more bundles require different versions of the same plugin. When strict is false (the default), conflicts are silently resolved by taking the highest (MAX) version.

func ResolveMaxVersion added in v1.1.1

func ResolveMaxVersion(pluginName string, versions []string) (string, error)

ResolveMaxVersion returns the maximum (newest) semver string from versions. All versions must be valid semver (with or without a leading "v" prefix). Returns an error if versions is empty or any entry is not valid semver. The returned string preserves the "v" prefix of the winning candidate.

Example:

ResolveMaxVersion("ai", []string{"1.0.0", "1.2.0", "1.1.0"}) → "1.2.0", nil

func ResolveVersion

func ResolveVersion(ctx context.Context, pluginName string, channel Channel) (string, error)

ResolveVersion returns the version pin for a single plugin under the given channel. Exposed for callers that need to pin one plugin (e.g. compat-check, install --version<latest-channel>).

func UnknownBundleError

func UnknownBundleError(slug string) error

UnknownBundleError formats a friendly error when a slug does not match a known bundle. Includes the sorted list of valid names as a hint.

Types

type Bundle

type Bundle struct {
	Name        string   // Display name (with ɳ glyph)
	Slug        string   // CLI-arg lowercase identifier
	Price       string   // Human-readable price string
	Description string   // Optional one-line description
	Plugins     []string // Plugin slugs in the bundle (nil for meta-bundle)
}

Bundle describes a canonical nSelf plugin bundle.

func All

func All() []Bundle

All returns every Bundle in canonical display order.

func Get

func Get(slug string) (Bundle, bool)

Get returns the Bundle for the given slug. The slug is case-insensitive and trimmed. Returns ok=false when the slug is unknown; callers can call Names() to render a useful error hint.

func (Bundle) IsInstallable

func (b Bundle) IsInstallable() bool

IsInstallable reports whether the bundle can be installed via `nself bundle install`. Meta-bundles (nself-plus) and free bundles (ntask) are not installable as a unit — operators install their constituent plugins directly via `nself plugin install`.

type Channel

type Channel string

Channel is a release-channel identifier (stable/beta/canary). Channels filter candidate registry entries by their PublishStatus before version selection:

stable → status in {"", "stable"}
beta   → status in {"", "stable", "beta"}
canary → all installable statuses (adds "experimental")

"deprecated" and "eol" entries are never resolved by this function regardless of channel — those install paths require explicit --allow-eol acknowledgment through plugin.CheckEOLBlock, not silent bundle install.

const (
	ChannelStable Channel = "stable"
	ChannelBeta   Channel = "beta"
	ChannelCanary Channel = "canary"
)

type Conflict added in v1.1.1

type Conflict struct {
	PluginName  string   // Plugin slug that has conflicting requirements
	Versions    []string // All requested versions (one per bundle that requires the plugin)
	Resolved    string   // MAX version selected; "" means skip (downgrade protection)
	BundleNames []string // Bundle names corresponding to each version entry
}

Conflict describes a version conflict for a single plugin across bundles. Conflicts are always resolved — they are informational, not errors.

func ResolveVersionConflicts added in v1.1.1

func ResolveVersionConflicts(bundles []Bundle) (map[string]string, []Conflict, error)

ResolveVersionConflicts takes a list of bundles and returns:

  • resolved: plugin slug → MAX version string (the version to install)
  • conflicts: informational list of plugins that had differing requirements
  • err: non-nil only for invalid semver strings in registry data

Algorithm:

  1. Collect all (plugin, version) requirements across all bundles.
  2. For each plugin with multiple requirements, select MAX using plugin.CompareVersions.
  3. Return the conflict list (informational — resolved, not errors).

The caller is responsible for downgrade detection against already-installed versions (see installer.go § buildInstalledVersionMap).

func ResolveVersionConflictsFromPins added in v1.1.1

func ResolveVersionConflictsFromPins(
	bundlePins map[string]VersionPins,
	strict bool,
	installedVersions map[string]string,
) (map[string]string, []Conflict, error)

ResolveVersionConflictsFromPins is the production entry point. It accepts per-bundle VersionPins (already fetched from the registry) and resolves cross-bundle conflicts by selecting the MAX version for each plugin.

Parameters:

  • bundlePins: map of bundle slug → VersionPins (plugin→version) for that bundle
  • strict: when true, emit a warning per conflict where MAX is HIGHER than what a particular bundle requested (that bundle tested against the lower version). Does NOT block the install.
  • installedVersions: map of lowercase plugin name → currently installed version; used for downgrade detection. Pass nil if no plugins are installed yet.

Returns:

  • resolved: plugin slug → version to install (MAX across all bundles)
  • conflicts: informational conflict list
  • err: non-nil only for invalid semver

type InstallOpts

type InstallOpts struct {
	DryRun  bool
	Force   bool
	Strict  bool
	Channel Channel

	// Out is the stream to print human-facing progress messages.
	// nil → os.Stderr.
	Out io.Writer

	// PluginDir overrides the default plugin install location. Empty → use
	// the same resolution as cmd/commands/plugin.resolvePluginDir.
	PluginDir string

	// Config overrides the loaded project config. Optional — when nil the
	// caller-loaded cfg is used by Install via cfg arg.
	Config *config.Config
	// contains filtered or unexported fields
}

InstallOpts captures the user-controllable knobs for bundle install.

type InstallResult

type InstallResult struct {
	Bundle        string
	Channel       Channel
	Planned       []string // plugins planned for install (post channel + strict filter)
	Skipped       []string // plugins skipped because missing from registry (non-strict)
	Installed     []string // plugins successfully installed in this call
	RolledBack    []string // plugins removed during rollback after a failure
	LicenseBypass bool     // reserved — always false; --force never bypasses license
	DryRun        bool
}

InstallResult captures the outcome of a bundle install for callers that need to surface details (e.g. CR/QA harness, audit logger).

func Install

func Install(ctx context.Context, bundleSlug string, opts InstallOpts) (*InstallResult, error)

Install runs the bundle install flow. The returned InstallResult is populated even on error so callers (and tests) can inspect partial state.

func InstallMultiple added in v1.1.1

func InstallMultiple(ctx context.Context, bundleSlugs []string, opts InstallOpts) ([]*InstallResult, error)

InstallMultiple installs several bundles simultaneously, resolving cross-bundle plugin version conflicts via MAX resolution (S2.T22). Plugins shared across bundles are installed at the MAX version required by any bundle.

Behavior:

  • Version conflicts across bundles → MAX version wins (informational warning).
  • --strict flag → warn per conflict where MAX is HIGHER than a bundle's pin.
  • Downgrade protection → if an installed plugin is already HIGHER than all requested versions, that plugin is skipped (exit code 3 scenario).
  • All other InstallOpts semantics (DryRun, Force, rollback) apply per-plugin.

The returned []*InstallResult has one entry per input bundle slug, in order. Conflict information is logged to opts.Out; callers may inspect the per-bundle results for full detail.

type RemoveOpts

type RemoveOpts struct {
	DryRun   bool
	KeepData bool

	// Out is the stream for human-facing progress; nil → os.Stderr.
	Out io.Writer

	// PluginDir overrides the default plugin location.
	PluginDir string

	// Config overrides the loaded project config.
	Config *config.Config
	// contains filtered or unexported fields
}

RemoveOpts captures the user-controllable knobs for bundle remove.

type RemoveResult

type RemoveResult struct {
	Bundle       string
	Planned      []string // plugins planned for removal (post not-installed filter)
	NotInstalled []string
	Removed      []string
	Failed       []string
	DryRun       bool
	KeepData     bool
}

RemoveResult captures the outcome of a bundle remove call.

func Remove

func Remove(ctx context.Context, bundleSlug string, opts RemoveOpts) (*RemoveResult, error)

Remove tears down every plugin in the bundle. Plugins not currently installed are reported under NotInstalled and skipped. Per-plugin removal failures are collected in Failed and surfaced as the returned error, but removal continues for the remaining plugins rather than aborting on first failure — partial cleanup is more useful than a hung-bundle state.

type VersionPins

type VersionPins map[string]string

VersionPins maps plugin slug → concrete semver version string for every installable plugin in a bundle, after channel filtering.

func ResolveBundleVersions

func ResolveBundleVersions(ctx context.Context, bundleSlug string, channel Channel) (VersionPins, error)

ResolveBundleVersions returns the resolved version pins for every plugin in the given bundle, using the highest registry version that satisfies the channel filter. Returns an error for unknown bundle slugs or non-installable bundles (nself-plus, ntask).

Network behavior: delegates to plugin.FetchRegistry which respects the registry cache + fallback chain. A registry fetch failure with no stale cache returns an error; callers should respect that.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL