Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/plugin-login.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
6 changes: 5 additions & 1 deletion docs/cli/plugin-logout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
276 changes: 276 additions & 0 deletions docs/guides/hosting-private-plugin.md
Original file line number Diff line number Diff line change
@@ -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`<br>Linux: `/srv/lando/plugin-auth.json`<br>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" <<EOF
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "${TOKEN}"
}
EOF

echo "Lando plugin credentials written to ${AUTH_FILE}"

# Install the plugin
lando plugin-add @myorg/my-lando-plugin
```

Distribute the token separately (1Password, Vault, AWS Secrets Manager, etc.) and have developers run:

```bash
GITHUB_PACKAGES_TOKEN=ghp_xxx ./onboard-lando-plugin.sh
```

### Option B: Dotfiles management (Ansible / chezmoi / etc.)

If your org manages developer machines with Ansible or a dotfiles tool, template `~/.lando/plugin-auth.json` the same way you'd manage `.npmrc`.

**Ansible example:**
```yaml
- name: Configure Lando private plugin registry
copy:
dest: "{{ ansible_env.HOME }}/.lando/plugin-auth.json"
content: |
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "{{ vault_github_packages_token }}"
}
mode: "0600"
```

**chezmoi example** — add `~/.lando/plugin-auth.json.tmpl`:
```json
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "{{ .githubPackagesToken }}"
}
```

### Option C: `lando plugin-login` (classic npm registries only)

`lando plugin-login` works with registries that support CouchDB-style auth (standard npmjs.org protocol). It does **not** work with GitHub Packages, which only accepts PATs. Use it with Verdaccio or Nexus if those are configured with username/password auth:

```bash
lando plugin-login \
--username myuser \
--password "$TOKEN_OR_PASSWORD" \
--registry https://verdaccio.myorg.internal
```

This writes the resulting session token to `~/.lando/plugin-auth.json` (the user-level file) automatically.

### Option D: System-level credentials (IT-managed machines)

If you manage developer workstations centrally — via MDM, configuration management, or a machine image — drop credentials into the system-level path instead of each user's home directory. Every user on the machine will inherit them automatically; individual users can still override specific keys in their own `~/.lando/plugin-auth.json`.

**macOS** (`/Library/Application Support/Lando/plugin-auth.json`):
```bash
sudo mkdir -p "/Library/Application Support/Lando"
sudo tee "/Library/Application Support/Lando/plugin-auth.json" <<EOF
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "${TOKEN}"
}
EOF
sudo chmod 644 "/Library/Application Support/Lando/plugin-auth.json"
```

**Linux** (`/srv/lando/plugin-auth.json`):
```bash
sudo mkdir -p /srv/lando
sudo tee /srv/lando/plugin-auth.json <<EOF
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "${TOKEN}"
}
EOF
sudo chmod 644 /srv/lando/plugin-auth.json
```

**Windows** (`%PROGRAMDATA%\Lando\plugin-auth.json`):
```powershell
New-Item -ItemType Directory -Force -Path "$env:ProgramData\Lando"
@"
{
"@myorg:registry": "https://npm.pkg.github.com",
"//npm.pkg.github.com/:_authToken": "$TOKEN"
}
"@ | Set-Content "$env:ProgramData\Lando\plugin-auth.json"
```

## 5. Install the Plugin

Once credentials are in place:

```bash
lando plugin-add @myorg/my-lando-plugin
lando --clear
```

::: tip Clear the cache after installing
The `--clear` flag is required after installing a new plugin to regenerate Lando's task cache. Without it, new commands from the plugin will not appear.
:::

Lando merges the system and user `plugin-auth.json` files with the command config before calling pacote, so no `--registry` or `--auth` flags are needed at install time.

## 6. Updates

No extra configuration is required for updates. Every time `lando` runs it checks `plugin.check4Update()`, which calls `pacote.packument()` using the same merged config built from system and user `plugin-auth.json` files. As long as credentials are stored in either file, update checks against your private registry happen automatically and developers get the standard update prompt.

## Summary

| Step | Tool / File |
|------|-------------|
| Publish plugin | `npm publish --registry https://npm.pkg.github.com` |
| Store credentials (user) | `~/.lando/plugin-auth.json` |
| Store credentials (system-wide) | macOS: `/Library/Application Support/Lando/plugin-auth.json`<br>Linux: `/srv/lando/plugin-auth.json`<br>Windows: `%PROGRAMDATA%\Lando\plugin-auth.json` |
| One-time install | `lando plugin-add @myorg/my-lando-plugin && lando --clear` |
| Ongoing updates | Automatic — no extra steps |
10 changes: 10 additions & 0 deletions examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down
6 changes: 3 additions & 3 deletions lib/lando.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tasks/plugin-add.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]);

Expand Down
16 changes: 11 additions & 5 deletions utils/get-plugin-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ 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) => {
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);
};
Loading