Ovvi

Packages and installs

Where plugins come from: npm, git or a local path, the Plugins page, and the default plugins as packages

A plugin is a package, as a pi package is. Ovvi installs it, lists it in ~/.config/ovvi/settings.json, and can update, reload and remove it. The app ships no package manager. It uses the npm, pnpm or bun on the user's login PATH.

Sources

settings.json lists the packages the user installed, under packages. An entry is a source string, or an object with a source (pi's form):

{
  "packages": [
    "npm:@acme/ovvi-csv@1.2.0",
    "git:github.com/acme/ovvi-kanban@v1",
    "~/code/my-plugin"
  ]
}
SourceWhat it isWhere it goes
npm:<spec>an npm package: npm:@scope/name@1.2.0, npm:name, or a tarball's path~/.config/ovvi/plugins/<name>/: npm pack, then the tarball unpacked. Needs npm
git:<repository>[@ref]a git repository: git:github.com/x/y@v1, an https:// or ssh:// URL, git@host:x/y, or a local repository's path~/.config/ovvi/plugins/<repository name>/: a clone at the ref
a path that starts with /, ~ or .a package on this Macnowhere: it loads in place. A relative path is from the folder of settings.json
  • A versioned spec stays at its version. Update fetches the same source again, so it gets the newest version that the spec allows.
  • A local path is never installed or changed. Its dependencies are its author's: run npm install and npm run build in its folder.
  • A folder that the user or an agent puts in ~/.config/ovvi/plugins/ by hand also loads. It is not in settings.json.
  • The repository's extra plugins, in plugins/extra/ (Tart VM), are local paths: npm ci and npm run build in the folder, then ovvi plugins install ~/code/ovvi/plugins/extra/<name> or the path in Settings ▸ Plugins. The app carries only plugins/builtin/.
  • hideInNewPane lists the ids of the plugins that the New pane chooser leaves out, for example "hideInNewPane": ["git"]. A plugin's page in Settings and the confirmation write it.

Install

  1. The app fetches the source into a staging folder in the plugins folder. No script of the package runs.
  2. It reads the manifest and shows the confirmation: the id, the version, the source and folder, the description, and that plugins run code as you (Security and trust). Cancel removes the staging folder.
  3. It installs the package's dependencies and devDependencies into the package's own node_modules, from its lockfile when it has one, with that lockfile's manager. Without a lockfile it uses the first of npm, pnpm and bun on the PATH.
  4. If the package has a build script and its built entry is missing, the app runs run build. A published package that ships its dist/ needs no build.
  5. It moves the package into place, over an older install, adds the source to settings.json, keeps its id in trust.json, and loads its open cards again.

The install commands:

ManagerCommand
npmnpm ci (with a lockfile) or npm install, with --no-audit --no-fund --ignore-scripts --legacy-peer-deps
pnpmpnpm install --frozen-lockfile (or --no-frozen-lockfile), with --ignore-scripts --config.auto-install-peers=false
bunbun install --ignore-scripts --omit=peer, with --frozen-lockfile when the lockfile is bun's
  • No install script of a dependency runs (--ignore-scripts). Only the package's own build script runs, and only after the user confirms it.
  • No peer dependency installs. @ovvi/plugin-api is the app's, and it is not on the npm registry (Types).
  • A fetch (npm pack, tar, git clone, git checkout) stops after 5 minutes, and an install or build command after 10 minutes. A failure shows its last 20 lines, and the plugin's log has the whole output.
  • An id that another package already has is refused: "The id ‹id› is taken by ‹folder›". So is a folder name that is already in the plugins folder.

The Plugins page

Ovvi ▸ Plugins… and ⌘K Plugins… open Settings at its Plugins page (Settings).

  • The field takes a source (npm:package, git:repository or a folder's path). Return or Install installs it. The spinner and "Installing…" take the Install button's place while it runs. A failure shows on a red notice, with Retry when running it again can help.
  • New Plugin… asks for a name, then makes a plugin from the template (Build a plugin).
  • Reload All reads every package again and reloads every plugin with an open card. ⌘K's Reload Plugins does the same.
  • A row per plugin, the browser first: its icon, its name and version, and its source under it: "Comes with Ovvi", the npm: or git: source, the local path, or the folder. A source in settings.json whose folder is missing shows "Not installed". A row opens the plugin's page.
  • An agent's work shows on the page too: an agent on This Mac makes, reloads and installs plugins with the ovvi command (CLI), and the page shows Creating…, Installing… and any failure, as for its own.

A plugin's page

  • The header: the icon, the name, the version, the description and the source, and the buttons that apply:

    ButtonOnDoes
    Updatenpm and git installs ("Install" while its folder is missing)fetches the source again. It does not ask again, unless the new version has another id
    Update to Version ‹x.y.z›, Reset to Defaultthe defaults, when the app offers itThe default plugins
    Show Logevery installed plugin but the browseropens <id>.log in the app for .log files (Console)
    Reloadevery installed plugin but the browserruns its build script and loads its cards again (Reload)
  • Remove…, in a section of its own at the end of the page, "Remove ‹name›" (every plugin but the defaults): asks, then takes it off the list, takes its id out of trust.json, forgets its settings, and moves its folder to the Trash. A local path's folder stays where it is.

  • Its settings, drawn from the manifest's settings (Settings). A change is saved at once and reaches the plugin's open cards.

  • Show in New Pane: a switch for each plugin whose manifest has a chooser row. The install's confirmation sets it with its checkbox, which is off by default. Off, the New pane chooser leaves the plugin out, and settings.json keeps its id in hideInNewPane. ⌘K's Open ‹chooser› Here stays.

  • The browser's page has its setting and Show in New Pane, and Show in Privacy for its approved websites (The browser).

Plugin settings in settings.json

settings.json keeps the values that the user set on the plugins' pages, under plugins, by plugin id and setting id. Only a value that differs from the setting's default is written, so a new default reaches a user who never changed it:

{
  "packages": ["npm:@acme/ovvi-csv@1.2.0"],
  "plugins": {
    "explorer": { "hidden-files": true },
    "git": { "commits": 1000 }
  }
}
  • A value that does not fit its setting (a wrong type, an option that is not there, a number out of range) reads as the default.
  • Remove takes the plugin's values out. A hand edit, or an agent's edit, takes effect at Reload Plugins.

Where the registry looks

The registry reads the manifests at launch, and again after an install, an update, a removal and Reload Plugins. Nothing watches the folders. The first package of an id wins, in this order:

  1. The browser, whose manifest is written in Swift.
  2. The default plugins: their installed copies in the plugins folder, or the app's own manifest while a copy is not installed yet.
  3. The local paths in settings.json.
  4. Every folder in ~/.config/ovvi/plugins/ but hidden ones and node_modules.

A package that is left out (a bad manifest, a taken id, a listed path with no package.json) is written to the log under the folder's name. A bad manifest also goes under the manifest's id when the id reads.

The default plugins

File Explorer (explorer) and Git (git) are packages, each with its package.json, lockfile, src/ and build script. The app carries their source, without node_modules and dist, and nothing of theirs is in the app's own build.

  • First launch. When a default's folder is missing in the plugins folder, the app copies its source there, runs npm ci from its lockfile, then its build script (about 1 s each). Its cards show "Installing ‹name›…" meanwhile. With no package manager, or offline, the card says what failed, with Try Again (Errors and logs).
  • They are the user's. The user or an agent can edit them, then press Reload Plugin. They never ask for a confirmation.
  • An app with a newer version (the package's version) replaces an unedited copy at launch. An older app never replaces a copy, so the installed app and another Release build (make app) can share the folder.
  • An edited copy, or a copy whose source differs from the app's at the same version, stays. Its page offers Update to Version ‹x.y.z› (a newer version) or Reset to Default. For an edited copy it asks first: "Replace your edited ‹name›?". The edited copy goes to the Trash.
  • .defaults.json in the plugins folder keeps each default's version, the hash of the app's source, and the hash of the copy after its install. A hash covers the files outside node_modules and dist, so a build does not count as an edit.
  • A quit during an install leaves a .install-* staging folder. The app removes it at the next launch.

Choices

The decisions are D96 and D100 on the Decisions page.

On this page