@oomerevren/tryforge 0.1.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +361 -0
  4. package/dist/cli/src/adapters/claude.js +31 -0
  5. package/dist/cli/src/adapters/codex.js +30 -0
  6. package/dist/cli/src/adapters/cursor.js +42 -0
  7. package/dist/cli/src/adapters/dsh.js +46 -0
  8. package/dist/cli/src/adapters/generic.js +23 -0
  9. package/dist/cli/src/adapters/index.js +30 -0
  10. package/dist/cli/src/adapters/opencode.js +41 -0
  11. package/dist/cli/src/adapters/types.js +140 -0
  12. package/dist/cli/src/adapters/windsurf.js +43 -0
  13. package/dist/cli/src/commands/audit.js +62 -0
  14. package/dist/cli/src/commands/init.js +104 -0
  15. package/dist/cli/src/commands/install.js +212 -0
  16. package/dist/cli/src/commands/update.js +119 -0
  17. package/dist/cli/src/core/config.js +47 -0
  18. package/dist/cli/src/core/installer.js +279 -0
  19. package/dist/cli/src/core/lock.js +41 -0
  20. package/dist/cli/src/core/permissions.js +29 -0
  21. package/dist/cli/src/core/plugin.js +34 -0
  22. package/dist/cli/src/core/project.js +60 -0
  23. package/dist/cli/src/core/registry.js +137 -0
  24. package/dist/cli/src/core/semver.js +162 -0
  25. package/dist/cli/src/core/sign.js +92 -0
  26. package/dist/cli/src/core/store.js +71 -0
  27. package/dist/cli/src/index.js +484 -0
  28. package/dist/scripts/build-registry.js +145 -0
  29. package/dist/scripts/publish-verified.js +82 -0
  30. package/dist/scripts/seed-registry-13lite.js +208 -0
  31. package/dist/scripts/seed-registry.js +144 -0
  32. package/dist/scripts/verify-npm-mcps.js +60 -0
  33. package/forge.toml +18 -0
  34. package/install.ps1 +59 -0
  35. package/install.sh +81 -0
  36. package/package.json +64 -0
  37. package/registry/index.json +336 -0
  38. package/registry/packages/agent-changelog-writer.json +27 -0
  39. package/registry/packages/agent-debugger.json +27 -0
  40. package/registry/packages/agent-pr-reviewer.json +27 -0
  41. package/registry/packages/agent-researcher.json +27 -0
  42. package/registry/packages/agent-security-auditor.json +27 -0
  43. package/registry/packages/cmd-plan.json +26 -0
  44. package/registry/packages/cmd-review.json +26 -0
  45. package/registry/packages/mcp-filesystem.json +34 -0
  46. package/registry/packages/mcp-github.json +33 -0
  47. package/registry/packages/mcp-memory.json +33 -0
  48. package/registry/packages/mcp-postgres.json +34 -0
  49. package/registry/packages/mcp-sequential-thinking.json +33 -0
  50. package/registry/packages/obra-superpowers.json +29 -0
  51. package/registry/packages/pdf-compress.json +27 -0
  52. package/registry/packages/pdf-convert.json +27 -0
  53. package/registry/packages/pdf-extract.json +27 -0
  54. package/registry/packages/pdf-forms.json +27 -0
  55. package/registry/packages/pdf-merge.json +27 -0
  56. package/registry/packages/pdf-ocr.json +27 -0
  57. package/registry/packages/pdf-split.json +27 -0
  58. package/registry/packages/pdf-tables.json +27 -0
  59. package/registry/search.json +248 -0
  60. package/registry/stats.json +13 -0
@@ -0,0 +1,137 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { toSlug } from "./store.js";
4
+ import { maxSatisfying } from "./semver.js";
5
+ import { loadConfig } from "./config.js";
6
+ function registryRoot() {
7
+ // Epoch 1d: config.registry desteği — uzak index URL'si kullanıcı tarafından belirtilebilir
8
+ try {
9
+ const cfg = loadConfig();
10
+ if (cfg.registry && /^https?:\/\//.test(cfg.registry)) {
11
+ return cfg.registry;
12
+ }
13
+ }
14
+ catch { }
15
+ const candidates = [
16
+ join(import.meta.dirname ?? "./", "../../../registry"),
17
+ join(import.meta.dirname ?? "./", "../../../../registry"),
18
+ join(process.cwd(), "registry"),
19
+ ];
20
+ for (const c of candidates) {
21
+ if (existsSync(join(c, "index.json")))
22
+ return c;
23
+ }
24
+ return join(import.meta.dirname ?? "./", "../../../registry");
25
+ }
26
+ export async function loadIndex() {
27
+ const root = registryRoot();
28
+ // Epoch 1d: uzak registry URL'si desteği
29
+ if (/^https?:\/\//.test(root)) {
30
+ try {
31
+ const res = await fetch(root + "/index.json");
32
+ if (!res.ok)
33
+ throw new Error(`HTTP ${res.status} for ${root}/index.json`);
34
+ return await res.json();
35
+ }
36
+ catch (e) {
37
+ throw new Error(`Remote registry fetch failed: ${e.message}`);
38
+ }
39
+ }
40
+ const p = join(root, "index.json");
41
+ if (!existsSync(p))
42
+ throw new Error(`Registry index not found: ${p}`);
43
+ return JSON.parse(readFileSync(p, "utf-8"));
44
+ }
45
+ export async function loadPackageDetail(pkg) {
46
+ const root = registryRoot();
47
+ const slug = toSlug(pkg);
48
+ // Epoch 1d: uzak registry URL'si desteği
49
+ if (/^https?:\/\//.test(root)) {
50
+ try {
51
+ const res = await fetch(`${root}/packages/${slug}.json`);
52
+ if (!res.ok)
53
+ throw new Error(`HTTP ${res.status} for ${root}/packages/${slug}.json`);
54
+ return await res.json();
55
+ }
56
+ catch (e) {
57
+ throw new Error(`Package not found: ${pkg} (${e.message})`);
58
+ }
59
+ }
60
+ const p = join(root, "packages", `${slug}.json`);
61
+ if (!existsSync(p))
62
+ throw new Error(`Package not found: ${pkg} (${p})`);
63
+ return JSON.parse(readFileSync(p, "utf-8"));
64
+ }
65
+ export async function resolveVersion(pkg, requested) {
66
+ const detail = await loadPackageDetail(pkg);
67
+ if (!requested || requested === "latest" || requested === "*") {
68
+ const v = detail.latest;
69
+ const meta = detail.versions[v];
70
+ if (!meta)
71
+ throw new Error(`Latest version ${v} not found for ${pkg}`);
72
+ return { detail, version: v, versionMeta: meta };
73
+ }
74
+ // exact version
75
+ if (detail.versions[requested]) {
76
+ return { detail, version: requested, versionMeta: detail.versions[requested] };
77
+ }
78
+ // semver range (Faz 10: semver.ts tek kaynak) — pick highest satisfying
79
+ const picked = maxSatisfying(Object.keys(detail.versions), requested);
80
+ if (!picked)
81
+ throw new Error(`No version satisfying ${requested} for ${pkg}. Available: ${Object.keys(detail.versions).join(", ")}`);
82
+ return { detail, version: picked, versionMeta: detail.versions[picked] };
83
+ }
84
+ export function parsePackageArg(arg) {
85
+ // anthropics/plan@1.2.0 or anthropics/plan@^1.0.0 or anthropics/plan
86
+ const at = arg.lastIndexOf("@");
87
+ // need to distinguish scope/name@ver vs @ver — scope always contains /
88
+ // So if arg is "pkg@ver", at > 0 and slash before at
89
+ if (at > 0 && arg.slice(0, at).includes("/")) {
90
+ return { name: arg.slice(0, at), version: arg.slice(at + 1) };
91
+ }
92
+ return { name: arg };
93
+ }
94
+ // --- semver helpers Faz 10'da semver.ts'e taşındı (tek kaynak) ---
95
+ // Bu dosya geriye dönük uyumluluk için re-export eder.
96
+ export { parseSemver, compareSemver, satisfiesRange, maxSatisfying, isValidRange } from "./semver.js";
97
+ export async function searchPackages(query, opts = {}) {
98
+ const index = await loadIndex();
99
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
100
+ if (tokens.length === 0)
101
+ return [];
102
+ const scored = [];
103
+ for (const p of Object.values(index.packages)) {
104
+ if (opts.type && p.type !== opts.type)
105
+ continue;
106
+ const name = p.name.toLowerCase();
107
+ const desc = (p.description ?? "").toLowerCase();
108
+ const kw = (p.keywords ?? []).join(" ").toLowerCase();
109
+ const type = (p.type ?? "").toLowerCase();
110
+ let score = 0;
111
+ for (const t of tokens) {
112
+ if (name === t)
113
+ score += 50;
114
+ else if (name.startsWith(t))
115
+ score += 15;
116
+ else if (name.includes(t))
117
+ score += 10;
118
+ if (kw.split(/\s+/).includes(t))
119
+ score += 8;
120
+ else if (kw.includes(t))
121
+ score += 5;
122
+ if (type === t)
123
+ score += 6;
124
+ if (desc.includes(t))
125
+ score += 2;
126
+ }
127
+ if (score > 0) {
128
+ // Verified tarballs rank first on ties — first touch should install cleanly.
129
+ if (p.verified)
130
+ score += 4;
131
+ scored.push({ p, score });
132
+ }
133
+ }
134
+ scored.sort((a, b) => b.score - a.score || a.p.name.localeCompare(b.p.name));
135
+ const out = scored.map((s) => s.p);
136
+ return typeof opts.limit === "number" ? out.slice(0, opts.limit) : out;
137
+ }
@@ -0,0 +1,162 @@
1
+ // cli/src/core/semver.ts — Epoch 1c: tam semver (^ ~ >= > <= < * exact + kompozit + x-range + pre-release)
2
+ // Tek kaynak (single source of truth) — registry.ts ve project.ts burayı kullanır.
3
+ export function parseSemver(v) {
4
+ const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-.]?([a-zA-Z0-9.]+))?/);
5
+ if (!m)
6
+ return { major: 0, minor: 0, patch: 0 };
7
+ return {
8
+ major: parseInt(m[1], 10),
9
+ minor: m[2] !== undefined ? parseInt(m[2], 10) : 0,
10
+ patch: m[3] !== undefined ? parseInt(m[3], 10) : 0,
11
+ pre: m[4],
12
+ };
13
+ }
14
+ export function compareSemver(a, b) {
15
+ const pa = parseSemver(a);
16
+ const pb = parseSemver(b);
17
+ if (pa.major !== pb.major)
18
+ return pa.major - pb.major;
19
+ if (pa.minor !== pb.minor)
20
+ return pa.minor - pb.minor;
21
+ if (pa.patch !== pb.patch)
22
+ return pa.patch - pb.patch;
23
+ // Pre-release: 1.2.3-beta < 1.2.3 (SemVer spec)
24
+ if (pa.pre && !pb.pre)
25
+ return -1;
26
+ if (!pa.pre && pb.pre)
27
+ return 1;
28
+ if (pa.pre && pb.pre)
29
+ return pa.pre.localeCompare(pb.pre);
30
+ return 0;
31
+ }
32
+ export function isValidRange(r) {
33
+ const t = r.trim();
34
+ if (t === "*" || t === "" || t === "latest")
35
+ return true;
36
+ // Exact: 1.2.0, v1.2.0
37
+ if (/^v?\d+\.\d+\.\d+/.test(t))
38
+ return true;
39
+ // ^ + full semver: ^1.2.0, ^0.2.3
40
+ if (/^\^v?\d+\.\d+\.\d+/.test(t))
41
+ return true;
42
+ // ^ + partial: ^1.2, ^1
43
+ if (/^\^\d+(\.\d+)?$/.test(t))
44
+ return true;
45
+ // ~ + full semver: ~1.2.3, ~0.1.0
46
+ if (/^~v?\d+\.\d+\.\d+/.test(t))
47
+ return true;
48
+ // x-range: 1.x, 1.2.x, 1.*, 1.2.*
49
+ if (/^\d+\.(\*|x)$/.test(t))
50
+ return true;
51
+ if (/^\d+\.\d+\.(\*|x)$/.test(t))
52
+ return true;
53
+ // Kısmi aralıklar: ^1, ~1, ~1.2
54
+ if (/^\^\d+$/.test(t))
55
+ return true;
56
+ if (/^~\d+$/.test(t))
57
+ return true;
58
+ if (/^~\d+\.\d+$/.test(t))
59
+ return true;
60
+ // Tek operatörler: >=1.0.0, <2.0.0, =1.2.3
61
+ if (/^(>=|<=|>|<|=)\s*v?\d+\.\d+\.\d+/.test(t))
62
+ return true;
63
+ // Kompozit aralıklar: >=1.2.0 <2.0.0
64
+ if (/^(>=|<=|>|<)\s*v?\d+\.\d+\.\d+\s+(>=|<=|>|<)\s*v?\d+\.\d+\.\d+/.test(t))
65
+ return true;
66
+ return false;
67
+ }
68
+ export function satisfiesRange(version, range) {
69
+ const r = range.trim();
70
+ if (r === "*" || r === "" || r === "latest")
71
+ return true;
72
+ // Kompozit aralık: "op1 ver1 op2 ver2"
73
+ const composite = r.match(/^(>=|<=|>|<)\s*(v?\d+\.\d+\.\d+)\s+(>=|<=|>|<)\s*(v?\d+\.\d+\.\d+)$/);
74
+ if (composite) {
75
+ const [, op1, ver1, op2, ver2] = composite;
76
+ return applyOp(version, op1, ver1) && applyOp(version, op2, ver2);
77
+ }
78
+ // ^ (caret): ^1.2.0, ^0.2.3, ^1.2, ^1
79
+ if (r.startsWith("^")) {
80
+ const baseStr = r.slice(1).trim();
81
+ const base = parseSemver(baseStr);
82
+ if (compareSemver(version, baseStr) < 0)
83
+ return false;
84
+ const pv = parseSemver(version);
85
+ // ^1 → >=1.0.0 <2.0.0
86
+ if (baseStr.match(/^\d+$/))
87
+ return pv.major === base.major;
88
+ // ^1.2 → >=1.2.0 <2.0.0
89
+ if (baseStr.match(/^\d+\.\d+$/))
90
+ return pv.major === base.major && pv.minor === base.minor;
91
+ // ^1.2.3 → >=1.2.3 <2.0.0
92
+ if (base.major !== 0)
93
+ return pv.major === base.major;
94
+ if (base.minor !== 0)
95
+ return pv.major === 0 && pv.minor === base.minor;
96
+ return pv.major === 0 && pv.minor === 0 && pv.patch === base.patch;
97
+ }
98
+ // ~ (tilde): ~1.2.3, ~1.2, ~1
99
+ if (r.startsWith("~")) {
100
+ const baseStr = r.slice(1).trim();
101
+ const base = parseSemver(baseStr);
102
+ if (compareSemver(version, baseStr) < 0)
103
+ return false;
104
+ const pv = parseSemver(version);
105
+ if (baseStr.match(/^\d+\.\d+\.\d+$/)) {
106
+ // ~1.2.3 → >=1.2.3 <1.3.0
107
+ return pv.major === base.major && pv.minor === base.minor;
108
+ }
109
+ if (baseStr.match(/^\d+\.\d+$/)) {
110
+ // ~1.2 → >=1.2.0 <1.3.0
111
+ return pv.major === base.major && pv.minor === base.minor;
112
+ }
113
+ // ~1 → >=1.0.0 <2.0.0
114
+ return pv.major === base.major;
115
+ }
116
+ // X-range: 1.x, 1.2.x, 1.*, 1.2.*
117
+ const xRange = r.match(/^(\d+)(?:\.(\d+))?\.(\*|x)$/);
118
+ if (xRange) {
119
+ const [, maj, min] = xRange;
120
+ const pv = parseSemver(version);
121
+ if (min === undefined) {
122
+ // 1.x → major = 1
123
+ return pv.major === parseInt(maj, 10);
124
+ }
125
+ // 1.2.x → major = 1, minor = 2
126
+ return pv.major === parseInt(maj, 10) && pv.minor === parseInt(min, 10);
127
+ }
128
+ // Tek operatörler
129
+ if (r.startsWith(">="))
130
+ return applyOp(version, ">=", r.slice(2).trim());
131
+ if (r.startsWith("<="))
132
+ return applyOp(version, "<=", r.slice(2).trim());
133
+ if (r.startsWith(">"))
134
+ return applyOp(version, ">", r.slice(1).trim());
135
+ if (r.startsWith("<"))
136
+ return applyOp(version, "<", r.slice(1).trim());
137
+ if (r.startsWith("="))
138
+ return compareSemver(version, r.slice(1).trim()) === 0;
139
+ // Exact (allow leading v)
140
+ const norm = r.startsWith("v") ? r.slice(1) : r;
141
+ const vnorm = version.startsWith("v") ? version.slice(1) : version;
142
+ if (/^\d+\.\d+\.\d+/.test(norm))
143
+ return compareSemver(vnorm, norm) === 0;
144
+ return version === r;
145
+ }
146
+ function applyOp(version, op, target) {
147
+ const cmp = compareSemver(version, target);
148
+ switch (op) {
149
+ case ">=": return cmp >= 0;
150
+ case "<=": return cmp <= 0;
151
+ case ">": return cmp > 0;
152
+ case "<": return cmp < 0;
153
+ default: return false;
154
+ }
155
+ }
156
+ export function maxSatisfying(versions, range) {
157
+ const sat = versions.filter((v) => satisfiesRange(v, range));
158
+ if (sat.length === 0)
159
+ return null;
160
+ sat.sort(compareSemver);
161
+ return sat[sat.length - 1];
162
+ }
@@ -0,0 +1,92 @@
1
+ // cli/src/core/sign.ts — Epoch 1e: paket imzalama/doğrulama
2
+ // RSA key pair ile paket imzalama, verified publisher sistemi
3
+ import { createHash, createSign, createVerify, generateKeyPairSync } from "crypto";
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ const KEY_DIR = join(homedir(), ".forge", "keys");
8
+ const PRIVATE_KEY_PATH = join(KEY_DIR, "forge.key");
9
+ const PUBLIC_KEY_PATH = join(KEY_DIR, "forge.pub");
10
+ /** Generate a new RSA key pair for signing */
11
+ export function generateKeyPair(name) {
12
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", {
13
+ modulusLength: 4096,
14
+ publicKeyEncoding: { type: "spki", format: "pem" },
15
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
16
+ });
17
+ if (!existsSync(KEY_DIR))
18
+ mkdirSync(KEY_DIR, { recursive: true });
19
+ writeFileSync(PRIVATE_KEY_PATH, privateKey);
20
+ writeFileSync(PUBLIC_KEY_PATH, publicKey);
21
+ console.log(`[forge] key pair generated for "${name}"`);
22
+ console.log(`[forge] private: ${PRIVATE_KEY_PATH}`);
23
+ console.log(`[forge] public: ${PUBLIC_KEY_PATH}`);
24
+ return { privateKey, publicKey };
25
+ }
26
+ /** Get or generate key pair */
27
+ export function getKeyPair() {
28
+ if (existsSync(PRIVATE_KEY_PATH) && existsSync(PUBLIC_KEY_PATH)) {
29
+ return {
30
+ privateKey: readFileSync(PRIVATE_KEY_PATH, "utf-8"),
31
+ publicKey: readFileSync(PUBLIC_KEY_PATH, "utf-8"),
32
+ };
33
+ }
34
+ return generateKeyPair("forge-user");
35
+ }
36
+ /** Sign a buffer and return base64 signature */
37
+ export function signData(data, privateKey) {
38
+ const sign = createSign("SHA256");
39
+ sign.update(data);
40
+ sign.end();
41
+ return sign.sign(privateKey, "base64");
42
+ }
43
+ /** Verify a signature */
44
+ export function verifySignature(data, signature, publicKey) {
45
+ const verify = createVerify("SHA256");
46
+ verify.update(data);
47
+ verify.end();
48
+ return verify.verify(publicKey, signature, "base64");
49
+ }
50
+ /** Sign a package tarball */
51
+ export function signPackage(tarballPath) {
52
+ const { privateKey, publicKey } = getKeyPair();
53
+ const data = readFileSync(tarballPath);
54
+ const sha256 = createHash("sha256").update(data).digest("hex");
55
+ const signature = signData(data, privateKey);
56
+ return {
57
+ sha256,
58
+ signature,
59
+ publicKey,
60
+ signedAt: new Date().toISOString(),
61
+ signer: "forge-user",
62
+ };
63
+ }
64
+ /** Verify a package signature */
65
+ export function verifyPackage(tarballPath, sig) {
66
+ const data = readFileSync(tarballPath);
67
+ const sha256 = createHash("sha256").update(data).digest("hex");
68
+ if (sha256 !== sig.sha256) {
69
+ return false;
70
+ }
71
+ return verifySignature(data, sig.signature, sig.publicKey);
72
+ }
73
+ /** Sign registry index */
74
+ export function signIndex(indexData) {
75
+ const { privateKey, publicKey } = getKeyPair();
76
+ const sha256 = createHash("sha256").update(indexData).digest("hex");
77
+ const signature = signData(indexData, privateKey);
78
+ return {
79
+ sha256,
80
+ signature,
81
+ publicKey,
82
+ signedAt: new Date().toISOString(),
83
+ signer: "forge-user",
84
+ };
85
+ }
86
+ /** Verify registry index signature */
87
+ export function verifyIndex(indexData, sig) {
88
+ const sha256 = createHash("sha256").update(indexData).digest("hex");
89
+ if (sha256 !== sig.sha256)
90
+ return false;
91
+ return verifySignature(indexData, sig.signature, sig.publicKey);
92
+ }
@@ -0,0 +1,71 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "fs";
4
+ export function forgeHome() {
5
+ return join(homedir(), ".forge");
6
+ }
7
+ export function packagesDir() {
8
+ return join(forgeHome(), "packages");
9
+ }
10
+ export function cacheDir() {
11
+ return join(forgeHome(), "cache");
12
+ }
13
+ export function tarballsDir() {
14
+ return join(cacheDir(), "tarballs");
15
+ }
16
+ export function linksFile() {
17
+ return join(forgeHome(), "links.json");
18
+ }
19
+ export function ensureForgeDirs() {
20
+ for (const dir of [forgeHome(), packagesDir(), cacheDir(), tarballsDir()]) {
21
+ if (!existsSync(dir))
22
+ mkdirSync(dir, { recursive: true });
23
+ }
24
+ if (!existsSync(linksFile())) {
25
+ writeFileSync(linksFile(), JSON.stringify({}, null, 2));
26
+ }
27
+ }
28
+ export function readLinks() {
29
+ ensureForgeDirs();
30
+ try {
31
+ return JSON.parse(readFileSync(linksFile(), "utf-8"));
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ export function writeLinks(links) {
38
+ ensureForgeDirs();
39
+ writeFileSync(linksFile(), JSON.stringify(links, null, 2));
40
+ }
41
+ export function packageDir(slug, version) {
42
+ return join(packagesDir(), `${slug}@${version}`);
43
+ }
44
+ export function isPackageInstalled(slug, version) {
45
+ return existsSync(packageDir(slug, version));
46
+ }
47
+ export function listInstalledPackages() {
48
+ ensureForgeDirs();
49
+ if (!existsSync(packagesDir()))
50
+ return [];
51
+ const entries = readdirSync(packagesDir(), { withFileTypes: true });
52
+ return entries
53
+ .filter((e) => e.isDirectory())
54
+ .map((e) => {
55
+ const at = e.name.lastIndexOf("@");
56
+ if (at === -1)
57
+ return null;
58
+ return { slug: e.name.slice(0, at), version: e.name.slice(at + 1), dir: join(packagesDir(), e.name) };
59
+ })
60
+ .filter(Boolean);
61
+ }
62
+ export function toSlug(pkg) {
63
+ return pkg.replace("/", "-");
64
+ }
65
+ export function fromSlug(slug) {
66
+ // anthropics-plan -> anthropics/plan (first dash is slash)
67
+ const idx = slug.indexOf("-");
68
+ if (idx === -1)
69
+ return slug;
70
+ return slug.slice(0, idx) + "/" + slug.slice(idx + 1);
71
+ }