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,15 @@
1
+ {
2
+ "name": "zmarketplace",
3
+ "version": "0.3.0",
4
+ "description": "Cross-agent marketplace search: find, audit, and install packages across pi, omp, claude code, opencode, gemini cli, and codex",
5
+ "author": { "name": "zico20047" },
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "marketplace",
9
+ "search",
10
+ "install",
11
+ "audit",
12
+ "cross-agent"
13
+ ],
14
+ "defaultEnabled": true
15
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "zmarketplace",
3
+ "version": "0.3.0",
4
+ "description": "Cross-agent marketplace search: find, audit, and install packages across pi, omp, claude code, opencode, gemini cli, and codex",
5
+ "license": "MIT"
6
+ }
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # zmarketplace
2
+
3
+ Cross-agent marketplace search plugin. Type `/zmarketplace` in pi/omp to search, inspect, audit, and install packages across npm, Claude marketplace, and Gemini extensions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # pi / omp (slash command)
9
+ pi install npm:zmarketplace
10
+ # or link locally:
11
+ omp plugin link ./search_marketplace
12
+
13
+ # then in omp TUI:
14
+ /reload
15
+ /zmarketplace search mcp
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```
21
+ /zmarketplace search <query> [--type=<type>] [--eco=<ecosystem>] [--limit=<n>]
22
+ /zmarketplace detail <id|name>
23
+ /zmarketplace audit <id|name>
24
+ /zmarketplace install <id|name>
25
+ ```
26
+
27
+ ### Search
28
+
29
+ ```
30
+ /zmarketplace search mcp
31
+ /zmarketplace search subagent --eco=pi
32
+ /zmarketplace search theme --type=theme --limit=10
33
+ ```
34
+
35
+ Opens an interactive select list. Scroll up/down, type to filter, enter to pick.
36
+
37
+ ### Detail + README
38
+
39
+ Pick a package → select "📋 Details + README" → full README in a scrollable list.
40
+
41
+ - **Enter on a URL line** (🖼 IMAGE, 🔗 link) → opens in your browser
42
+ - **Enter on text** → stays in detail view
43
+ - **Type to search** → filters README lines
44
+ - **ESC or "Back to menu"** → returns to action menu
45
+
46
+ ### Install
47
+
48
+ Pick "⬇ Install" → choose ecosystem:
49
+
50
+ | Option | Command |
51
+ |---|---|
52
+ | 🥧 pi install | `pi install npm:<name>` |
53
+ | ⌥ omp install | `omp plugin install npm:<name>` |
54
+ | 🤖 claude install | `claude plugin install npm:<name>` |
55
+ | 🔓 opencode install | `opencode plugin <name>` |
56
+ | 💎 gemini install | `gemini extension install <url>` |
57
+ | 🔲 codex install | `codex plugin add npm:<name>` |
58
+ | 📦 npm install | `npm install <name>` |
59
+ | ⚡ bunx | `bunx <name>` |
60
+
61
+ Quick security check runs before showing the command. High-risk packages require confirmation.
62
+
63
+ ### Audit
64
+
65
+ ```
66
+ /zmarketplace audit pi-mcp-adapter
67
+ ```
68
+
69
+ Two-layer security scan:
70
+ - **Layer 1 (metadata):** dependency count, file count, size, license
71
+ - **Layer 2 (source):** downloads tarball, scans `.ts`/`.js` for dangerous patterns (`rm -rf`, `eval()`, `execSync()`, `child_process`, etc.)
72
+
73
+ ## Registries
74
+
75
+ | Source | Coverage |
76
+ |---|---|
77
+ | npm | `pi-package`, `claude-code`, `opencode`, `gemini-cli`, `codex` keywords |
78
+ | Claude marketplace | `anthropics/claude-plugins-official` + community (~800+ plugins) |
79
+ | Gemini extensions | `geminicli.com/extensions.json` (~993 extensions) |
80
+
81
+ ## CLI (standalone, no agent needed)
82
+
83
+ ```bash
84
+ bunx zmarketplace search "mcp" --limit=5
85
+ bunx zmarketplace detail pi-marketplace
86
+ bunx zmarketplace audit pi-marketplace
87
+ bunx zmarketplace install pi-marketplace
88
+ ```
89
+
90
+ ## Architecture
91
+
92
+ ```
93
+ src/
94
+ ├── index.ts ← pi/omp extension: registerCommand("/zmarketplace")
95
+ ├── cli.ts ← standalone CLI: bunx zmarketplace
96
+ ├── opencode.ts ← opencode plugin entry
97
+ ├── core/
98
+ │ ├── types.ts ← unified package model
99
+ │ ├── search.ts ← cross-registry search + dedup + ranking
100
+ │ ├── detail.ts ← npm metadata + README fetch
101
+ │ ├── audit.ts ← 2-layer security scanner
102
+ │ ├── install.ts ← agent detection + install command dispatch
103
+ │ ├── cache.ts ← results cache for ID-based reference
104
+ │ └── tui.ts ← icons, formatting, arg parser
105
+ └── registries/
106
+ ├── npm.ts ← npm registry (parallel per-keyword queries)
107
+ ├── claude.ts ← Claude marketplace JSON
108
+ └── gemini.ts ← Gemini CLI extensions registry
109
+ ```
110
+
111
+ ## Cross-agent manifests
112
+
113
+ | File | Ecosystem |
114
+ |---|---|
115
+ | `package.json` (`pi.extensions`, `omp.extensions`) | Pi, OMP |
116
+ | `package.json` (`bin`, `exports`) | CLI, OpenCode |
117
+ | `.claude-plugin/plugin.json` | Claude Code |
118
+ | `gemini-extension.json` | Gemini CLI |
119
+ | `.codex-plugin/plugin.json` | Codex |
120
+
121
+ ## Support
122
+
123
+ If this plugin helped you, consider supporting:
124
+
125
+ ☕ [ko-fi.com/zicodev](https://ko-fi.com/zicodev)
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "zmarketplace",
3
+ "version": "0.3.0",
4
+ "description": "Cross-agent marketplace search: find, audit, and install packages across pi, omp, claude code, opencode, gemini cli, and codex",
5
+ "url": "https://github.com/zico20047/zmarketplace",
6
+ "contextFileName": "zmarketplace"
7
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "zmarketplace",
3
+ "version": "0.4.2",
4
+ "description": "Cross-agent marketplace search: find, audit, and install plugins/skills/themes/prompts across pi, omp, claude code, opencode, gemini cli, and codex",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "zmarketplace": "./src/cli.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ ".claude-plugin",
14
+ ".codex-plugin",
15
+ "gemini-extension.json"
16
+ ],
17
+ "keywords": [
18
+ "pi-package",
19
+ "claude-code",
20
+ "opencode",
21
+ "gemini-cli",
22
+ "codex",
23
+ "marketplace",
24
+ "plugin",
25
+ "search",
26
+ "install",
27
+ "audit"
28
+ ],
29
+ "pi": {
30
+ "extensions": ["./src/index.ts"]
31
+ },
32
+ "omp": {
33
+ "extensions": ["./src/index.ts"]
34
+ },
35
+ "exports": {
36
+ ".": "./src/index.ts",
37
+ "./server": "./src/opencode.ts"
38
+ },
39
+ "scripts": {
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "bun run test/full-test.ts"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5",
45
+ "@types/bun": "^1.3.14"
46
+ },
47
+ "engines": {
48
+ "bun": ">=1.1.0"
49
+ }
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CLI entry point — standalone command for `bunx zmarketplace`.
4
+ * Works without any agent runtime. Used by Claude Code commands/,
5
+ * Gemini CLI, and Codex.
6
+ *
7
+ * Usage:
8
+ * bunx zmarketplace search "mcp" --eco=pi --limit=10
9
+ * bunx zmarketplace detail pi-marketplace
10
+ * bunx zmarketplace audit pi-marketplace
11
+ * bunx zmarketplace install pi-marketplace
12
+ */
13
+
14
+ import { search } from "./core/search.ts";
15
+ import { getDetail } from "./core/detail.ts";
16
+ import { auditPackage } from "./core/audit.ts";
17
+ import { installPackage } from "./core/install.ts";
18
+ import { formatResultOption, formatDetailCard, formatAuditReport, formatHelp, parseArgs } from "./core/tui.ts";
19
+ import type { SearchOptions } from "./core/types.ts";
20
+
21
+ async function main(): Promise<void> {
22
+ const args = process.argv.slice(2);
23
+ const parsed = parseArgs(args);
24
+
25
+ switch (parsed.subcommand) {
26
+ case "":
27
+ case "help": {
28
+ console.log(formatHelp());
29
+ break;
30
+ }
31
+
32
+ case "search":
33
+ case "s": {
34
+ const query = parsed.positional.join(" ").trim();
35
+ const opts: SearchOptions = {
36
+ query,
37
+ type: (parsed.flags["type"] as SearchOptions["type"]) ?? "all",
38
+ ecosystem: (parsed.flags["eco"] as SearchOptions["ecosystem"]) ?? "all",
39
+ limit: parseInt(parsed.flags["limit"] ?? "20", 10),
40
+ };
41
+
42
+ console.log(`Searching for "${query || "all"}"...`);
43
+ const results = await search(opts);
44
+
45
+ if (results.length === 0) {
46
+ console.log("No packages found.");
47
+ break;
48
+ }
49
+
50
+ console.log(`\n${results.length} package(s) found:\n`);
51
+ for (let i = 0; i < results.length; i++) {
52
+ const opt = formatResultOption(results[i], i);
53
+ console.log(` ${opt.label}`);
54
+ if (results[i].installCommand) {
55
+ console.log(` → ${results[i].installCommand}`);
56
+ }
57
+ }
58
+ break;
59
+ }
60
+
61
+ case "detail":
62
+ case "d":
63
+ case "info": {
64
+ const name = parsed.positional[0];
65
+ if (!name) {
66
+ console.error("Error: package name required. Usage: zmarketplace detail <name>");
67
+ process.exit(1);
68
+ }
69
+
70
+ const detail = await getDetail(name);
71
+ if (!detail) {
72
+ console.error(`Package "${name}" not found.`);
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log(formatDetailCard(detail));
77
+ break;
78
+ }
79
+
80
+ case "audit":
81
+ case "a": {
82
+ const name = parsed.positional[0];
83
+ if (!name) {
84
+ console.error("Error: package name required. Usage: zmarketplace audit <name>");
85
+ process.exit(1);
86
+ }
87
+
88
+ console.log(`Auditing ${name} (downloading source)...`);
89
+ const report = await auditPackage(name, { deepScan: parsed.flags["deepScan"] !== "false" });
90
+ console.log("\n" + formatAuditReport(report));
91
+ break;
92
+ }
93
+
94
+ case "install":
95
+ case "i": {
96
+ const name = parsed.positional[0];
97
+ if (!name) {
98
+ console.error("Error: package name required. Usage: zmarketplace install <name>");
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log(`Auditing ${name} before install...`);
103
+ const report = await auditPackage(name, { deepScan: true });
104
+ console.log("\n" + formatAuditReport(report));
105
+
106
+ if (report.risk === "critical" || report.risk === "high") {
107
+ console.error(`\n⚠️ HIGH RISK: ${name} has ${report.risk} security issues. Review findings above.`);
108
+ console.error("If you still want to install, run the command manually.");
109
+ process.exit(1);
110
+ }
111
+
112
+ const result = await installPackage(name, { skipAudit: true });
113
+ console.log(`\n✅ Ready to install:`);
114
+ console.log(` ${result.command}`);
115
+ break;
116
+ }
117
+
118
+ default:
119
+ console.error(`Unknown subcommand: ${parsed.subcommand}\n`);
120
+ console.log(formatHelp());
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ main().catch(err => {
126
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
127
+ process.exit(1);
128
+ });
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Security audit — two-layer scan of a package before installation.
3
+ * Layer 1: metadata check (zero cost).
4
+ * Layer 2: source code scan (downloads tarball, scans for dangerous patterns).
5
+ */
6
+
7
+ import type { AuditReport, AuditFinding, AuditSeverity } from "./types.ts";
8
+ import { getNpmPackageMeta } from "../registries/npm.ts";
9
+
10
+ // Danger patterns ordered by severity.
11
+ const CRITICAL_PATTERNS: Array<[RegExp, string]> = [
12
+ [/rm\s+-rf/i, "Recursive forced deletion command"],
13
+ [/\brimraf\b/i, "Recursive directory deletion library"],
14
+ [/fs\.unlink(Sync)?\s*\(/g, "File deletion"],
15
+ [/fs\.rmdir(Sync)?\s*\(/g, "Directory deletion"],
16
+ [/fs\.rm(Sync)?\s*\(/g, "File/directory removal"],
17
+ ];
18
+
19
+ const HIGH_PATTERNS: Array<[RegExp, string]> = [
20
+ [/\beval\s*\(/g, "Dynamic code execution via eval()"],
21
+ [/\bnew\s+Function\s*\(/g, "Dynamic code execution via Function()"],
22
+ [/execSync\s*\(/g, "Synchronous command execution"],
23
+ [/execFile(Sync)?\s*\(/g, "Child process execution"],
24
+ [/\bspawn(Sync)?\s*\(/g, "Process spawning"],
25
+ ];
26
+
27
+ const MEDIUM_PATTERNS: Array<[RegExp, string]> = [
28
+ [/process\.env\b/g, "Environment variable access"],
29
+ [/child_process/g, "Child process module import"],
30
+ [/\bfetch\s*\(/g, "HTTP fetch call"],
31
+ [/\bXMLHttpRequest\b/g, "HTTP request"],
32
+ [/https?:\/\/[^'")\s]+/g, "Hardcoded URL"],
33
+ ];
34
+
35
+ const LOW_PATTERNS: Array<[RegExp, string]> = [
36
+ [/fs\.chmod(Sync)?\s*\(/g, "File permission change"],
37
+ [/fs\.chown(Sync)?\s*\(/g, "File ownership change"],
38
+ ];
39
+
40
+ const ALL_PATTERN_LAYERS: Array<[AuditSeverity, Array<[RegExp, string]>]> = [
41
+ ["critical", CRITICAL_PATTERNS],
42
+ ["high", HIGH_PATTERNS],
43
+ ["medium", MEDIUM_PATTERNS],
44
+ ["low", LOW_PATTERNS],
45
+ ];
46
+
47
+ const SEVERITY_WEIGHT: Record<AuditSeverity, number> = {
48
+ critical: 100,
49
+ high: 25,
50
+ medium: 5,
51
+ low: 1,
52
+ info: 0,
53
+ };
54
+
55
+ const SCANABLE_EXTENSIONS = new Set([".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx"]);
56
+
57
+ /** Scan a source file string for dangerous patterns. */
58
+ function scanSource(content: string, fileName: string): AuditFinding[] {
59
+ const lines = content.split("\n");
60
+ const findings: AuditFinding[] = [];
61
+
62
+ for (const [severity, patterns] of ALL_PATTERN_LAYERS) {
63
+ for (const [regex, reason] of patterns) {
64
+ regex.lastIndex = 0;
65
+ let match: RegExpExecArray | null;
66
+ while ((match = regex.exec(content)) !== null) {
67
+ // Find line number for the match offset
68
+ const offset = match.index;
69
+ const before = content.slice(0, offset);
70
+ const lineNum = before.split("\n").length;
71
+ const lineText = lines[lineNum - 1]?.trim() ?? "";
72
+
73
+ findings.push({
74
+ severity,
75
+ pattern: match[0],
76
+ file: fileName,
77
+ line: lineNum,
78
+ excerpt: lineText.slice(0, 200),
79
+ reason,
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ return findings;
86
+ }
87
+
88
+ /** Compute overall risk from findings. */
89
+ function computeRisk(findings: AuditFinding[]): AuditReport["risk"] {
90
+ const score = findings.reduce((sum, f) => sum + SEVERITY_WEIGHT[f.severity], 0);
91
+ if (score >= 100) return "critical";
92
+ if (score >= 50) return "high";
93
+ if (score >= 15) return "moderate";
94
+ if (score >= 5) return "low";
95
+ return "safe";
96
+ }
97
+
98
+ /** Run a full audit on a package. */
99
+ export async function auditPackage(
100
+ packageName: string,
101
+ options: { deepScan?: boolean } = {},
102
+ ): Promise<AuditReport> {
103
+ const deepScan = options.deepScan ?? true;
104
+ const meta = await getNpmPackageMeta(packageName);
105
+ const latestVersion = meta?.["dist-tags"]?.latest;
106
+
107
+ // Layer 1: Metadata check
108
+ const metadataFindings: AuditFinding[] = [];
109
+ if (meta && latestVersion) {
110
+ const versionData = meta.versions[latestVersion];
111
+ const depCount = Object.keys(versionData?.dependencies ?? {}).length;
112
+ const size = versionData?.dist?.unpackedSize ?? 0;
113
+ const fileCount = versionData?.dist?.fileCount ?? 0;
114
+
115
+ if (depCount > 20) {
116
+ metadataFindings.push({
117
+ severity: "medium",
118
+ pattern: `${depCount} dependencies`,
119
+ reason: "High dependency count increases supply chain risk",
120
+ });
121
+ }
122
+ if (size > 10 * 1024 * 1024) {
123
+ metadataFindings.push({
124
+ severity: "low",
125
+ pattern: `${(size / 1024 / 1024).toFixed(1)} MB`,
126
+ reason: "Large package size",
127
+ });
128
+ }
129
+ if (fileCount > 500) {
130
+ metadataFindings.push({
131
+ severity: "low",
132
+ pattern: `${fileCount} files`,
133
+ reason: "Large file count",
134
+ });
135
+ }
136
+ if (!versionData?.license) {
137
+ metadataFindings.push({
138
+ severity: "low",
139
+ pattern: "no license",
140
+ reason: "No license declared — usage rights unclear",
141
+ });
142
+ }
143
+ }
144
+
145
+ // Layer 2: Source scan
146
+ let sourceFindings: AuditFinding[] = [];
147
+
148
+ if (deepScan && latestVersion) {
149
+ try {
150
+ const tarballUrl = meta?.versions[latestVersion]?.dist?.tarball;
151
+ if (tarballUrl) {
152
+ const resp = await fetch(tarballUrl, { signal: AbortSignal.timeout(30000) });
153
+ if (resp.ok) {
154
+ const tarball = new Uint8Array(await resp.arrayBuffer());
155
+ const files = extractTextFilesFromTar(tarball);
156
+ sourceFindings = files.flatMap(({ name, content }) => scanSource(content, name));
157
+ }
158
+ }
159
+ } catch {
160
+ sourceFindings.push({
161
+ severity: "info",
162
+ pattern: "scan failed",
163
+ reason: "Could not download or extract tarball for source scanning",
164
+ });
165
+ }
166
+ }
167
+
168
+ const allFindings = [...metadataFindings, ...sourceFindings];
169
+ const risk = computeRisk(allFindings);
170
+
171
+ const summaryParts: string[] = [];
172
+ const counts: Partial<Record<AuditSeverity, number>> = {};
173
+ for (const f of allFindings) {
174
+ counts[f.severity] = (counts[f.severity] ?? 0) + 1;
175
+ }
176
+ const order: AuditSeverity[] = ["critical", "high", "medium", "low", "info"];
177
+ for (const sev of order) {
178
+ if (counts[sev]) summaryParts.push(`${counts[sev]} ${sev}`);
179
+ }
180
+
181
+ return {
182
+ packageName,
183
+ version: latestVersion,
184
+ risk,
185
+ metadataFindings,
186
+ sourceFindings,
187
+ findings: allFindings,
188
+ deepScanned: deepScan,
189
+ summary: `Risk: ${risk.toUpperCase()}. Findings: ${summaryParts.join(", ") || "none"}. ${deepScan ? "Source scanned." : "Metadata only."}`,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Minimal tar text-file extractor. Bun can handle .tar natively,
195
+ * but we do a pure-JS fallback to avoid loading heavy deps for large tarballs.
196
+ * Extracts only text files (.ts/.js/.mjs/.cjs/.tsx/.jsx).
197
+ */
198
+ function extractTextFilesFromTar(tarball: Uint8Array): Array<{ name: string; content: string }> {
199
+ const results: Array<{ name: string; content: string }> = [];
200
+ const decoder = new TextDecoder("utf-8", { fatal: false });
201
+
202
+ let offset = 0;
203
+ while (offset + 512 <= tarball.length) {
204
+ // Read header block (512 bytes)
205
+ const header = tarball.subarray(offset, offset + 512);
206
+
207
+ // Check for end-of-archive (two zero blocks)
208
+ if (header.every(b => b === 0)) break;
209
+
210
+ // Parse name (offset 0, 100 bytes)
211
+ const name = decoder.decode(header.subarray(0, 100)).replace(/\0/g, "").trim();
212
+
213
+ // Parse size (offset 124, 12 bytes, octal)
214
+ const sizeStr = decoder.decode(header.subarray(124, 136)).replace(/\0/g, "").trim();
215
+ const size = sizeStr ? parseInt(sizeStr, 8) : 0;
216
+
217
+ // Parse type flag (offset 156, 1 byte) — '0' or '\0' = regular file
218
+ const typeFlag = String.fromCharCode(header[156] ?? 0);
219
+
220
+ offset += 512;
221
+
222
+ if ((typeFlag === "0" || typeFlag === "\0") && size > 0) {
223
+ // Check if it's a scanable file
224
+ const ext = name.substring(name.lastIndexOf("."));
225
+ if (SCANABLE_EXTENSIONS.has(ext)) {
226
+ const fileContent = tarball.subarray(offset, offset + size);
227
+ results.push({
228
+ name,
229
+ content: decoder.decode(fileContent),
230
+ });
231
+ }
232
+ }
233
+
234
+ // Advance past file data (padded to 512-byte boundary)
235
+ offset += Math.ceil(size / 512) * 512;
236
+ }
237
+
238
+ return results;
239
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Results cache — stores the last search so IDs persist between commands.
3
+ * `/zmarketplace install 3` refers to the 3rd result from the last search.
4
+ */
5
+
6
+ import type { PackageResult, AuditReport } from "./types.ts";
7
+
8
+ let lastResults: PackageResult[] = [];
9
+ let lastQuery = "";
10
+
11
+ /** Store search results. */
12
+ export function cacheResults(results: PackageResult[], query: string): void {
13
+ lastResults = results;
14
+ lastQuery = query;
15
+ }
16
+
17
+ /** Get cached results. */
18
+ export function getCachedResults(): PackageResult[] {
19
+ return lastResults;
20
+ }
21
+
22
+ /** Get the last query string. */
23
+ export function getLastQuery(): string {
24
+ return lastQuery;
25
+ }
26
+
27
+ /** Resolve a reference (ID number or package name) to a cached result. */
28
+ export function resolveRef(ref: string): PackageResult | undefined {
29
+ // Try as numeric ID (1-based)
30
+ const id = parseInt(ref, 10);
31
+ if (!isNaN(id) && id >= 1 && id <= lastResults.length) {
32
+ return lastResults[id - 1];
33
+ }
34
+ // Try as package name (case-insensitive)
35
+ return lastResults.find(r => r.name.toLowerCase() === ref.toLowerCase());
36
+ }
37
+
38
+ /** Cache for audit reports by package name. */
39
+ const auditCache = new Map<string, AuditReport>();
40
+
41
+ export function cacheAudit(name: string, report: AuditReport): void {
42
+ auditCache.set(name.toLowerCase(), report);
43
+ }
44
+
45
+ export function getCachedAudit(name: string): AuditReport | undefined {
46
+ return auditCache.get(name.toLowerCase());
47
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Package detail fetcher — retrieves full metadata, README, and manifest info.
3
+ */
4
+
5
+ import type { PackageDetail } from "./types.ts";
6
+ import { getNpmPackageMeta } from "../registries/npm.ts";
7
+
8
+ /** Fetch detailed package information from npm + any available manifests. */
9
+ export async function getDetail(packageName: string): Promise<PackageDetail | null> {
10
+ const meta = await getNpmPackageMeta(packageName);
11
+ if (!meta) return null;
12
+
13
+ const latestVersion = meta["dist-tags"]?.latest;
14
+ if (!latestVersion) return null;
15
+
16
+ const versionData = meta.versions[latestVersion];
17
+ if (!versionData) return null;
18
+
19
+ const repoUrl = typeof versionData.repository === "string"
20
+ ? versionData.repository
21
+ : versionData.repository?.url;
22
+
23
+ const dependencies = versionData.dependencies ?? {};
24
+ const depCount = Object.keys(dependencies).length;
25
+
26
+ return {
27
+ name: packageName,
28
+ description: meta.description ?? versionData.description ?? "",
29
+ version: latestVersion,
30
+ source: "npm",
31
+ ecosystems: [],
32
+ type: "unknown",
33
+ readme: meta.readme,
34
+ dependencyCount: depCount,
35
+ size: versionData.dist?.unpackedSize,
36
+ fileCount: versionData.dist?.fileCount,
37
+ license: versionData.license,
38
+ homepage: versionData.homepage ?? repoUrl,
39
+ repository: repoUrl,
40
+ npmUrl: `https://www.npmjs.com/package/${packageName}`,
41
+ keywords: versionData.keywords ?? [],
42
+ publishedAt: meta.time?.[latestVersion],
43
+ };
44
+ }