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 resolved from bundles.json (ADR-P6-03), the single source of truth for bundle -> plugin membership, served at plugins.nself.org/bundles.json. See registry.go for the fetch+cache loader. The cmd layer must consume this package via Get/Names/All — do NOT hand-maintain a second bundle map anywhere else in the CLI.
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).
App-ready (P7-PLUG-19): after the members are installed the installer records the ledger and, when InstallOpts.Hooks is set, runs reconcile, start, readiness, tracking and health (package appready; installer_appready.go). The member loop is in installer_members.go and the licence pre-flight in installer_licence.go. A free bundle (Bundle.IsFree) skips the ping_api bundle entitlement call.
registry.go — fetch+cache loader for bundles.json (ADR-P6-03).
Load is called once, eagerly, at command start (bundleCmd's PersistentPreRunE) — never lazily on first Get/All access. It fetches the live document from plugins.nself.org/bundles.json, validates it, and populates the in-memory bundle map every invocation. On fetch failure it falls back to the last-known-good local cache (mirrors internal/license/validate.go's RefreshCache/ExportCache/ImportCache cache-then-fall-back convention); if no cache exists either, Load returns a clear network error rather than silently serving an empty bundle set.
There is no go:embed fallback — the loader always fetches live, cache fallback only on fetch failure (per ticket P6-E4-W3-S3-T10).
registry_cache.go — local on-disk cache for the last-known-good bundles.json (P6-E4-W3-S3-T10). Split out of registry.go to stay under the 300-line/file cap (internal/repoqa).
remove_guard.go — `remove <plugin>` and the bundle ledger (P7-PLUG-20).
Purpose: stop a single-plugin remove from leaving an installed bundle half installed, and keep the ledger in step after a plugin is removed. Inputs: the plugin directory and a plugin name. Outputs: ReferencingBundles (who needs the plugin), GuardPluginRemove (the E133 refusal or a warning), ForgetPlugin (drop the removed plugin's record). Constraints: reads go through Store.Load (a missing ledger is bootstrapped in memory and never written); ForgetPlugin writes only when the ledger file already exists and never creates it.
remove_guard_testhook.go — a test hook for the bundle catalog (P7-PLUG-20).
Purpose: tests of callers that run before any command has loaded the bundle catalog (remove <plugin>, remove <bundle>) need the "not loaded yet" state. Inputs: none. Outputs: the catalog is unloaded; the returned func restores it. Constraints: tests only; production code never calls it.
remover.go — `nself bundle remove <bundle>` orchestrator (P7-PLUG-20).
Purpose: remove a bundle without deleting what something else still needs. A plugin goes only when the bundle install ledger says no other installed bundle references it AND the owner did not install it on its own (PLUG D6); a plugin another bundle or the owner needs stays, with the reason printed. Inputs: a bundle slug and RemoveOpts (dry-run, delete-data, confirmation). Outputs: a RemoveResult; the plugins removed from the plugin directory and the ledger at <pluginDir>/.nself-state/bundles.json updated to match. Constraints: the plan, the removals and the ledger writes all happen inside one ledger lock (Store.Locked), so a concurrent install cannot add a reference between the plan and the delete. Before the first removal the ledger is written (a bootstrap ledger included) and a remove intent is recorded in .nself-state/remove-intent.json (remover_intent.go); the intent shrinks after each member and is cleared on full success, so a kill or a failed final write is resumed by running the same command again. Removal stops at the first failure and the ledger then reflects exactly what was removed. A member that an installed plugin outside the plan depends on is kept and recorded explicit (remover_deps.go). Plugin data is kept unless --delete-data (see remover_plan.go for the v1.4/v1.5 rules and the confirmation gate). Members are removed in reverse bundle order (a dependency before its dependents is removed last).
remover_deps.go — keep bundle members that a kept plugin depends on (D-0325).
Purpose: `bundle remove` releases a member when no bundle and no explicit install needs it, but a plugin outside the plan can still list that member in its plugin.json dependencies. Removing it would break that plugin. Inputs: a removePlan, the original ledger, the plugin directory and a reverse-dependency scan (plugin.ReverseDependencies unless a test swaps it). Outputs: the plan with such members moved from Planned to Kept (reason "needed by <plugins>; recorded explicit") and their records in plan.work restored as explicit, so a later bundle remove keeps them too. Constraints: iterates to a fixpoint (a kept plugin is itself an outside dependent of what it needs). A scan error refuses the plan: nothing is removed. No I/O beyond the scan.
remover_intent.go — the durable steps of a real `nself bundle remove` (D-0368).
Purpose: before this, the plan, every plugin removal and the ledger write ran inside one ledger Update that wrote the file only at the end. With no ledger file yet, a kill between members (or a failed final write) left nothing on disk, so the next run re-bootstrapped and, because bootstrap records a bundle only when every member is installed, lost the half-removed bundle (E134). Inputs: the held ledger Tx and ledger, the bundle, the plugin directory and the Remove options. Outputs: members removed; the ledger and the remove intent kept in step. Constraints: all of it runs inside ONE Store.Locked. Order: read the intent, plan, checkpoint the ledger, record the intent, remove members (intent shrinks after each), on full success clear the intent, then write the final ledger (a kill between the last two is harmless: see locked). If either checkpoint write fails nothing is removed. A failed intent shrink is only a warning: the intent then over-approximates and a gone member plans as not installed. RemoveOpts.noIntent (tests only) skips the intent and the checkpoint to reproduce the old behaviour.
remover_plan.go — the plan, the data policy and the ledger settlement of `nself bundle remove` (P7-PLUG-20).
Purpose: keep Remove short. Holds the pure planning step (what the ledger lets this bundle remove), the data-handling rules, the --delete-data confirmation gate and the ledger bookkeeping after removals. Inputs: a Bundle, a ledger.Ledger and the plugin directory. Outputs: a removePlan, the data decision, and the ledger to write. Constraints: computePlan and settleLedger do no I/O beyond stat calls on the plugin directory; the caller holds the ledger lock for real runs.
remover_recover.go — read the remove intent of an interrupted `bundle remove` (D-0368).
Purpose: tell the user a remove is being resumed, and drop an intent whose bundle the ledger no longer records. Inputs: the held ledger Tx, the loaded ledger and the bundle. Outputs: whether the intent belongs to a recorded bundle; a stale intent is cleared. Constraints: runs inside the Locked callback of Remove. The checkpoint write makes the ledger durable before any delete, so a recorded bundle needs no repair. If the ledger does not record the bundle (the file was deleted and re-bootstrapped, which marks leftovers explicit), the intent is only cleared and the normal path follows (E134 on v1.5, a no-op warning on v1.4): nothing is rebuilt from the intent, because a plugin that came back unrecorded after a stale intent must not be deleted on its say-so.
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
- Variables
- func EnsureLoaded(ctx context.Context) error
- func ForgetPlugin(pluginDir, name string) error
- func GuardPluginRemove(w io.Writer, pluginDir, name string, force bool) error
- func InstalledFacts(pluginDir string, names []string) ([]ledger.InstalledPlugin, error)
- func IsRecorded(pluginDir, slug string) (bool, error)
- func LedgerSources(pluginDir string) func() (ledger.Sources, error)
- func LedgerStore(pluginDir string) *ledger.Store
- func Load(ctx context.Context) error
- func LoadBytes(data []byte) error
- func Names() []string
- func ReferencingBundles(pluginDir, name string) ([]string, error)
- func ResolveConflicts(requirements map[string][]string, strict bool) (map[string]string, error)
- func ResolveMaxVersion(pluginName string, versions []string) (string, error)
- func ResolveVersion(ctx context.Context, pluginName string, channel Channel) (string, error)
- func UnknownBundleError(slug string) error
- func UnloadCatalogForTest() (restore func())
- type Bundle
- type Channel
- type Conflict
- type InstallOpts
- type InstallResult
- type KeptPlugin
- type RemoveOpts
- type RemoveResult
- type VersionPins
Constants ¶
const ( // DefaultBundlesURL is the canonical bundles.json endpoint (T8's CF // worker). Override with NSELF_BUNDLES_URL for staging/tests. DefaultBundlesURL = "https://plugins.nself.org/bundles.json" )
Variables ¶
var DisplayOrder = []string{
"task", "chat", "claw", "family", "sentry", "tv", "clawde", "nself-plus",
}
DisplayOrder is the canonical print order for bundle listings (ADR-P6-03 ordering canon): task -> chat -> claw -> family -> sentry -> tv -> clawde, plus the computed ɳSelf+ meta-bundle last.
Functions ¶
func EnsureLoaded ¶ added in v1.3.6
EnsureLoaded populates the resolver if it has not been loaded yet in this process — a no-op once Load/LoadBytes has already succeeded once. This is the safety net for callers OUTSIDE the `nself bundle` command family (which eagerly calls Load in its PersistentPreRunE): `nself build`, `start`, `restart`, `stop`, `ops`, and `status` all reach bundle expansion via internal/build's manifest loader without ever running through that PreRunE, so without this, a manifest's `bundles:` entries would silently expand to nothing in every one of those commands (P6-E4-W3-S3-T10 CI regression: TestLoadProjectManifest_FlatPluginsAndBundleExpansion). Deliberately NOT a refresh-on-every-call: once state is non-nil, this returns immediately, matching Load's own "eager once, not lazy-per-Get" design intent, just anchored at the first real caller in a process instead of assuming that caller is always the bundle subcommand tree.
func ForgetPlugin ¶ added in v1.4.13
ForgetPlugin drops the plugin's record from the ledger after it was removed from disk. It does nothing when the ledger file does not exist (a plugin remove never bootstraps the ledger) or has no record of the plugin; a ledger file that cannot be stat'ed for another reason is an error.
func GuardPluginRemove ¶ added in v1.4.13
GuardPluginRemove checks a plugin remove against the bundle ledger. In v1.5 mode a plugin an installed bundle references is refused with E133 unless force is set; in v1.4 mode (or with force) a warning naming the bundles is written to w and the remove proceeds. A ledger that cannot be read cannot prove the plugin is unreferenced, so v1.5 without force returns that error.
func InstalledFacts ¶ added in v1.4.13
func InstalledFacts(pluginDir string, names []string) ([]ledger.InstalledPlugin, error)
InstalledFacts returns the ledger facts (name, version, tier, checksum) of the named plugins that are on disk in pluginDir, from their plugin.json files. It is the exported form of installedMembers for callers outside this package (cmd/commands/plugin_install_ledger.go); a plugin that is not on disk is omitted.
func IsRecorded ¶ added in v1.4.13
IsRecorded reports whether the bundle ledger of pluginDir records the bundle (alias or slug) as installed. Read only: a missing ledger is bootstrapped in memory and never written. An unknown bundle or a ledger that cannot be read is reported as not recorded, with the error for callers that want it.
func LedgerSources ¶ added in v1.4.13
LedgerSources returns the bootstrap source for the ledger stored in pluginDir: every bundle's membership plus the plugins installed there. A plugin directory that cannot be read is an error, never an empty Sources. So is a bundle catalog that cannot be loaded (P7-PLUG-20): with no catalog every plugin would bootstrap as explicit, which hides every bundle reference.
func LedgerStore ¶ added in v1.4.13
LedgerStore returns the ledger store of pluginDir with its bootstrap source wired.
func Load ¶ added in v1.3.6
Load eager-fetches and validates bundles.json, populating the resolver used by Get/Names/All/IsInstallable. Call once at command start. On fetch failure, or when the fetched document fails validation, falls back to the validated local cache (see loadBundlesFromCache); with no usable cache, returns an error rather than leaving the resolver empty.
func LoadBytes ¶ added in v1.3.6
LoadBytes parses and validates a raw bundles.json payload and installs it as the current bundle set. Exported so tests (and any future offline import path) can seed a fixture document without a network round trip.
func Names ¶
func Names() []string
Names returns every known bundle slug sorted alphabetically. Useful for error messages and shell completion. Canonical slugs only — aliases never appear here.
func ReferencingBundles ¶ added in v1.4.13
ReferencingBundles returns, sorted, the installed bundles whose install references the plugin (its installed_by), or nil when none does.
func ResolveConflicts ¶ added in v1.1.1
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
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 ¶
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 ¶
UnknownBundleError formats a friendly error when a slug does not match a known bundle. Includes the sorted list of valid names as a hint.
func UnloadCatalogForTest ¶ added in v1.4.13
func UnloadCatalogForTest() (restore func())
UnloadCatalogForTest empties the loaded bundle catalog (the state of a process that has not run Load yet) and returns a func that restores it.
Types ¶
type Bundle ¶
type Bundle struct {
Name string // Display name (with ɳ glyph)
Slug string // CLI-arg lowercase identifier (bundles.json canonical slug)
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. Bundles not present in the loaded set (e.g. a future bundles.json omitting one) are silently skipped rather than panicking.
func Get ¶
Get returns the Bundle for the given slug. The slug is case-insensitive and trimmed. Falls back to bundleAliases when there is no canonical match. Returns ok=false when the slug is unknown or bundles.json has not been loaded yet (see Load); callers can call Names() to render a useful hint.
func (Bundle) IsFree ¶ added in v1.4.13
IsFree reports whether the bundle costs nothing. formatPrice in registry.go yields "FREE" exactly for a free tier or a zero price. It is a price heuristic, not licence evidence, so a free bundle still gets the per-plugin licence pre-flight; only the ping_api bundle entitlement call is skipped.
func (Bundle) IsInstallable ¶
IsInstallable reports whether the bundle can be installed via `nself bundle install`. Only the ɳSelf+ meta-bundle (no single SKU, only the union of every paid bundle's entitlements) and a bundle with no plugins are not installable as a unit. The free Task Bundle is installable since P7-PLUG-19 (`nself bundle install task` is its app-ready path); a free bundle skips the bundle entitlement call (see IsFree).
This is the ONLY bundle-membership rule the CLI hand-codes: which display slug is a structural category, not plugin data. The plugin SET of every installable bundle, including "sentry", is exactly bundles.json's plugins[] array: no cli-side subsetting and no special case for nself-stripe or any other plugin.
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.
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
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:
- Collect all (plugin, version) requirements across all bundles.
- For each plugin with multiple requirements, select MAX using plugin.CompareVersions.
- 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
// Hooks, when non-nil, make Install run reconcile, start, readiness,
// tracking and health after the ledger step and judge app-ready. Nil keeps
// the legacy behaviour: install, ledger, and the caller reconciles.
Hooks *appready.Hooks
// 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
Changed bool // membership changed; the command layer must reconcile
// Report lists the app-ready steps (nil for a dry run). Reconciled is true
// once the Reconcile hook ran, so the caller must not reconcile again.
Report *appready.Report
Reconciled 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 KeptPlugin ¶ added in v1.4.13
KeptPlugin is a bundle member that stays installed, and why.
type RemoveOpts ¶
type RemoveOpts struct {
DryRun bool
// DeleteData drops plugin data (schema/tables). It needs confirmation:
// Yes, or Confirm answering true; otherwise Remove refuses with E403.
DeleteData bool
// Yes is the --yes flag: confirms DeleteData without a prompt.
Yes bool
// Confirm asks the owner on a terminal; nil means not interactive.
Confirm func(prompt string) bool
// KeepData is the deprecated --keep-data alias. From v1.5 it is a no-op
// (data is kept by default); in v1.4 mode it keeps data of a paid bundle.
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 this call removes (on disk, no other reference)
Kept []KeptPlugin // members still on disk that stay, with the reason
NotInstalled []string // members not on disk
Removed []string
Failed []string
DryRun bool
KeepData bool // effective: plugin data was (or would be) kept
Changed bool // membership changed; the command layer must reconcile
}
RemoveResult captures the outcome of a bundle remove call.
func Remove ¶
func Remove(ctx context.Context, bundleSlug string, opts RemoveOpts) (*RemoveResult, error)
Remove removes the bundle's plugins that no other bundle and no explicit install needs, and forgets the bundle in the ledger. See the file comment.
type VersionPins ¶
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.
Source Files
¶
- bundles.go
- installer.go
- installer_appready.go
- installer_helpers.go
- installer_helpers_ledger.go
- installer_helpers_licence.go
- installer_licence.go
- installer_members.go
- installer_multi.go
- ledger_sources.go
- registry.go
- registry_cache.go
- remove_guard.go
- remove_guard_testhook.go
- remover.go
- remover_deps.go
- remover_intent.go
- remover_plan.go
- remover_recover.go
- resolver.go
- semver_resolve.go
- version_resolver.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package appready defines what "app-ready" means for a bundle install (P7-PLUG-19, cap:cli.bundle-app-ready) and runs the steps after the plugin files are on disk.
|
Package appready defines what "app-ready" means for a bundle install (P7-PLUG-19, cap:cli.bundle-app-ready) and runs the steps after the plugin files are on disk. |
|
atomic.go — the durable file writer shared by the ledger and the remove intent.
|
atomic.go — the durable file writer shared by the ledger and the remove intent. |