@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,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();