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.
- package/.claude-plugin/plugin.json +15 -0
- package/.codex-plugin/plugin.json +6 -0
- package/README.md +129 -0
- package/gemini-extension.json +7 -0
- package/package.json +50 -0
- package/src/cli.ts +128 -0
- package/src/core/audit.ts +239 -0
- package/src/core/cache.ts +47 -0
- package/src/core/detail.ts +44 -0
- package/src/core/install.ts +109 -0
- package/src/core/search.ts +109 -0
- package/src/core/tui.ts +176 -0
- package/src/core/types.ts +135 -0
- package/src/index.ts +255 -0
- package/src/opencode.ts +119 -0
- package/src/registries/claude.ts +80 -0
- package/src/registries/gemini.ts +89 -0
- package/src/registries/mcp.ts +77 -0
- package/src/registries/npm.ts +166 -0
|
@@ -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
|
+
}
|
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
|
+
}
|