From fc347e2b99d8f97a4c957efdad0b6e0f570ae315 Mon Sep 17 00:00:00 2001 From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com> Date: Tue, 12 May 2026 09:40:07 -0700 Subject: [PATCH 1/4] feat: add system-level plugin-auth.json support Lando now reads and merges a system-level plugin-auth.json in addition to the user-level ~/.lando/plugin-auth.json. The system file acts as a base that user config overrides, enabling IT/MDM-managed machines to deploy registry credentials once for all users. System paths: /Library/Application Support/Lando/ (macOS), /srv/lando/ (Linux), %PROGRAMDATA%\Lando\ (Windows). --- docs/cli/plugin-login.md | 2 +- docs/guides/hosting-private-plugin.md | 276 ++++++++++++++++++++++++++ lib/cli.js | 1 + lib/lando.js | 6 +- tasks/plugin-add.js | 2 +- utils/get-plugin-config.js | 9 +- 6 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 docs/guides/hosting-private-plugin.md diff --git a/docs/cli/plugin-login.md b/docs/cli/plugin-login.md index af24d97f0..fe906fc23 100644 --- a/docs/cli/plugin-login.md +++ b/docs/cli/plugin-login.md @@ -7,7 +7,7 @@ description: lando plugin-login logs you into a valid lando plugin registry Logs into a Lando plugin registry. -This will authorize your Lando installation against a Lando plugin registry. A "Lando Plugin Registry" is any `npm` compatible regsitry eg: +This will authorize your Lando installation against a Lando plugin registry and write the resulting credentials to `~/.lando/plugin-auth.json`. A "Lando Plugin Registry" is any `npm` compatible regsitry eg: * `https://registry.npmjs.org` * `https://npm.pkg.github.com` diff --git a/docs/guides/hosting-private-plugin.md b/docs/guides/hosting-private-plugin.md new file mode 100644 index 000000000..3dba4089d --- /dev/null +++ b/docs/guides/hosting-private-plugin.md @@ -0,0 +1,276 @@ +--- +title: Hosting and Distributing a Private Lando Plugin +description: Learn how to build, publish, and distribute a private Lando plugin using a private npm-compatible registry like GitHub Packages. + +authors: + - name: Team Lando + pic: https://gravatar.com/avatar/c335f31e62b453f747f39a84240b3bbd + link: https://twitter.com/devwithlando +updated: + timestamp: 1778457600000 + +mailchimp: + # action is required + action: https://dev.us12.list-manage.com/subscribe/post?u=59874b4d6910fa65e724a4648&id=613837077f + # everything else is optional + title: Want similar content? + byline: Signup and we will send you a weekly blog digest of similar content to keep you satiated. + button: Sign me up! +--- + +# Hosting and Distributing a Private Lando Plugin + +This guide walks through publishing a Lando plugin to a private npm-compatible registry and distributing it to developer machines — including how to handle authentication so `lando plugin-add` and automatic update checks both work seamlessly. + +[[toc]] + +## 1. Choose a Registry + +| Option | Best for | +|--------|----------| +| **GitHub Packages** | Orgs already on GitHub — free for private repos with GitHub Enterprise or limited free tier | +| **Verdaccio** | Self-hosted, lightweight, zero cost | +| **Nexus / Artifactory** | Enterprise environments with existing artifact infrastructure | + +The instructions below use GitHub Packages (`npm.pkg.github.com`) as the example, but the pattern is identical for other registries — only the registry URL and auth method differ. + +## 2. Prepare Your Plugin Package + +Your plugin needs three things to be recognized and loaded by Lando. + +### package.json + +Your `package.json` must have either a `lando` property or `"lando-plugin"` in `keywords`: + +```json +{ + "name": "@myorg/my-lando-plugin", + "version": "1.0.0", + "keywords": ["lando-plugin"], + "lando": {}, + "main": "index.js", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} +``` + +The `@myorg` scope must match your GitHub organization name when using GitHub Packages. + +### plugin.yml + +Every plugin **must** include a `plugin.yml` at its root. Lando's plugin loader filters out any plugin installed to a path matching `plugins/lando-*` unless this file exists — without it the plugin is silently ignored. + +```yaml +name: "@myorg/my-lando-plugin" + +api: 3 +is-updateable: true +``` + +### Task file + +For this demo, we're going to publish a simple command that outputs `Hello World`. Tasks live in a `tasks/` subdirectory. Each file exports a function that receives the `lando` instance: + +```javascript +// tasks/my-command.js +'use strict'; + +module.exports = lando => ({ + command: 'my-command', + describe: 'Does something useful', + usage: '$0 my-command', + level: 'tasks', + run: async () => { + console.log('Hello World'); + }, +}); +``` + +### Minimum file structure + +``` +my-lando-plugin/ +├── package.json # must have "lando": {} or lando-plugin keyword +├── plugin.yml # required — gates plugin discovery +├── index.js # minimal: module.exports = async lando => {}; +└── tasks/ + └── my-command.js +``` + +## 3. Publish the Plugin + +```bash +# Authenticate as a publisher (needs write:packages scope) +npm login --registry=https://npm.pkg.github.com --scope=@myorg + +# Publish +npm publish +``` + +For CI/CD, use a GitHub Actions token or a service account PAT: + +```bash +npm publish --registry=https://npm.pkg.github.com +``` + +## 4. Distribute Credentials to Developer Machines + +Lando reads registry credentials from two `plugin-auth.json` files and merges them — the system-level file is the base and the user-level file overrides it: + +| Scope | Path | +|-------|------| +| **System** (all users on the machine) | macOS: `/Library/Application Support/Lando/plugin-auth.json`
Linux: `/srv/lando/plugin-auth.json`
Windows: `%PROGRAMDATA%\Lando\plugin-auth.json` | +| **User** (current user only) | `~/.lando/plugin-auth.json` | + +Both files use the same key format as npm's config, translated to JSON: + +```json +{ + "@myorg:registry": "https://npm.pkg.github.com", + "//npm.pkg.github.com/:_authToken": "ghp_TOKENHERE" +} +``` + +- `@myorg:registry` — tells Lando (via pacote) which registry to use for `@myorg`-scoped packages +- `//npm.pkg.github.com/:_authToken` — the auth token for that registry (nerfdart format) + +### Option A: Onboarding script (recommended) + +Write a shell script that drops the file, substituting a token from a secret manager or environment variable: + +```bash +#!/usr/bin/env bash +# onboard-lando-plugin.sh +# Requires: GITHUB_PACKAGES_TOKEN set in environment or passed as $1 + +TOKEN="${1:-$GITHUB_PACKAGES_TOKEN}" +LANDO_DIR="${HOME}/.lando" +AUTH_FILE="${LANDO_DIR}/plugin-auth.json" + +mkdir -p "$LANDO_DIR" + +cat > "$AUTH_FILE" <Linux: `/srv/lando/plugin-auth.json`
Windows: `%PROGRAMDATA%\Lando\plugin-auth.json` | +| One-time install | `lando plugin-add @myorg/my-lando-plugin && lando --clear` | +| Ongoing updates | Automatic — no extra steps | diff --git a/lib/cli.js b/lib/cli.js index a37eccb38..4c0b288b3 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -187,6 +187,7 @@ module.exports = class Cli { packaged, pluginConfig: {}, pluginConfigFile: path.join(this.userConfRoot, 'plugin-auth.json'), + systemPluginConfigFile: path.join(getSysDataPath('lando'), 'plugin-auth.json'), pluginDirs: [ // src locations {path: path.join(srcRoot, 'node_modules', '@lando'), subdir: '.', namespace: '@lando'}, diff --git a/lib/lando.js b/lib/lando.js index 3403e5c08..f8d028a64 100644 --- a/lib/lando.js +++ b/lib/lando.js @@ -218,7 +218,7 @@ module.exports = class Lando { agent: this.config.userAgent, channel: this.config.channel, cli: _.get(this, 'config.cli'), - config: getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig), + config: getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile), debug: require('../utils/debug-shim')(this.log), }); @@ -477,7 +477,7 @@ module.exports = class Lando { const Plugin = require('../components/plugin'); // reset Plugin static defaults for v3 purposes - Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig); + Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile); Plugin.debug = require('../utils/debug-shim')(this.log); // attempt to compute the destination to install the plugin @@ -522,7 +522,7 @@ module.exports = class Lando { const Plugin = require('../components/plugin'); // reset Plugin static defaults for v3 purposes - Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig); + Plugin.config = getPluginConfig(this.config.pluginConfigFile, this.config.pluginConfig, this.config.systemPluginConfigFile); Plugin.debug = require('../utils/debug-shim')(this.log); // attempt to compute the destination to install the plugin diff --git a/tasks/plugin-add.js b/tasks/plugin-add.js index 33a7cf653..654eccb91 100644 --- a/tasks/plugin-add.js +++ b/tasks/plugin-add.js @@ -59,7 +59,7 @@ module.exports = lando => { // normalize incoming options on top of any managed or user plugin config we already have options.config = merge({}, [ - getPluginConfig(lando.config.pluginConfigFile, lando.config.pluginConfig), + getPluginConfig(lando.config.pluginConfigFile, lando.config.pluginConfig, lando.config.systemPluginConfigFile), lopts2Popts(options), ]); diff --git a/utils/get-plugin-config.js b/utils/get-plugin-config.js index ac0d07086..7ec37c3a6 100644 --- a/utils/get-plugin-config.js +++ b/utils/get-plugin-config.js @@ -4,9 +4,8 @@ const fs = require('fs'); const merge = require('lodash/merge'); const read = require('./read-file'); -module.exports = (file, config = {}) => { - // if config file exists then rebase config on top of it - if (fs.existsSync(file)) return merge({}, read(file), config); - // otherwise return config alone - return config; +module.exports = (file, config = {}, systemFile = null) => { + const base = systemFile && fs.existsSync(systemFile) ? read(systemFile) : {}; + if (fs.existsSync(file)) return merge({}, base, read(file), config); + return merge({}, base, config); }; From 8d88b4e85b41d7bad647747d7d6787a4b648d932 Mon Sep 17 00:00:00 2001 From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com> Date: Tue, 12 May 2026 09:50:39 -0700 Subject: [PATCH 2/4] fix: handle unreadable system plugin-auth.json and clarify logout scope Wrap system file read in try/catch so a permission-denied error doesn't crash Lando at startup. Also note in plugin-logout docs that the command only clears user-level credentials, not system-level ones. --- docs/cli/plugin-logout.md | 6 +++++- utils/get-plugin-config.js | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/cli/plugin-logout.md b/docs/cli/plugin-logout.md index 2167c7a3c..e3095323f 100644 --- a/docs/cli/plugin-logout.md +++ b/docs/cli/plugin-logout.md @@ -5,7 +5,11 @@ description: lando plugin-logout Logs you out of all active sessions established # lando plugin-logout -Logs you out of _all_ active sessions established by `lando plugin-login` +Logs you out of _all_ active sessions established by `lando plugin-login` by clearing `~/.lando/plugin-auth.json`. + +::: warning System-level credentials are not affected +If credentials were deployed to the system-level `plugin-auth.json` (by an IT/MDM process), this command will not remove them. Those must be removed manually from the system path. +::: ## Usage diff --git a/utils/get-plugin-config.js b/utils/get-plugin-config.js index 7ec37c3a6..27d7f1b36 100644 --- a/utils/get-plugin-config.js +++ b/utils/get-plugin-config.js @@ -5,7 +5,14 @@ const merge = require('lodash/merge'); const read = require('./read-file'); module.exports = (file, config = {}, systemFile = null) => { - const base = systemFile && fs.existsSync(systemFile) ? read(systemFile) : {}; + let base = {}; + if (systemFile && fs.existsSync(systemFile)) { + try { + base = read(systemFile); + } catch { + // system file unreadable (e.g. wrong permissions) — skip silently + } + } if (fs.existsSync(file)) return merge({}, base, read(file), config); return merge({}, base, config); }; From b09f42c05ef419563fc1f91c3bb82918b71ac982 Mon Sep 17 00:00:00 2001 From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com> Date: Tue, 12 May 2026 09:55:26 -0700 Subject: [PATCH 3/4] test: add Leia test for system-level plugin-auth.json credentials Verifies that a private plugin can be installed using only credentials in /srv/lando/plugin-auth.json (no user-level ~/.lando/plugin-auth.json), exercising the new system-level auth merge. Test runs before plugin-login so there is no user-level file present to mask a failure. --- examples/plugins/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 50a493ef3..397d158f6 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -58,6 +58,16 @@ lando config | grep -q "plugins/@lando/php" lando plugin-remove "@lando/php" lando config | grep -qv "plugins/@lando/php" +# Should be able to add a private plugin using only system-level plugin-auth.json credentials +sudo mkdir -p /srv/lando +echo "{\"@lando:registry\":\"https://npm.pkg.github.com\",\"//npm.pkg.github.com/:_authToken\":\"$GITHUB_PAT\"}" | sudo tee /srv/lando/plugin-auth.json > /dev/null +lando config | grep -qv "plugins/@lando/lando-plugin-test" +lando plugin-add "@lando/lando-plugin-test" +lando config | grep -q "plugins/@lando/lando-plugin-test" +lando plugin-remove "@lando/lando-plugin-test" +lando config | grep -qv "plugins/@lando/lando-plugin-test" +sudo rm /srv/lando/plugin-auth.json + # Should execute `lando plugin-login` lando plugin-login --registry "https://npm.pkg.github.com" --password "$GITHUB_PAT" --username "rtfm-47" --scope "lando::registry=https://npm.pkg.github.com" From ac421444c6c380a668470bd3f2ad65ed83151b74 Mon Sep 17 00:00:00 2001 From: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com> Date: Tue, 12 May 2026 09:59:43 -0700 Subject: [PATCH 4/4] docs: update CHANGELOG for system-level plugin-auth.json --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e8a85cd5..a04e5d39e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## {{ UNRELEASED_VERSION }} - [{{ UNRELEASED_DATE }}]({{ UNRELEASED_LINK }}) +* Added system-level `plugin-auth.json` support so IT/MDM-managed machines can deploy private registry credentials once for all users (macOS: `/Library/Application Support/Lando/plugin-auth.json`, Linux: `/srv/lando/plugin-auth.json`, Windows: `%PROGRAMDATA%\Lando\plugin-auth.json`) + ## v3.26.4 - [April 28, 2026](https://github.com/lando/core/releases/tag/v3.26.4) * Fixed `lando ssh` defaulting to a v3 service instead of the v4 `appserver` in mixed-api apps [#461](https://github.com/lando/core/pull/461)