zmarketplace 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Install dispatcher — runs the correct install command for the detected agent.
3
+ * Always runs an audit first and requires explicit confirmation.
4
+ */
5
+
6
+ import type { InstallResult, InstallTarget, AuditReport } from "./types.ts";
7
+ import { auditPackage } from "./audit.ts";
8
+ import { getNpmPackageMeta } from "../registries/npm.ts";
9
+
10
+ /** Which agent are we running inside? Detected by probing globals/env. */
11
+ export function detectAgent(): InstallTarget {
12
+ const env = globalThis.process?.env ?? {};
13
+ // Pi/omp sets these
14
+ if (env["PI_VERSION"] || env["OMP_VERSION"]) return "pi";
15
+ // Claude Code sets this
16
+ if (env["CLAUDE_CODE"] || env["CLAUDE"]) return "claude";
17
+ // OpenCode
18
+ if (env["OPENCODE"]) return "opencode";
19
+ // Gemini CLI
20
+ if (env["GEMINI_CLI"]) return "gemini";
21
+ // Codex
22
+ if (env["CODEX"]) return "codex";
23
+ return "auto";
24
+ }
25
+
26
+ /** Build the install command for a given target. */
27
+ function buildCommand(packageName: string, target: InstallTarget): string {
28
+ switch (target) {
29
+ case "pi": return `pi install npm:${packageName}`;
30
+ case "omp": return `omp plugin install npm:${packageName}`;
31
+ case "claude": return `claude plugin install npm:${packageName}`;
32
+ case "opencode": return `opencode plugin ${packageName}`;
33
+ case "gemini": return `gemini extension install npm:${packageName}`;
34
+ case "codex": return `codex plugin add npm:${packageName}`;
35
+ case "auto": return `npm install ${packageName}`;
36
+ }
37
+ }
38
+
39
+ /** Resolve a "auto" target by checking package keywords. */
40
+ async function resolveAutoTarget(packageName: string): Promise<InstallTarget> {
41
+ const meta = await getNpmPackageMeta(packageName);
42
+ if (!meta) return "auto";
43
+ const latest = meta["dist-tags"]?.latest;
44
+ if (!latest) return "auto";
45
+ const version = meta.versions[latest];
46
+ const kws = new Set((version?.keywords ?? []).map((k: string) => k.toLowerCase()));
47
+ if (kws.has("pi-package")) return "pi";
48
+ if (kws.has("claude-code")) return "claude";
49
+ if (kws.has("opencode")) return "opencode";
50
+ if (kws.has("gemini-cli")) return "gemini";
51
+ if (kws.has("codex")) return "codex";
52
+ return "auto";
53
+ }
54
+
55
+ export interface InstallOptions {
56
+ /** Force the install target instead of auto-detecting. */
57
+ target?: InstallTarget;
58
+ /** Skip the pre-install audit. Default false. */
59
+ skipAudit?: boolean;
60
+ /** Callback for audit-before-confirmation flow. Returns true to proceed. */
61
+ confirm?: (report: AuditReport | null) => boolean;
62
+ }
63
+
64
+ /**
65
+ * Install a package: audit → confirm → run command.
66
+ * Never auto-installs. Returns the command and result.
67
+ */
68
+ export async function installPackage(
69
+ packageName: string,
70
+ options: InstallOptions = {},
71
+ ): Promise<InstallResult> {
72
+ // Step 1: Audit (unless skipped)
73
+ let report: AuditReport | null = null;
74
+ if (!options.skipAudit) {
75
+ report = await auditPackage(packageName, { deepScan: true });
76
+ }
77
+
78
+ // Step 2: Confirm
79
+ if (options.confirm && !options.confirm(report)) {
80
+ return {
81
+ packageName,
82
+ target: options.target ?? "auto",
83
+ command: "",
84
+ success: false,
85
+ message: "Installation cancelled by user.",
86
+ };
87
+ }
88
+
89
+ // Step 3: Resolve target
90
+ let target = options.target ?? detectAgent();
91
+ if (target === "auto") {
92
+ target = await resolveAutoTarget(packageName);
93
+ }
94
+
95
+ // Step 4: Build and return the command (do not execute — the agent/user runs it)
96
+ const command = buildCommand(packageName, target);
97
+
98
+ const riskNote = report && report.risk !== "safe"
99
+ ? ` ⚠️ Audit risk: ${report.risk.toUpperCase()}`
100
+ : "";
101
+
102
+ return {
103
+ packageName,
104
+ target,
105
+ command,
106
+ success: true,
107
+ message: `Ready to install. Run:\n ${command}${riskNote}`,
108
+ };
109
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Unified search engine — aggregates results across all registries.
3
+ * Deduplicates and ranks by relevance.
4
+ */
5
+
6
+ import type { PackageResult, SearchOptions, Ecosystem, RegistrySource } from "./types.ts";
7
+ import { searchNpm, detectEcosystems } from "../registries/npm.ts";
8
+ import { searchClaudeMarketplace } from "../registries/claude.ts";
9
+ import { searchGeminiExtensions } from "../registries/gemini.ts";
10
+ import { searchMcpRegistry } from "../registries/mcp.ts";
11
+
12
+ /** Determine which registries to query based on options. */
13
+ function registriesForOptions(registry?: RegistrySource | "all"): RegistrySource[] {
14
+ if (!registry || registry === "all") {
15
+ return ["npm", "claude-marketplace", "gemini-extensions", "mcp-registry"];
16
+ }
17
+ return [registry];
18
+ }
19
+
20
+ /** Map a user-facing ecosystem filter to npm ecosystem list for the npm adapter. */
21
+ function ecosystemsForFilter(ecosystem?: Ecosystem | "all"): Ecosystem[] | undefined {
22
+ if (!ecosystem || ecosystem === "all") return undefined;
23
+ // pi and omp share the same npm keyword
24
+ if (ecosystem === "omp") return ["pi"];
25
+ return [ecosystem];
26
+ }
27
+
28
+ /** Deduplicate results by package name, preferring npm source for richer metadata. */
29
+ function deduplicate(results: PackageResult[]): PackageResult[] {
30
+ const byName = new Map<string, PackageResult>();
31
+ for (const r of results) {
32
+ const key = r.name.toLowerCase();
33
+ const existing = byName.get(key);
34
+ if (!existing) {
35
+ byName.set(key, r);
36
+ } else {
37
+ // Merge: union ecosystems, keep richer source
38
+ byName.set(key, {
39
+ ...existing,
40
+ ecosystems: [...new Set([...existing.ecosystems, ...r.ecosystems])],
41
+ source: existing.source === "npm" ? existing.source : r.source,
42
+ });
43
+ }
44
+ }
45
+ return [...byName.values()];
46
+ }
47
+
48
+ /** Score a result by how well it matches the query. */
49
+ function scoreResult(result: PackageResult, query: string): number {
50
+ const q = query.toLowerCase().trim();
51
+ if (!q) return 0;
52
+
53
+ const name = result.name.toLowerCase();
54
+ const desc = result.description.toLowerCase();
55
+
56
+ let score = 0;
57
+ if (name === q) score += 100;
58
+ else if (name.startsWith(q)) score += 50;
59
+ else if (name.includes(q)) score += 25;
60
+ if (desc.includes(q)) score += 10;
61
+ // Prefer packages with known ecosystems
62
+ score += result.ecosystems.filter(e => e !== "unknown").length * 5;
63
+ return score;
64
+ }
65
+
66
+ /** Execute a unified search across all configured registries. */
67
+ export async function search(options: SearchOptions): Promise<PackageResult[]> {
68
+ const limit = options.limit ?? 20;
69
+ const registries = registriesForOptions(options.registry);
70
+ const ecosystems = ecosystemsForFilter(options.ecosystem);
71
+
72
+ const queries: Promise<PackageResult[]>[] = [];
73
+
74
+ if (registries.includes("npm")) {
75
+ queries.push(searchNpm(options.query, { ecosystems, type: options.type, limit }));
76
+ }
77
+ if (registries.includes("claude-marketplace")) {
78
+ queries.push(searchClaudeMarketplace(options.query, limit));
79
+ }
80
+ if (registries.includes("gemini-extensions")) {
81
+ queries.push(searchGeminiExtensions(options.query, limit));
82
+ }
83
+ if (registries.includes("mcp-registry")) {
84
+ queries.push(searchMcpRegistry(options.query, limit));
85
+ }
86
+
87
+ const settled = await Promise.allSettled(queries);
88
+ let results = settled
89
+ .filter((s): s is PromiseFulfilledResult<PackageResult[]> => s.status === "fulfilled")
90
+ .flatMap(s => s.value);
91
+
92
+ // Filter by type
93
+ if (options.type && options.type !== "all") {
94
+ results = results.filter(r => r.type === options.type);
95
+ }
96
+
97
+ // Filter by ecosystem
98
+ if (options.ecosystem && options.ecosystem !== "all") {
99
+ results = results.filter(r => r.ecosystems.includes(options.ecosystem as Ecosystem));
100
+ }
101
+
102
+ // Deduplicate
103
+ results = deduplicate(results);
104
+
105
+ // Rank: exact name > name prefix > name contains > description match
106
+ results.sort((a, b) => scoreResult(b, options.query) - scoreResult(a, options.query));
107
+
108
+ return results.slice(0, limit);
109
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * TUI display layer — formatting helpers for search results, detail cards,
3
+ * audit reports, and interactive option builders.
4
+ *
5
+ * Uses Unicode icons + box-drawing for terminal rendering.
6
+ */
7
+
8
+ import type { PackageResult, PackageDetail, AuditReport, Ecosystem, PackageType, AuditSeverity } from "./types.ts";
9
+
10
+ const TYPE_ICON: Record<PackageType, string> = {
11
+ extension: "🔧",
12
+ skill: "⭐",
13
+ theme: "🎨",
14
+ prompt: "📝",
15
+ plugin: "📦",
16
+ mcp: "🔌",
17
+ unknown: "❓",
18
+ };
19
+
20
+ const ECO_LABEL: Record<Ecosystem, string> = {
21
+ pi: "pi",
22
+ omp: "omp",
23
+ claude: "claude",
24
+ opencode: "opencode",
25
+ gemini: "gemini",
26
+ codex: "codex",
27
+ universal: "universal",
28
+ unknown: "?",
29
+ };
30
+
31
+ const SEVERITY_ICON: Record<AuditSeverity, string> = {
32
+ critical: "🔴",
33
+ high: "🟠",
34
+ medium: "🟡",
35
+ low: "🟢",
36
+ info: "ℹ️",
37
+ };
38
+
39
+ const RISK_ICON: Record<AuditReport["risk"], string> = {
40
+ safe: "✅",
41
+ low: "🟢",
42
+ moderate: "🟡",
43
+ high: "🟠",
44
+ critical: "🔴",
45
+ };
46
+
47
+ /** Format a single result as a one-line select option. */
48
+ export function formatResultOption(result: PackageResult, index: number): { label: string; value: string } {
49
+ const icon = TYPE_ICON[result.type] ?? "📦";
50
+ const eco = result.ecosystems
51
+ .filter(e => e !== "unknown")
52
+ .map(e => ECO_LABEL[e])
53
+ .join(",");
54
+ const desc = result.description.length > 70
55
+ ? result.description.slice(0, 67) + "..."
56
+ : result.description;
57
+ const ver = result.version ? ` v${result.version}` : "";
58
+ return {
59
+ label: `${icon} [${index + 1}] ${result.name}${ver} (${eco}) — ${desc}`,
60
+ value: String(index),
61
+ };
62
+ }
63
+
64
+ /** Format results for ctx.ui.select(). */
65
+ export function buildSelectOptions(results: PackageResult[]): Array<{ label: string; value: string }> {
66
+ return results.map((r, i) => formatResultOption(r, i));
67
+ }
68
+
69
+ /** Format a detailed package card. */
70
+ export function formatDetailCard(detail: PackageDetail): string {
71
+ const icon = TYPE_ICON[detail.type] ?? "📦";
72
+ const eco = detail.ecosystems.filter(e => e !== "unknown").map(e => ECO_LABEL[e]).join(", ") || "npm";
73
+ const lines: string[] = [
74
+ `━━━ ${icon} ${detail.name} v${detail.version ?? "?"} [${eco}] ━━━`,
75
+ "",
76
+ ];
77
+
78
+ if (detail.description) lines.push(` ${detail.description}`);
79
+ lines.push("");
80
+ if (detail.license) lines.push(` License: ${detail.license}`);
81
+ if (detail.dependencyCount !== undefined) lines.push(` Dependencies: ${detail.dependencyCount}`);
82
+ if (detail.size) lines.push(` Size: ${(detail.size / 1024).toFixed(1)} KB`);
83
+ if (detail.fileCount) lines.push(` Files: ${detail.fileCount}`);
84
+ if (detail.publishedAt) lines.push(` Published: ${detail.publishedAt.slice(0, 10)}`);
85
+ if (detail.npmUrl) lines.push(` npm: ${detail.npmUrl}`);
86
+ if (detail.repository) lines.push(` Repository: ${detail.repository}`);
87
+
88
+ lines.push("");
89
+ if (detail.installCommand) lines.push(` ▶ Install: ${detail.installCommand}`);
90
+
91
+ return lines.join("\n");
92
+ }
93
+
94
+ /** Format an audit report for display. */
95
+ export function formatAuditReport(report: AuditReport): string {
96
+ const riskIcon = RISK_ICON[report.risk];
97
+ const lines: string[] = [
98
+ `${riskIcon} Security Audit: ${report.packageName} v${report.version ?? "?"}`,
99
+ ` Risk: ${report.risk.toUpperCase()}`,
100
+ ` Deep scan: ${report.deepScanned ? "yes (source code scanned)" : "no (metadata only)"}`,
101
+ "",
102
+ ];
103
+
104
+ if (report.findings.length === 0) {
105
+ lines.push(" ✅ No security issues found.");
106
+ } else {
107
+ lines.push(` ${report.findings.length} finding(s):`);
108
+ for (const f of report.findings.slice(0, 20)) {
109
+ const icon = SEVERITY_ICON[f.severity];
110
+ const loc = f.file ? ` ${f.file}${f.line ? `:${f.line}` : ""}` : "";
111
+ lines.push(` ${icon} [${f.severity.toUpperCase()}] ${f.reason}${loc}`);
112
+ }
113
+ if (report.findings.length > 20) {
114
+ lines.push(` ... and ${report.findings.length - 20} more`);
115
+ }
116
+ }
117
+
118
+ return lines.join("\n");
119
+ }
120
+
121
+ /** Format a help message for /zmarketplace with no args. */
122
+ export function formatHelp(): string {
123
+ return [
124
+ "📦 zmarketplace — cross-agent package search",
125
+ "",
126
+ "Usage:",
127
+ " /zmarketplace search <query> Search across all registries",
128
+ " /zmarketplace detail <id|name> Show package details",
129
+ " /zmarketplace audit <id|name> Run security audit",
130
+ " /zmarketplace install <id|name> Audit + install a package",
131
+ "",
132
+ "Options for 'search':",
133
+ " --type=<type> Filter: extension, skill, theme, prompt, plugin, mcp",
134
+ " --eco=<ecosystem> Filter: pi, claude, opencode, gemini, codex",
135
+ " --limit=<n> Max results (default 20)",
136
+ "",
137
+ "Registries: npm + Claude marketplace + Gemini extensions",
138
+ "",
139
+ "Examples:",
140
+ " /zmarketplace search mcp",
141
+ " /zmarketplace search subagent --eco=pi",
142
+ " /zmarketplace install 3",
143
+ " /zmarketplace audit pi-mcp-adapter",
144
+ ].join("\n");
145
+ }
146
+
147
+ /** Parse CLI-style args from the command input. */
148
+ export interface ParsedArgs {
149
+ subcommand: string;
150
+ positional: string[];
151
+ flags: Record<string, string>;
152
+ }
153
+
154
+ export function parseArgs(args: string[]): ParsedArgs {
155
+ const positional: string[] = [];
156
+ const flags: Record<string, string> = {};
157
+ let subcommand = "";
158
+
159
+ for (let i = 0; i < args.length; i++) {
160
+ const arg = args[i];
161
+ if (arg.startsWith("--")) {
162
+ const eq = arg.indexOf("=");
163
+ if (eq !== -1) {
164
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
165
+ } else {
166
+ flags[arg.slice(2)] = "true";
167
+ }
168
+ } else if (!subcommand) {
169
+ subcommand = arg;
170
+ } else {
171
+ positional.push(arg);
172
+ }
173
+ }
174
+
175
+ return { subcommand, positional, flags };
176
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Core types for zmarketplace — unified package model across all registries.
3
+ */
4
+
5
+ /** Which agent ecosystem a package targets. */
6
+ export type Ecosystem = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "universal" | "unknown";
7
+
8
+ /** Package resource type. */
9
+ export type PackageType = "extension" | "skill" | "theme" | "prompt" | "plugin" | "mcp" | "unknown";
10
+
11
+ /** Which registry a result came from. */
12
+ export type RegistrySource = "npm" | "claude-marketplace" | "gemini-extensions" | "pi-dev" | "mcp-registry";
13
+
14
+ /** A normalized search result — the common currency across all registries. */
15
+ export interface PackageResult {
16
+ /** Canonical package name (npm name, or marketplace entry name). */
17
+ name: string;
18
+ /** Short description. */
19
+ description: string;
20
+ /** Semantic version if known. */
21
+ version?: string;
22
+ /** Author display name or handle. */
23
+ author?: string;
24
+ /** Which ecosystems this package targets. */
25
+ ecosystems: Ecosystem[];
26
+ /** Resource type. */
27
+ type: PackageType;
28
+ /** Which registry surfaced this result. */
29
+ source: RegistrySource;
30
+ /** Homepage or repository URL. */
31
+ homepage?: string;
32
+ /** npm registry URL. */
33
+ npmUrl?: string;
34
+ /** Repository URL. */
35
+ repository?: string;
36
+ /** Download counts if available (monthly). */
37
+ downloads?: number;
38
+ /** Last published date (ISO string). */
39
+ publishedAt?: string;
40
+ /** License. */
41
+ license?: string;
42
+ /** Install command string for the detected/queried ecosystem. */
43
+ installCommand?: string;
44
+ }
45
+
46
+ /** Options for a search operation. */
47
+ export interface SearchOptions {
48
+ query: string;
49
+ /** Filter by resource type. */
50
+ type?: PackageType | "all";
51
+ /** Filter by ecosystem. */
52
+ ecosystem?: Ecosystem | "all";
53
+ /** Filter by specific registry. */
54
+ registry?: RegistrySource | "all";
55
+ /** Max results total. Default 20. */
56
+ limit?: number;
57
+ }
58
+
59
+ /** Detailed package info — richer than PackageResult. */
60
+ export interface PackageDetail extends PackageResult {
61
+ /** Full README or long description. */
62
+ readme?: string;
63
+ /** Dependencies count. */
64
+ dependencyCount?: number;
65
+ /** Unpacked size in bytes. */
66
+ size?: number;
67
+ /** File count in the tarball. */
68
+ fileCount?: number;
69
+ /** Keywords from npm. */
70
+ keywords?: string[];
71
+ /** Pi/omp manifest if present. */
72
+ piManifest?: Record<string, unknown>;
73
+ /** Claude plugin manifest if present. */
74
+ claudeManifest?: Record<string, unknown>;
75
+ /** Gemini extension manifest if present. */
76
+ geminiManifest?: Record<string, unknown>;
77
+ }
78
+
79
+ /** Severity for audit findings. */
80
+ export type AuditSeverity = "critical" | "high" | "medium" | "low" | "info";
81
+
82
+ /** A single security finding from the audit. */
83
+ export interface AuditFinding {
84
+ severity: AuditSeverity;
85
+ /** Pattern that was matched. */
86
+ pattern: string;
87
+ /** File where the match was found. */
88
+ file?: string;
89
+ /** Line number. */
90
+ line?: number;
91
+ /** Excerpt of the matching code. */
92
+ excerpt?: string;
93
+ /** Explanation of why this is flagged. */
94
+ reason: string;
95
+ }
96
+
97
+ /** Result of a security audit. */
98
+ export interface AuditReport {
99
+ packageName: string;
100
+ version?: string;
101
+ /** Overall risk rating. */
102
+ risk: "safe" | "low" | "moderate" | "high" | "critical";
103
+ /** Metadata-based findings (Layer 1 — zero cost). */
104
+ metadataFindings: AuditFinding[];
105
+ /** Source-scan findings (Layer 2 — requires tarball download). */
106
+ sourceFindings: AuditFinding[];
107
+ /** All findings combined. */
108
+ findings: AuditFinding[];
109
+ /** Whether a deep source scan was performed. */
110
+ deepScanned: boolean;
111
+ /** Summary text for display. */
112
+ summary: string;
113
+ }
114
+
115
+ /** Install target — which agent's install command to use. */
116
+ export type InstallTarget = "pi" | "omp" | "claude" | "opencode" | "gemini" | "codex" | "auto";
117
+
118
+ /** Result of an install operation. */
119
+ export interface InstallResult {
120
+ packageName: string;
121
+ target: InstallTarget;
122
+ command: string;
123
+ success: boolean;
124
+ message: string;
125
+ }
126
+
127
+ /** Keywords that mark packages for each ecosystem on npm. */
128
+ export const ECOSYSTEM_KEYWORDS: Record<Exclude<Ecosystem, "unknown" | "universal">, string[]> = {
129
+ pi: ["pi-package"],
130
+ omp: ["pi-package"],
131
+ claude: ["claude-code", "claude-code-plugin", "cc-plugin"],
132
+ opencode: ["opencode", "opencode-plugin"],
133
+ gemini: ["gemini-cli", "gemini-extension", "gemini-cli-extension"],
134
+ codex: ["codex", "codex-plugin", "codex-cli"],
135
+ };