@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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/cli/src/adapters/claude.js +31 -0
- package/dist/cli/src/adapters/codex.js +30 -0
- package/dist/cli/src/adapters/cursor.js +42 -0
- package/dist/cli/src/adapters/dsh.js +46 -0
- package/dist/cli/src/adapters/generic.js +23 -0
- package/dist/cli/src/adapters/index.js +30 -0
- package/dist/cli/src/adapters/opencode.js +41 -0
- package/dist/cli/src/adapters/types.js +140 -0
- package/dist/cli/src/adapters/windsurf.js +43 -0
- package/dist/cli/src/commands/audit.js +62 -0
- package/dist/cli/src/commands/init.js +104 -0
- package/dist/cli/src/commands/install.js +212 -0
- package/dist/cli/src/commands/update.js +119 -0
- package/dist/cli/src/core/config.js +47 -0
- package/dist/cli/src/core/installer.js +279 -0
- package/dist/cli/src/core/lock.js +41 -0
- package/dist/cli/src/core/permissions.js +29 -0
- package/dist/cli/src/core/plugin.js +34 -0
- package/dist/cli/src/core/project.js +60 -0
- package/dist/cli/src/core/registry.js +137 -0
- package/dist/cli/src/core/semver.js +162 -0
- package/dist/cli/src/core/sign.js +92 -0
- package/dist/cli/src/core/store.js +71 -0
- package/dist/cli/src/index.js +484 -0
- package/dist/scripts/build-registry.js +145 -0
- package/dist/scripts/publish-verified.js +82 -0
- package/dist/scripts/seed-registry-13lite.js +208 -0
- package/dist/scripts/seed-registry.js +144 -0
- package/dist/scripts/verify-npm-mcps.js +60 -0
- package/forge.toml +18 -0
- package/install.ps1 +59 -0
- package/install.sh +81 -0
- package/package.json +64 -0
- package/registry/index.json +336 -0
- package/registry/packages/agent-changelog-writer.json +27 -0
- package/registry/packages/agent-debugger.json +27 -0
- package/registry/packages/agent-pr-reviewer.json +27 -0
- package/registry/packages/agent-researcher.json +27 -0
- package/registry/packages/agent-security-auditor.json +27 -0
- package/registry/packages/cmd-plan.json +26 -0
- package/registry/packages/cmd-review.json +26 -0
- package/registry/packages/mcp-filesystem.json +34 -0
- package/registry/packages/mcp-github.json +33 -0
- package/registry/packages/mcp-memory.json +33 -0
- package/registry/packages/mcp-postgres.json +34 -0
- package/registry/packages/mcp-sequential-thinking.json +33 -0
- package/registry/packages/obra-superpowers.json +29 -0
- package/registry/packages/pdf-compress.json +27 -0
- package/registry/packages/pdf-convert.json +27 -0
- package/registry/packages/pdf-extract.json +27 -0
- package/registry/packages/pdf-forms.json +27 -0
- package/registry/packages/pdf-merge.json +27 -0
- package/registry/packages/pdf-ocr.json +27 -0
- package/registry/packages/pdf-split.json +27 -0
- package/registry/packages/pdf-tables.json +27 -0
- package/registry/search.json +248 -0
- package/registry/stats.json +13 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// forge CLI — v0.1 (Faz 1: gerçek add/remove/list/doctor)
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, rmSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { loadPackageDetail, resolveVersion, parsePackageArg, searchPackages } from "./core/registry.js";
|
|
8
|
+
import { ensurePackageContent } from "./core/installer.js";
|
|
9
|
+
import { ensureForgeDirs, readLinks, writeLinks, packageDir, toSlug, listInstalledPackages } from "./core/store.js";
|
|
10
|
+
import { allAdapters, detectAdapters, addMcpServerToConfig, removeMcpServerFromConfig, readMcpConfig } from "./adapters/index.js";
|
|
11
|
+
import { runInit } from "./commands/init.js";
|
|
12
|
+
import { runInstall } from "./commands/install.js";
|
|
13
|
+
import { runOutdated, runUpdate } from "./commands/update.js";
|
|
14
|
+
import { runAudit } from "./commands/audit.js";
|
|
15
|
+
import { DEP_NAME_RE } from "./core/project.js";
|
|
16
|
+
import { ensureConfig } from "./core/config.js";
|
|
17
|
+
ensureConfig();
|
|
18
|
+
const program = new Command();
|
|
19
|
+
program
|
|
20
|
+
.name("forge")
|
|
21
|
+
.description("The Homebrew for AI Agents — one CLI for skills, MCPs, plugins, agents")
|
|
22
|
+
.version("0.1.1")
|
|
23
|
+
.helpOption("-h, --help", "display help for command");
|
|
24
|
+
// --- add ---
|
|
25
|
+
program
|
|
26
|
+
.command("add")
|
|
27
|
+
.description("Install a package (e.g. forge add anthropics/plan@1.2.0)")
|
|
28
|
+
.argument("<pkg>", "package name, e.g. anthropics/plan or anthropics/plan@1.2.0")
|
|
29
|
+
.option("--global", "install globally (default)")
|
|
30
|
+
.option("--dry-run", "show what would be installed without writing")
|
|
31
|
+
.option("--mock", "allow mock content for packages with no verified tarball yet")
|
|
32
|
+
.action(async (pkgArg, opts) => {
|
|
33
|
+
const { name, version: requested } = parsePackageArg(pkgArg);
|
|
34
|
+
if (!DEP_NAME_RE.test(name)) {
|
|
35
|
+
console.error(`[forge] invalid package name "${name}" — expected scope/name (e.g. anthropics/plan)`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
console.log(`[forge] resolving ${name}${requested ? "@" + requested : ""}...`);
|
|
39
|
+
let detail, version, versionMeta;
|
|
40
|
+
try {
|
|
41
|
+
const resolved = await resolveVersion(name, requested);
|
|
42
|
+
detail = resolved.detail;
|
|
43
|
+
version = resolved.version;
|
|
44
|
+
versionMeta = resolved.versionMeta;
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
console.error(`[forge] error: ${e.message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const slug = toSlug(name);
|
|
51
|
+
console.log(`[forge] found ${name}@${version} [${detail.type}] — ${detail.description}`);
|
|
52
|
+
console.log(versionMeta.verified
|
|
53
|
+
? `[forge] verified tarball ✓ (sha256 pinned)`
|
|
54
|
+
: `[forge] community tier (unverified) — install needs a verified tarball or --mock`);
|
|
55
|
+
if (opts.dryRun) {
|
|
56
|
+
console.log(`[forge] (dry-run) would install to ~/.forge/packages/${slug}@${version}/`);
|
|
57
|
+
const adapters = detectAdapters();
|
|
58
|
+
console.log(`[forge] (dry-run) would link to: ${adapters.map((a) => a.name).join(", ")}`);
|
|
59
|
+
if (versionMeta.mcp)
|
|
60
|
+
console.log(`[forge] (dry-run) MCP: ${versionMeta.mcp.command} ${(versionMeta.mcp.args ?? []).join(" ")}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Ensure content in store (fail-closed: throws on unverified/failed downloads)
|
|
64
|
+
console.log(`[forge] installing ${name}@${version}...`);
|
|
65
|
+
let srcDir;
|
|
66
|
+
try {
|
|
67
|
+
srcDir = await ensurePackageContent(name, version, detail, versionMeta, { allowMock: opts.mock });
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// exitCode + return (not process.exit): lets open fetch handles close,
|
|
71
|
+
// avoids a libuv handle-closing crash on Windows.
|
|
72
|
+
console.error(e.message);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`[forge] package content ready: ${srcDir}`);
|
|
77
|
+
// Detect adapters
|
|
78
|
+
const adapters = detectAdapters();
|
|
79
|
+
console.log(`[forge] detected harnesses: ${adapters.map((a) => `${a.displayName} (${a.name})`).join(", ")}`);
|
|
80
|
+
// Install to each adapter
|
|
81
|
+
let failed = 0;
|
|
82
|
+
for (const adapter of adapters) {
|
|
83
|
+
try {
|
|
84
|
+
await adapter.install(slug, srcDir, detail.type);
|
|
85
|
+
console.log(` ✓ ${adapter.displayName} → ${adapter.skillDir(slug)}`);
|
|
86
|
+
// MCP: inject into mcp config
|
|
87
|
+
if (detail.type === "mcp" && versionMeta.mcp) {
|
|
88
|
+
const cfgPath = adapter.mcpConfigPath();
|
|
89
|
+
if (cfgPath) {
|
|
90
|
+
const mcpName = slug; // use slug as MCP server key
|
|
91
|
+
addMcpServerToConfig(cfgPath, mcpName, versionMeta.mcp);
|
|
92
|
+
console.log(` ✓ MCP config → ${cfgPath} [${mcpName}]`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
console.warn(` ✗ ${adapter.displayName} failed: ${e.message}`);
|
|
98
|
+
failed++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Epoch 1e: herhangi bir adapter hatasında exit 1
|
|
102
|
+
if (failed > 0) {
|
|
103
|
+
console.log(`\n[forge] ✗ ${failed} harness(es) failed — installed on ${adapters.length - failed}/${adapters.length}`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(`\n[forge] ✓ installed ${name}@${version} on ${adapters.length} harness(es)`);
|
|
108
|
+
}
|
|
109
|
+
// Handle dependencies (shallow, one level for v0.1)
|
|
110
|
+
const deps = versionMeta.dependencies ?? {};
|
|
111
|
+
if (Object.keys(deps).length > 0) {
|
|
112
|
+
console.log(`[forge] installing ${Object.keys(deps).length} dependencies...`);
|
|
113
|
+
for (const [depName, depRange] of Object.entries(deps)) {
|
|
114
|
+
try {
|
|
115
|
+
const depResolved = await resolveVersion(depName, depRange);
|
|
116
|
+
const depSrc = await ensurePackageContent(depName, depResolved.version, depResolved.detail, depResolved.versionMeta, { allowMock: opts.mock });
|
|
117
|
+
for (const adapter of adapters) {
|
|
118
|
+
await adapter.install(toSlug(depName), depSrc, depResolved.detail.type);
|
|
119
|
+
}
|
|
120
|
+
console.log(` ✓ dep ${depName}@${depResolved.version}`);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.warn(` ✗ dep ${depName} failed: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Record link
|
|
128
|
+
ensureForgeDirs();
|
|
129
|
+
const links = readLinks();
|
|
130
|
+
links[name] = {
|
|
131
|
+
pkg: name,
|
|
132
|
+
version,
|
|
133
|
+
slug,
|
|
134
|
+
type: detail.type,
|
|
135
|
+
adapters: adapters.map((a) => a.name),
|
|
136
|
+
installedAt: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
writeLinks(links);
|
|
139
|
+
console.log(`\n[forge] ✓ installed ${name}@${version} on ${adapters.length} harness(es)`);
|
|
140
|
+
console.log(`[forge] run 'forge list' to see installed packages, 'forge doctor' to check health`);
|
|
141
|
+
});
|
|
142
|
+
// --- remove ---
|
|
143
|
+
program
|
|
144
|
+
.command("remove")
|
|
145
|
+
.alias("rm")
|
|
146
|
+
.alias("uninstall")
|
|
147
|
+
.description("Remove a package")
|
|
148
|
+
.argument("<pkg>", "package name")
|
|
149
|
+
.action(async (pkgArg) => {
|
|
150
|
+
const { name } = parsePackageArg(pkgArg);
|
|
151
|
+
if (!DEP_NAME_RE.test(name)) {
|
|
152
|
+
console.error(`[forge] invalid package name "${name}" — expected scope/name (e.g. anthropics/plan)`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const slug = toSlug(name);
|
|
156
|
+
const links = readLinks();
|
|
157
|
+
const record = links[name];
|
|
158
|
+
if (!record) {
|
|
159
|
+
console.warn(`[forge] ${name} not found in ~/.forge/links.json — trying to remove anyway`);
|
|
160
|
+
}
|
|
161
|
+
const version = record?.version ?? "unknown";
|
|
162
|
+
// Remove from adapters
|
|
163
|
+
const adapters = allAdapters; // try all to be thorough
|
|
164
|
+
let cleaned = false;
|
|
165
|
+
for (const adapter of adapters) {
|
|
166
|
+
const wasInstalled = await adapter.isInstalled(slug);
|
|
167
|
+
if (wasInstalled) {
|
|
168
|
+
// need type to know what to clean — try skill first, also MCP
|
|
169
|
+
try {
|
|
170
|
+
await adapter.uninstall(slug, "skill");
|
|
171
|
+
await adapter.uninstall(slug, "mcp");
|
|
172
|
+
await adapter.uninstall(slug, "agent");
|
|
173
|
+
console.log(` ✓ removed from ${adapter.displayName}`);
|
|
174
|
+
cleaned = true;
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
console.warn(` ✗ ${adapter.displayName}: ${e.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Epoch 1d: MCP config cleanup — ALWAYS, even if link is missing
|
|
181
|
+
// (orphan MCP entries from crashed installs)
|
|
182
|
+
const cfgPath = adapter.mcpConfigPath();
|
|
183
|
+
if (cfgPath) {
|
|
184
|
+
try {
|
|
185
|
+
removeMcpServerFromConfig(cfgPath, slug);
|
|
186
|
+
}
|
|
187
|
+
catch { }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Remove from store (keep cache? remove package dir)
|
|
191
|
+
if (record) {
|
|
192
|
+
const dir = packageDir(slug, record.version);
|
|
193
|
+
if (existsSync(dir)) {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
console.log(`[forge] removed store: ${dir}`);
|
|
196
|
+
}
|
|
197
|
+
delete links[name];
|
|
198
|
+
writeLinks(links);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Epoch 1d: always clean links entry if it exists (even partial)
|
|
202
|
+
if (links[name]) {
|
|
203
|
+
delete links[name];
|
|
204
|
+
writeLinks(links);
|
|
205
|
+
}
|
|
206
|
+
// try to clean any version dir matching slug
|
|
207
|
+
for (const p of listInstalledPackages()) {
|
|
208
|
+
if (p.slug === slug) {
|
|
209
|
+
rmSync(p.dir, { recursive: true, force: true });
|
|
210
|
+
console.log(`[forge] removed store: ${p.dir}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
console.log(`[forge] ✓ removed ${name}${version !== "unknown" ? "@" + version : ""}`);
|
|
215
|
+
});
|
|
216
|
+
// --- list ---
|
|
217
|
+
program
|
|
218
|
+
.command("list")
|
|
219
|
+
.alias("ls")
|
|
220
|
+
.description("List installed packages")
|
|
221
|
+
.action(async () => {
|
|
222
|
+
const links = readLinks();
|
|
223
|
+
const entries = Object.values(links);
|
|
224
|
+
if (entries.length === 0) {
|
|
225
|
+
// fallback: list store dirs
|
|
226
|
+
const pkgs = listInstalledPackages();
|
|
227
|
+
if (pkgs.length === 0) {
|
|
228
|
+
console.log("[forge] no packages installed. Try: forge add anthropics/plan");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
console.log("[forge] installed packages (from store):");
|
|
232
|
+
for (const p of pkgs) {
|
|
233
|
+
console.log(` - ${p.slug}@${p.version} (${p.dir})`);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log(`[forge] ${entries.length} package(s) installed:\n`);
|
|
238
|
+
for (const r of entries) {
|
|
239
|
+
const dir = packageDir(r.slug, r.version);
|
|
240
|
+
const exists = existsSync(dir) ? "" : " (missing dir!)";
|
|
241
|
+
console.log(` - ${r.pkg}@${r.version} [${r.adapters.join(", ")}]${exists}`);
|
|
242
|
+
console.log(` ${dir}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// --- doctor ---
|
|
246
|
+
program
|
|
247
|
+
.command("doctor")
|
|
248
|
+
.description("Check harness health")
|
|
249
|
+
.option("--fix", "try to fix broken links")
|
|
250
|
+
.option("--mock", "allow mock content when --fix restores packages with no verified tarball")
|
|
251
|
+
.action(async (opts) => {
|
|
252
|
+
ensureForgeDirs();
|
|
253
|
+
console.log("[forge] doctor — checking harnesses...\n");
|
|
254
|
+
for (const adapter of allAdapters) {
|
|
255
|
+
const detected = adapter.detect();
|
|
256
|
+
const icon = detected ? "✓" : "✗";
|
|
257
|
+
const extra = detected ? "(found)" : "(not found)";
|
|
258
|
+
// try to list count if detected
|
|
259
|
+
let countStr = "";
|
|
260
|
+
if (detected) {
|
|
261
|
+
try {
|
|
262
|
+
const list = await adapter.list();
|
|
263
|
+
countStr = ` — ${list.length} package(s)`;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
countStr = "";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let cfgInfo = "";
|
|
270
|
+
const cfgPath = adapter.mcpConfigPath();
|
|
271
|
+
if (cfgPath)
|
|
272
|
+
cfgInfo = ` mcp: ${cfgPath}`;
|
|
273
|
+
console.log(`${icon} ${adapter.displayName} (${adapter.name}) ${extra}${countStr}${cfgInfo ? "\n " + cfgInfo : ""}`);
|
|
274
|
+
}
|
|
275
|
+
console.log("\n[forge] store:");
|
|
276
|
+
console.log(` packages: ${join(homedir(), ".forge", "packages")}`);
|
|
277
|
+
console.log(` cache: ${join(homedir(), ".forge", "cache")}`);
|
|
278
|
+
const pkgs = listInstalledPackages();
|
|
279
|
+
console.log(` installed: ${pkgs.length} package(s)`);
|
|
280
|
+
if (pkgs.length > 0) {
|
|
281
|
+
for (const p of pkgs.slice(0, 5))
|
|
282
|
+
console.log(` - ${p.slug}@${p.version}`);
|
|
283
|
+
if (pkgs.length > 5)
|
|
284
|
+
console.log(` ... and ${pkgs.length - 5} more`);
|
|
285
|
+
}
|
|
286
|
+
// Epoch 1c: Check for broken links (links.json vs FS + --fix restores)
|
|
287
|
+
// MCP packages: check config entry existence (not skillDir)
|
|
288
|
+
const links = readLinks();
|
|
289
|
+
let broken = 0;
|
|
290
|
+
let fixed = 0;
|
|
291
|
+
for (const [name, rec] of Object.entries(links)) {
|
|
292
|
+
const dir = packageDir(rec.slug, rec.version);
|
|
293
|
+
if (!existsSync(dir)) {
|
|
294
|
+
broken++;
|
|
295
|
+
console.log(` ! broken: ${name}@${rec.version} — missing ${dir}`);
|
|
296
|
+
if (opts.fix) {
|
|
297
|
+
try {
|
|
298
|
+
const detail = await loadPackageDetail(name);
|
|
299
|
+
const resolved = await resolveVersion(name, rec.version);
|
|
300
|
+
const srcDir = await ensurePackageContent(name, resolved.version, detail, resolved.versionMeta, { allowMock: opts.mock });
|
|
301
|
+
console.log(` → store restored: ${srcDir}`);
|
|
302
|
+
fixed++;
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
console.log(` → restore failed: ${e.message}`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const liveDir = existsSync(dir) ? dir : packageDir(rec.slug, rec.version);
|
|
311
|
+
for (const adapterName of rec.adapters) {
|
|
312
|
+
const adapter = allAdapters.find((a) => a.name === adapterName);
|
|
313
|
+
if (!adapter)
|
|
314
|
+
continue;
|
|
315
|
+
// Epoch 1c: MCP packages — verify config entry exists
|
|
316
|
+
if (rec.type === "mcp") {
|
|
317
|
+
const cfgPath = adapter.mcpConfigPath();
|
|
318
|
+
if (cfgPath) {
|
|
319
|
+
try {
|
|
320
|
+
const cfg = readMcpConfig(cfgPath);
|
|
321
|
+
const servers = cfg?.["mcpServers"];
|
|
322
|
+
if (!servers || !(rec.slug in servers)) {
|
|
323
|
+
console.log(` ! missing MCP config: ${name} on ${adapterName} → ${cfgPath}`);
|
|
324
|
+
if (opts.fix && existsSync(liveDir)) {
|
|
325
|
+
try {
|
|
326
|
+
const detail = await loadPackageDetail(name);
|
|
327
|
+
const resolved = await resolveVersion(name, rec.version);
|
|
328
|
+
if (resolved.versionMeta.mcp) {
|
|
329
|
+
addMcpServerToConfig(cfgPath, rec.slug, resolved.versionMeta.mcp);
|
|
330
|
+
console.log(` → MCP config restored`);
|
|
331
|
+
fixed++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
console.log(` → cannot restore MCP config: ${e.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
console.log(` ! MCP config unreadable: ${cfgPath}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (adapterName === "generic")
|
|
347
|
+
continue;
|
|
348
|
+
const installed = await adapter.isInstalled(rec.slug);
|
|
349
|
+
if (!installed) {
|
|
350
|
+
console.log(` ! missing link: ${name} on ${adapterName} → ${adapter.skillDir(rec.slug)}`);
|
|
351
|
+
if (opts.fix) {
|
|
352
|
+
const src = liveDir;
|
|
353
|
+
if (existsSync(src)) {
|
|
354
|
+
await adapter.install(rec.slug, src, rec.type ?? "skill");
|
|
355
|
+
console.log(` → fixed`);
|
|
356
|
+
fixed++;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log(` → cannot fix, store missing`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (broken === 0)
|
|
366
|
+
console.log("\n[forge] ✓ no broken packages");
|
|
367
|
+
else
|
|
368
|
+
console.log(`\n[forge] ${broken} broken package(s)${opts.fix ? ` (${fixed} fixed)` : " — run with --fix"}`);
|
|
369
|
+
});
|
|
370
|
+
// --- search ---
|
|
371
|
+
program
|
|
372
|
+
.command("search")
|
|
373
|
+
.description("Search registry")
|
|
374
|
+
.argument("<query>", "search query")
|
|
375
|
+
.option("--type <type>", "filter by type (skill/mcp/plugin/agent/command/hook)")
|
|
376
|
+
.option("--json", "output JSON")
|
|
377
|
+
.action(async (query, opts) => {
|
|
378
|
+
const results = await searchPackages(query);
|
|
379
|
+
let filtered = results;
|
|
380
|
+
if (opts.type)
|
|
381
|
+
filtered = filtered.filter((p) => p.type === opts.type);
|
|
382
|
+
if (opts.json) {
|
|
383
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (filtered.length === 0) {
|
|
387
|
+
console.log(`No results for "${query}"${opts.type ? ` [type=${opts.type}]` : ""}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log(`Found ${filtered.length} package(s) for "${query}":\n`);
|
|
391
|
+
for (const p of filtered) {
|
|
392
|
+
const mark = p.verified ? " [✓ verified]" : "";
|
|
393
|
+
console.log(` - ${p.name}@${p.latest} [${p.type}]${mark} — ${p.description}`);
|
|
394
|
+
if (p.keywords?.length)
|
|
395
|
+
console.log(` keywords: ${p.keywords.join(", ")}`);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// --- info ---
|
|
399
|
+
program
|
|
400
|
+
.command("info")
|
|
401
|
+
.description("Show package info")
|
|
402
|
+
.argument("<pkg>", "package name")
|
|
403
|
+
.option("--json", "output JSON")
|
|
404
|
+
.action(async (pkg, opts) => {
|
|
405
|
+
try {
|
|
406
|
+
const detail = await loadPackageDetail(pkg);
|
|
407
|
+
if (opts.json) {
|
|
408
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log(`\n${detail.name} [${detail.type}] — ${detail.description}\n`);
|
|
412
|
+
if (detail.homepage)
|
|
413
|
+
console.log(`homepage: ${detail.homepage}`);
|
|
414
|
+
if (detail.repository)
|
|
415
|
+
console.log(`repository: ${detail.repository}`);
|
|
416
|
+
if (detail.keywords?.length)
|
|
417
|
+
console.log(`keywords: ${detail.keywords.join(", ")}`);
|
|
418
|
+
console.log(`latest: ${detail.latest}`);
|
|
419
|
+
console.log(`versions: ${Object.keys(detail.versions).join(", ")}\n`);
|
|
420
|
+
for (const [ver, meta] of Object.entries(detail.versions)) {
|
|
421
|
+
const marker = ver === detail.latest ? " (latest)" : "";
|
|
422
|
+
const trust = meta.verified ? "verified ✓" : "community (unverified)";
|
|
423
|
+
console.log(` ${ver}${marker} [${trust}]:`);
|
|
424
|
+
console.log(` tarball: ${meta.tarball}`);
|
|
425
|
+
console.log(` sha256: ${meta.sha256.slice(0, 16)}...`);
|
|
426
|
+
if (meta.engines)
|
|
427
|
+
console.log(` engines: ${JSON.stringify(meta.engines)}`);
|
|
428
|
+
if (meta.dependencies && Object.keys(meta.dependencies).length)
|
|
429
|
+
console.log(` deps: ${JSON.stringify(meta.dependencies)}`);
|
|
430
|
+
if (meta.mcp)
|
|
431
|
+
console.log(` mcp: ${meta.mcp.command} ${(meta.mcp.args ?? []).join(" ")}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
console.error(`Package ${pkg} not found: ${e.message}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
// --- init ---
|
|
440
|
+
program
|
|
441
|
+
.command("init")
|
|
442
|
+
.description("Scaffold a new package (e.g. forge init my-skill --type skill)")
|
|
443
|
+
.argument("[name]", "package name (e.g. my-skill or scope/name)")
|
|
444
|
+
.option("--type <type>", "package type (skill/mcp/agent/command/hook/plugin)", "skill")
|
|
445
|
+
.option("--yes", "skip prompts and use defaults", false)
|
|
446
|
+
.option("--force", "overwrite existing forge.toml", false)
|
|
447
|
+
.action(async (name, opts) => {
|
|
448
|
+
await runInit({ name, type: opts.type, yes: opts.yes, force: opts.force });
|
|
449
|
+
});
|
|
450
|
+
// --- install ---
|
|
451
|
+
program
|
|
452
|
+
.command("install")
|
|
453
|
+
.alias("i")
|
|
454
|
+
.description("Install all dependencies from forge.toml (team sync, e.g. forge install)")
|
|
455
|
+
.option("--frozen", "install exactly from forge.lock", false)
|
|
456
|
+
.option("--mock", "allow mock content for packages with no verified tarball yet", false)
|
|
457
|
+
.action(async (opts) => {
|
|
458
|
+
await runInstall({ frozen: opts.frozen, mock: opts.mock });
|
|
459
|
+
});
|
|
460
|
+
// --- outdated ---
|
|
461
|
+
program
|
|
462
|
+
.command("outdated")
|
|
463
|
+
.description("Show outdated packages")
|
|
464
|
+
.action(async () => {
|
|
465
|
+
await runOutdated();
|
|
466
|
+
});
|
|
467
|
+
// --- update ---
|
|
468
|
+
program
|
|
469
|
+
.command("update")
|
|
470
|
+
.description("Update packages to latest")
|
|
471
|
+
.argument("[pkg]", "package to update (all if omitted)")
|
|
472
|
+
.option("--mock", "allow mock content for packages with no verified tarball yet", false)
|
|
473
|
+
.action(async (pkg, opts) => {
|
|
474
|
+
await runUpdate(pkg, { mock: opts.mock });
|
|
475
|
+
});
|
|
476
|
+
// --- audit (Faz 10 iskelet, Faz 22'de tam) ---
|
|
477
|
+
program
|
|
478
|
+
.command("audit")
|
|
479
|
+
.description("Audit installed packages (skeleton — full DB in Faz 22)")
|
|
480
|
+
.option("--json", "output JSON", false)
|
|
481
|
+
.action(async (opts) => {
|
|
482
|
+
await runAudit({ json: opts.json });
|
|
483
|
+
});
|
|
484
|
+
program.parse();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
// scripts/build-registry.ts — packages/*.json → index.json + search.json + stats.json
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
const PACKAGES_DIR = "registry/packages";
|
|
6
|
+
const INDEX_FILE = "registry/index.json";
|
|
7
|
+
const SEARCH_FILE = "registry/search.json";
|
|
8
|
+
const STATS_FILE = "registry/stats.json";
|
|
9
|
+
function isCheckMode() {
|
|
10
|
+
return process.argv.includes("--check");
|
|
11
|
+
}
|
|
12
|
+
function loadAll() {
|
|
13
|
+
if (!existsSync(PACKAGES_DIR))
|
|
14
|
+
return [];
|
|
15
|
+
const files = readdirSync(PACKAGES_DIR).filter((f) => f.endsWith(".json"));
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const slug = file.replace(/\.json$/, "");
|
|
19
|
+
const raw = readFileSync(join(PACKAGES_DIR, file), "utf-8");
|
|
20
|
+
try {
|
|
21
|
+
const detail = JSON.parse(raw);
|
|
22
|
+
// validate minimal
|
|
23
|
+
if (!detail.name || !detail.type || !detail.description || !detail.latest || !detail.versions) {
|
|
24
|
+
console.warn(`[build] skip invalid ${file}: missing required fields`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!/^[a-z0-9-]+\/[a-z0-9-]+$/.test(detail.name)) {
|
|
28
|
+
console.warn(`[build] skip ${file}: invalid name ${detail.name}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (detail.description.length < 10 || detail.description.length > 200) {
|
|
32
|
+
console.warn(`[build] skip ${file}: description length ${detail.description.length} not in 10-200`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!detail.versions[detail.latest]) {
|
|
36
|
+
console.warn(`[build] skip ${file}: latest ${detail.latest} not in versions`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
out.push({ slug, detail });
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.warn(`[build] skip ${file}: JSON error ${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function build() {
|
|
48
|
+
const all = loadAll();
|
|
49
|
+
// sort by name for deterministic output
|
|
50
|
+
all.sort((a, b) => a.detail.name.localeCompare(b.detail.name));
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
let placeholderCount = 0;
|
|
53
|
+
let verifiedCount = 0;
|
|
54
|
+
// index.json
|
|
55
|
+
const packages = {};
|
|
56
|
+
for (const { detail } of all) {
|
|
57
|
+
const versions = Object.keys(detail.versions).sort((a, b) => {
|
|
58
|
+
// semver desc
|
|
59
|
+
const pa = a.split(".").map(Number);
|
|
60
|
+
const pb = b.split(".").map(Number);
|
|
61
|
+
for (let i = 0; i < 3; i++) {
|
|
62
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
63
|
+
return (pb[i] ?? 0) - (pa[i] ?? 0);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
});
|
|
67
|
+
// check placeholder sha
|
|
68
|
+
for (const v of Object.values(detail.versions)) {
|
|
69
|
+
if (v.sha256?.startsWith("placeholder"))
|
|
70
|
+
placeholderCount++;
|
|
71
|
+
}
|
|
72
|
+
const latestMeta = detail.versions[detail.latest];
|
|
73
|
+
const latestVerified = latestMeta?.verified === true;
|
|
74
|
+
if (latestVerified)
|
|
75
|
+
verifiedCount++;
|
|
76
|
+
packages[detail.name] = {
|
|
77
|
+
name: detail.name,
|
|
78
|
+
type: detail.type,
|
|
79
|
+
description: detail.description,
|
|
80
|
+
latest: detail.latest,
|
|
81
|
+
versions,
|
|
82
|
+
...(detail.keywords ? { keywords: detail.keywords } : {}),
|
|
83
|
+
updatedAt: latestMeta?.publishedAt ?? now,
|
|
84
|
+
verified: latestVerified,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const index = {
|
|
88
|
+
generatedAt: now,
|
|
89
|
+
count: all.length,
|
|
90
|
+
packages,
|
|
91
|
+
};
|
|
92
|
+
// search.json — flat array for offline search
|
|
93
|
+
const search = all.map(({ detail }) => ({
|
|
94
|
+
name: detail.name,
|
|
95
|
+
type: detail.type,
|
|
96
|
+
description: detail.description,
|
|
97
|
+
keywords: detail.keywords ?? [],
|
|
98
|
+
latest: detail.latest,
|
|
99
|
+
verified: detail.versions[detail.latest]?.verified === true,
|
|
100
|
+
}));
|
|
101
|
+
// stats.json
|
|
102
|
+
const byType = {};
|
|
103
|
+
for (const { detail } of all) {
|
|
104
|
+
byType[detail.type] = (byType[detail.type] ?? 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
const stats = {
|
|
107
|
+
generatedAt: now,
|
|
108
|
+
totalPackages: all.length,
|
|
109
|
+
byType,
|
|
110
|
+
placeholderSha: placeholderCount,
|
|
111
|
+
verifiedCount,
|
|
112
|
+
communityCount: all.length - verifiedCount,
|
|
113
|
+
};
|
|
114
|
+
if (isCheckMode()) {
|
|
115
|
+
// compare with existing index.json
|
|
116
|
+
if (!existsSync(INDEX_FILE)) {
|
|
117
|
+
console.error(`[build --check] ${INDEX_FILE} not found`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const existing = JSON.parse(readFileSync(INDEX_FILE, "utf-8"));
|
|
121
|
+
const expected = JSON.stringify(index, null, 2);
|
|
122
|
+
const actual = JSON.stringify(existing, null, 2);
|
|
123
|
+
// compare count and package keys (ignore generatedAt)
|
|
124
|
+
const existingCount = existing.count;
|
|
125
|
+
const expectedCount = index.count;
|
|
126
|
+
if (existingCount !== expectedCount || JSON.stringify(Object.keys(existing.packages).sort()) !== JSON.stringify(Object.keys(packages).sort())) {
|
|
127
|
+
console.error(`[build --check] index.json drift: existing count=${existingCount} expected=${expectedCount}`);
|
|
128
|
+
console.error(`[build --check] run 'npm run registry:build' to fix`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
console.log(`[build --check] OK — index.json in sync (count=${existingCount})`);
|
|
132
|
+
if (placeholderCount > 0)
|
|
133
|
+
console.warn(`[build --check] warn: ${placeholderCount} placeholder sha256 (expected in Faz 2)`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2) + "\n");
|
|
137
|
+
writeFileSync(SEARCH_FILE, JSON.stringify(search, null, 2) + "\n");
|
|
138
|
+
writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2) + "\n");
|
|
139
|
+
console.log(`[build] wrote ${INDEX_FILE} (${all.length} packages)`);
|
|
140
|
+
console.log(`[build] wrote ${SEARCH_FILE} (${search.length} entries)`);
|
|
141
|
+
console.log(`[build] wrote ${STATS_FILE} ${JSON.stringify(byType)}`);
|
|
142
|
+
if (placeholderCount > 0)
|
|
143
|
+
console.warn(`[build] warn: ${placeholderCount} placeholder sha256 (Faz 2 mock, Faz 3 real)`);
|
|
144
|
+
}
|
|
145
|
+
build();
|