7b488b5675
`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
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
/**
|
|
* Classify an install spec as a local path, marketplace plugin reference, or
|
|
* plain npm package.
|
|
*
|
|
* Rules (applied in order):
|
|
* 0. Looks like a filesystem path (`.`, `..`, `./…`, `..\…`, `/…`, `~/…`,
|
|
* `C:\…`, `\\unc`) -> local. Routed through `PluginManager.link()` so the
|
|
* `omp plugin install <path>` and `omp plugin link <path>` flows agree.
|
|
* 1. Starts with `@` (scoped npm) -> always npm.
|
|
* 2. Contains `@` after the first character -> split on the LAST `@`.
|
|
* If the right-hand side is a known marketplace name, it's a marketplace ref.
|
|
* Otherwise it's an npm spec (e.g. `pkg@1.2.3`).
|
|
* 3. No `@` -> npm.
|
|
*/
|
|
// Common npm dist-tags that should never be interpreted as marketplace names
|
|
const NPM_DIST_TAGS = new Set([
|
|
"latest",
|
|
"next",
|
|
"beta",
|
|
"alpha",
|
|
"canary",
|
|
"rc",
|
|
"dev",
|
|
"stable",
|
|
"nightly",
|
|
"experimental",
|
|
]);
|
|
|
|
// Semver-like: starts with digit, or contains version range prefixes
|
|
const LOOKS_LIKE_VERSION = /^[\d~^>=<]/;
|
|
|
|
/**
|
|
* Detect specs that name a filesystem path rather than a package: bare `.` /
|
|
* `..`, cwd-relative (`./`, `../`, `.\`, `..\`), absolute (`/`, `C:\`, `C:/`,
|
|
* UNC `\\`), and tilde-prefixed (`~`, `~/`, `~\`). Tilde paths still rely on
|
|
* the shell or the caller for expansion — we only classify them so they reach
|
|
* the link path instead of npm-name validation.
|
|
*/
|
|
function isLocalPathSpec(spec: string): boolean {
|
|
if (spec === "." || spec === ".." || spec === "~") return true;
|
|
if (spec.startsWith("./") || spec.startsWith("../")) return true;
|
|
if (spec.startsWith(".\\") || spec.startsWith("..\\")) return true;
|
|
if (spec.startsWith("~/") || spec.startsWith("~\\")) return true;
|
|
if (spec.startsWith("/")) return true;
|
|
if (spec.startsWith("\\\\")) return true;
|
|
if (/^[A-Za-z]:[\\/]/.test(spec)) return true;
|
|
return false;
|
|
}
|
|
|
|
export type ClassifiedInstallTarget =
|
|
| { type: "local"; path: string }
|
|
| { type: "marketplace"; name: string; marketplace: string }
|
|
| { type: "npm"; spec: string };
|
|
|
|
export function classifyInstallTarget(spec: string, knownMarketplaces: Set<string>): ClassifiedInstallTarget {
|
|
// Rule 0: filesystem path — bypass npm/marketplace validation entirely.
|
|
if (isLocalPathSpec(spec)) return { type: "local", path: spec };
|
|
// Rule 1: scoped npm package — @ at position 0 is never a marketplace separator.
|
|
if (spec.startsWith("@")) return { type: "npm", spec };
|
|
// Rule 2: @ somewhere after the first character.
|
|
const atIdx = spec.lastIndexOf("@");
|
|
if (atIdx > 0) {
|
|
const rhs = spec.slice(atIdx + 1);
|
|
// Dist-tags and version specifiers are never marketplace names.
|
|
if (NPM_DIST_TAGS.has(rhs) || LOOKS_LIKE_VERSION.test(rhs)) {
|
|
return { type: "npm", spec };
|
|
}
|
|
if (knownMarketplaces.has(rhs)) {
|
|
return { type: "marketplace", name: spec.slice(0, atIdx), marketplace: rhs };
|
|
}
|
|
// Not a known marketplace — treat as npm version specifier.
|
|
return { type: "npm", spec };
|
|
}
|
|
// Rule 3: no @ at all.
|
|
return { type: "npm", spec };
|
|
}
|