22e564a85d
Extends `omp plugin install` to accept git sources alongside npm specs and
marketplace refs. Bun's installer already understands git URLs; the blocker
was `PluginManager.install`'s strict npm-name validator and the assumption
that the actual package name could be derived from the spec.
- `git-url.ts`: `parseGitUrl` now recognizes npm-style namespaced shorthand
(`github:user/repo`, `gitlab:`, `bitbucket:`, `codeberg:`, `sourcehut:` /
`srht:`), with optional `#ref` and `.git` suffix. Exposes `isGitSpec` as
`parseGitUrl(s) !== null`. Existing protocol-URL and `git:` shorthand paths
are untouched.
- `manager.ts`: `install()` branches on `isGitSpec`. Git specs go through a
separate `validateGitSpec` (shell-metachar rejection only — `/`, `:`, `@`,
`#`, `+` are legal) and the real package name is discovered by snapshotting
`plugins/package.json` deps before `bun install` and diffing afterwards.
Falls back to value-match on force-reinstall where the key already exists.
- Help text in `plugin-cli` documents the new sources and adds a github:
example.
Smoke tested end-to-end on Windows with both forms against the test repo:
PluginManager.install('github:oldschoola/omp-insights')
PluginManager.install('https://github.com/oldschoola/omp-insights')
both resolve `@oldschoola/omp-insights@1.2.3` and write a correct lock entry.
Shell-injection probe (`github:foo/bar; rm -rf /`) is rejected.
205 lines
5.5 KiB
TypeScript
205 lines
5.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { isGitSpec, parseGitUrl } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/git-url";
|
|
|
|
describe("parseGitUrl", () => {
|
|
describe("protocol URLs (accepted without git: prefix)", () => {
|
|
test("parses https URL", () => {
|
|
const result = parseGitUrl("https://github.com/user/repo");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
pinned: false,
|
|
});
|
|
});
|
|
|
|
test("parses ssh URL", () => {
|
|
const result = parseGitUrl("ssh://git@github.com/user/repo");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "ssh://git@github.com/user/repo",
|
|
pinned: false,
|
|
});
|
|
});
|
|
|
|
test("parses git protocol URL", () => {
|
|
const result = parseGitUrl("git://github.com/user/repo");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "git://github.com/user/repo",
|
|
pinned: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("shorthand URLs (accepted only with git: prefix)", () => {
|
|
test("parses host/path shorthand with git: prefix", () => {
|
|
const result = parseGitUrl("git:github.com/user/repo");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
pinned: false,
|
|
});
|
|
});
|
|
|
|
test("parses scp-like ssh shorthand with git: prefix", () => {
|
|
const result = parseGitUrl("git:git@github.com:user/repo@v1.0.0");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "git@github.com:user/repo",
|
|
ref: "v1.0.0",
|
|
pinned: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("local paths and unprefixed shorthand", () => {
|
|
test("rejects unprefixed host/path shorthand", () => {
|
|
expect(parseGitUrl("github.com/user/repo")).toBeNull();
|
|
});
|
|
|
|
test("rejects unprefixed scp-like SSH shorthand", () => {
|
|
expect(parseGitUrl("git@github.com:user/repo")).toBeNull();
|
|
});
|
|
|
|
test("does not misclassify local paths containing dots", () => {
|
|
expect(parseGitUrl("plugins.v2/my-plugin")).toBeNull();
|
|
expect(parseGitUrl("vendor/github.enterprise/tools")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("namespaced shorthand (github:user/repo, gitlab:, …)", () => {
|
|
test("parses github: shorthand", () => {
|
|
expect(parseGitUrl("github:user/repo")).toEqual({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
ref: undefined,
|
|
pinned: false,
|
|
});
|
|
});
|
|
|
|
test("captures #ref and marks pinned", () => {
|
|
const result = parseGitUrl("github:user/repo#v1.0");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
ref: "v1.0",
|
|
pinned: true,
|
|
});
|
|
});
|
|
|
|
test("strips trailing .git from the path", () => {
|
|
const result = parseGitUrl("github:user/repo.git");
|
|
expect(result).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
pinned: false,
|
|
});
|
|
});
|
|
|
|
test("maps gitlab: to gitlab.com", () => {
|
|
expect(parseGitUrl("gitlab:user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "gitlab.com",
|
|
path: "user/repo",
|
|
repo: "https://gitlab.com/user/repo",
|
|
});
|
|
});
|
|
|
|
test("maps bitbucket: to bitbucket.org", () => {
|
|
expect(parseGitUrl("bitbucket:user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "bitbucket.org",
|
|
path: "user/repo",
|
|
repo: "https://bitbucket.org/user/repo",
|
|
});
|
|
});
|
|
|
|
test("maps codeberg: to codeberg.org", () => {
|
|
expect(parseGitUrl("codeberg:user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "codeberg.org",
|
|
path: "user/repo",
|
|
repo: "https://codeberg.org/user/repo",
|
|
});
|
|
});
|
|
|
|
test("maps sourcehut: and srht: to git.sr.ht", () => {
|
|
expect(parseGitUrl("sourcehut:user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "git.sr.ht",
|
|
path: "user/repo",
|
|
repo: "https://git.sr.ht/user/repo",
|
|
});
|
|
expect(parseGitUrl("srht:user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "git.sr.ht",
|
|
path: "user/repo",
|
|
repo: "https://git.sr.ht/user/repo",
|
|
});
|
|
});
|
|
|
|
test("rejects missing repo segment", () => {
|
|
expect(parseGitUrl("github:user")).toBeNull();
|
|
});
|
|
|
|
test("rejects empty body", () => {
|
|
expect(parseGitUrl("github:")).toBeNull();
|
|
});
|
|
|
|
test("rejects unknown shorthand prefix", () => {
|
|
expect(parseGitUrl("notahost:user/repo")).toBeNull();
|
|
});
|
|
|
|
test("does not swallow protocol URLs (regression)", () => {
|
|
expect(parseGitUrl("https://github.com/user/repo")).toMatchObject({
|
|
type: "git",
|
|
host: "github.com",
|
|
path: "user/repo",
|
|
repo: "https://github.com/user/repo",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isGitSpec", () => {
|
|
test("returns true for namespaced shorthand", () => {
|
|
expect(isGitSpec("github:user/repo")).toBe(true);
|
|
});
|
|
|
|
test("returns true for https git URLs", () => {
|
|
expect(isGitSpec("https://github.com/user/repo")).toBe(true);
|
|
});
|
|
|
|
test("returns false for unprefixed scp-like ssh (parseGitUrl rejects it)", () => {
|
|
expect(isGitSpec("git@github.com:user/repo")).toBe(false);
|
|
});
|
|
|
|
test("returns false for bare npm name", () => {
|
|
expect(isGitSpec("my-plugin")).toBe(false);
|
|
});
|
|
|
|
test("returns false for scoped npm name", () => {
|
|
expect(isGitSpec("@scope/pkg")).toBe(false);
|
|
});
|
|
|
|
test("returns false for scoped npm name with version", () => {
|
|
expect(isGitSpec("@scope/pkg@1.2.3")).toBe(false);
|
|
});
|
|
});
|