fix(cli): routed plugin install local paths through link instead of npm

`omp plugin install .` (and any cwd-relative, absolute, or tilde-prefixed
spec) failed with `Invalid package name: .` because
`classifyInstallTarget` only emitted `marketplace` / `npm`, so local paths
fell through to `validatePackageName`, which rejects every non-npm name.

The classifier now emits a third `local` arm for `.`, `..`, `./…`,
`..\…`, `~`, `~/…`, `~\…`, `/…`, `C:\…`, `C:/…`, and `\\unc` specs.
`handleInstall` dispatches that arm to `PluginManager.link()` — the same
code path as `omp plugin link <path>` — so the two verbs are
interchangeable for local plugin directories. `--dry-run` short-circuits
before any filesystem work, `--scope`/`--force` surface a warning since
they are no-ops here (link is idempotent, and scope only governs
marketplace installs).

Coverage: thirteen-case classifier matrix in `marketplace/cli.test.ts`
plus a new `plugin-install-local.test.ts` with spy-based routing checks
for `./`, `../`, `/`, `~/`, plus a real-filesystem test that stages a
plugin folder, invokes `runPluginCommand`, and verifies the resulting
symlink + lockfile entry. The new test calls `mock.restore()` in
`afterEach` so `piUtils` / `MarketplaceManager.prototype` spies do not
leak into sibling suites.

Fixes #1945
This commit is contained in:
roboomp
2026-06-05 17:48:54 +00:00
parent 61f11a6ce0
commit 7b488b5675
5 changed files with 251 additions and 5 deletions
@@ -354,6 +354,7 @@ async function handleInstall(
console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`));
console.error(chalk.dim(` ${APP_NAME} plugin install github:user/repo`));
console.error(chalk.dim(` ${APP_NAME} plugin install https://github.com/user/repo#v1.0`));
console.error(chalk.dim(` ${APP_NAME} plugin install ./path/to/local/plugin`));
process.exit(1);
}
@@ -382,6 +383,49 @@ async function handleInstall(
continue;
}
if (target.type === "local") {
// Local paths route to link(): symlink the directory into the plugins
// node_modules tree so source edits show up without a reinstall. Matches
// `omp plugin link <path>` so users can use either verb interchangeably.
if (flags.scope) {
console.error(
chalk.yellow(
`Warning: --scope is only supported for marketplace installs (name@marketplace). Ignoring for ${spec}.`,
),
);
}
if (flags.force) {
console.error(
chalk.yellow(
`Warning: --force has no effect for local path installs (link is already idempotent). Ignoring for ${spec}.`,
),
);
}
if (flags.dryRun) {
if (flags.json) {
console.log(JSON.stringify({ dryRun: true, action: "link", path: target.path }, null, 2));
} else {
console.log(chalk.dim(`[dry-run] Would link ${spec}`));
}
continue;
}
try {
const result = await manager.link(target.path);
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${spec}`));
if (result.manifest.description) {
console.log(chalk.dim(` ${result.manifest.description}`));
}
}
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
process.exit(1);
}
continue;
}
// --scope only applies to marketplace installs; warn when it would be silently no-op'd for npm.
if (flags.scope) {
console.error(
@@ -923,6 +967,7 @@ ${chalk.bold("Sources:")}
github:user/repo[#ref] GitHub shorthand (also gitlab:, bitbucket:, codeberg:, sourcehut:)
https://github.com/user/repo Full git URL (https, ssh, or git protocol)
name@marketplace Marketplace plugin (see marketplace command)
./path, ../path, /abs, ~/path Local plugin directory (symlinked, same as plugin link)
${chalk.bold("Config Subcommands:")}
config list <pkg> List all settings