@oomerevren/tryforge 0.1.2 → 0.1.3

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 (41) hide show
  1. package/CHANGELOG.md +107 -13
  2. package/README.md +172 -232
  3. package/dist/cli/src/adapters/agents-md.js +24 -0
  4. package/dist/cli/src/adapters/base.js +99 -0
  5. package/dist/cli/src/adapters/claude.js +43 -14
  6. package/dist/cli/src/adapters/codex.js +18 -17
  7. package/dist/cli/src/adapters/cursor.js +44 -30
  8. package/dist/cli/src/adapters/dsh.js +28 -30
  9. package/dist/cli/src/adapters/generic.js +10 -7
  10. package/dist/cli/src/adapters/opencode.js +23 -23
  11. package/dist/cli/src/adapters/types.js +12 -9
  12. package/dist/cli/src/adapters/windsurf.js +48 -28
  13. package/dist/cli/src/commands/add-external.js +111 -0
  14. package/dist/cli/src/commands/audit.js +68 -30
  15. package/dist/cli/src/commands/init.js +2 -2
  16. package/dist/cli/src/commands/install.js +90 -15
  17. package/dist/cli/src/commands/pack.js +119 -0
  18. package/dist/cli/src/commands/sync.js +181 -0
  19. package/dist/cli/src/commands/test.js +72 -0
  20. package/dist/cli/src/commands/tui.js +191 -0
  21. package/dist/cli/src/commands/update.js +4 -4
  22. package/dist/cli/src/commands/verify.js +62 -0
  23. package/dist/cli/src/core/fsutil.js +37 -0
  24. package/dist/cli/src/core/installer.js +9 -9
  25. package/dist/cli/src/core/lock.js +97 -10
  26. package/dist/cli/src/core/merge.js +108 -0
  27. package/dist/cli/src/core/plugin.js +2 -2
  28. package/dist/cli/src/core/project.js +136 -4
  29. package/dist/cli/src/core/registry.js +9 -9
  30. package/dist/cli/src/core/scan.js +159 -0
  31. package/dist/cli/src/core/semver.js +7 -7
  32. package/dist/cli/src/core/sign.js +2 -2
  33. package/dist/cli/src/core/sources.js +171 -0
  34. package/dist/cli/src/index.js +83 -16
  35. package/dist/index.cjs +359 -0
  36. package/dist/scripts/build-registry.js +2 -4
  37. package/dist/scripts/publish-verified.js +17 -9
  38. package/dist/scripts/seed-registry-13lite.js +5 -5
  39. package/dist/scripts/seed-registry.js +4 -4
  40. package/dist/scripts/verify-npm-mcps.js +6 -4
  41. package/package.json +15 -6
@@ -1,5 +1,5 @@
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.
1
+ // cli/src/core/semver.ts — Epoch 1c: full semver (^ ~ >= > <= < * exact + composite + x-range + pre-release)
2
+ // Single source of truth — used by registry.ts and project.ts.
3
3
  export function parseSemver(v) {
4
4
  const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-.]?([a-zA-Z0-9.]+))?/);
5
5
  if (!m)
@@ -50,17 +50,17 @@ export function isValidRange(r) {
50
50
  return true;
51
51
  if (/^\d+\.\d+\.(\*|x)$/.test(t))
52
52
  return true;
53
- // Kısmi aralıklar: ^1, ~1, ~1.2
53
+ // Partial ranges: ^1, ~1, ~1.2
54
54
  if (/^\^\d+$/.test(t))
55
55
  return true;
56
56
  if (/^~\d+$/.test(t))
57
57
  return true;
58
58
  if (/^~\d+\.\d+$/.test(t))
59
59
  return true;
60
- // Tek operatörler: >=1.0.0, <2.0.0, =1.2.3
60
+ // Single operators: >=1.0.0, <2.0.0, =1.2.3
61
61
  if (/^(>=|<=|>|<|=)\s*v?\d+\.\d+\.\d+/.test(t))
62
62
  return true;
63
- // Kompozit aralıklar: >=1.2.0 <2.0.0
63
+ // Composite ranges: >=1.2.0 <2.0.0
64
64
  if (/^(>=|<=|>|<)\s*v?\d+\.\d+\.\d+\s+(>=|<=|>|<)\s*v?\d+\.\d+\.\d+/.test(t))
65
65
  return true;
66
66
  return false;
@@ -69,7 +69,7 @@ export function satisfiesRange(version, range) {
69
69
  const r = range.trim();
70
70
  if (r === "*" || r === "" || r === "latest")
71
71
  return true;
72
- // Kompozit aralık: "op1 ver1 op2 ver2"
72
+ // Composite range: "op1 ver1 op2 ver2"
73
73
  const composite = r.match(/^(>=|<=|>|<)\s*(v?\d+\.\d+\.\d+)\s+(>=|<=|>|<)\s*(v?\d+\.\d+\.\d+)$/);
74
74
  if (composite) {
75
75
  const [, op1, ver1, op2, ver2] = composite;
@@ -125,7 +125,7 @@ export function satisfiesRange(version, range) {
125
125
  // 1.2.x → major = 1, minor = 2
126
126
  return pv.major === parseInt(maj, 10) && pv.minor === parseInt(min, 10);
127
127
  }
128
- // Tek operatörler
128
+ // Single operators
129
129
  if (r.startsWith(">="))
130
130
  return applyOp(version, ">=", r.slice(2).trim());
131
131
  if (r.startsWith("<="))
@@ -1,5 +1,5 @@
1
- // cli/src/core/sign.ts — Epoch 1e: paket imzalama/doğrulama
2
- // RSA key pair ile paket imzalama, verified publisher sistemi
1
+ // cli/src/core/sign.ts — Epoch 1e: package signing/verification
2
+ // RSA key-pair package signing, verified-publisher system
3
3
  import { createHash, createSign, createVerify, generateKeyPairSync } from "crypto";
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
5
  import { join } from "path";
@@ -0,0 +1,171 @@
1
+ // cli/src/core/sources.ts — Decentralized package sources (Phase 3).
2
+ //
3
+ // Beyond the verified registry, forge resolves:
4
+ // registry: pdf/merge, pdf/merge@^1.0.0 (default)
5
+ // github: github:owner/repo[#ref], owner/repo (fallback when the
6
+ // registry has no such name), https://github.com/o/r[.git][#ref]
7
+ // git: https://host/...git, git@host:... (shallow clone)
8
+ // local: ./path, ../path, /abs/path (monorepo / custom skills)
9
+ //
10
+ // Registry names win on conflict (owner/repo first tries the registry, then
11
+ // falls back to GitHub). Remote sources are cloned shallow (--depth 1) into
12
+ // a temp dir; the manifest (forge.toml [package], else SKILL.md / agent.md)
13
+ // is parsed for name/version/type/description.
14
+ import { existsSync, readFileSync, mkdirSync, rmSync, readdirSync } from "fs";
15
+ import { join, resolve, basename } from "path";
16
+ import { tmpdir } from "os";
17
+ import { execFileSync } from "child_process";
18
+ import { parse } from "smol-toml";
19
+ import { copyDirRecursive } from "./fsutil.js";
20
+ export function parseSourceArg(arg) {
21
+ const a = arg.trim();
22
+ // Local paths: ./x, ../x, /abs, ~/x, or .\x on Windows
23
+ if (a.startsWith("./") ||
24
+ a.startsWith("../") ||
25
+ a.startsWith(".\\") ||
26
+ a.startsWith("/") ||
27
+ a.startsWith("~/") ||
28
+ /^[a-zA-Z]:[\\/]/.test(a)) {
29
+ return { kind: "local", source: `local:${a}`, ref: a, explicit: true };
30
+ }
31
+ // github:owner/repo[#ref]
32
+ if (a.startsWith("github:")) {
33
+ const rest = a.slice("github:".length);
34
+ const hash = rest.indexOf("#");
35
+ const repo = hash === -1 ? rest : rest.slice(0, hash);
36
+ const want = hash === -1 ? undefined : rest.slice(hash + 1);
37
+ assertRepoShape(repo);
38
+ return { kind: "github", source: want ? `github:${repo}@${want}` : `github:${repo}`, ref: repo, want, explicit: true };
39
+ }
40
+ // Direct git URLs (optional #ref trailer)
41
+ if (/^(https?:\/\/|git@|ssh:\/\/)/.test(a)) {
42
+ let url = a;
43
+ let want;
44
+ const schemeEnd = a.indexOf("://") + 3;
45
+ const hash = a.lastIndexOf("#");
46
+ if (hash > schemeEnd) {
47
+ want = a.slice(hash + 1);
48
+ url = a.slice(0, hash);
49
+ }
50
+ const kind = url.includes("github.com") ? "github" : "git";
51
+ return { kind, source: want ? `${url}@${want}` : url, ref: url, want, explicit: true };
52
+ }
53
+ // owner/repo shorthand (registry fallback handled by caller)
54
+ if (/^[a-z0-9-]+\/[a-z0-9-_.]+(#[a-z0-9-_.]+)?$/i.test(a)) {
55
+ const hash = a.indexOf("#");
56
+ const repo = hash === -1 ? a : a.slice(0, hash);
57
+ const want = hash === -1 ? undefined : a.slice(hash + 1);
58
+ return { kind: "github", source: want ? `github:${repo}@${want}` : `github:${repo}`, ref: repo, want, explicit: false };
59
+ }
60
+ return { kind: "registry", source: "registry", ref: a, explicit: true };
61
+ }
62
+ function assertRepoShape(repo) {
63
+ if (!/^[a-z0-9-]+\/[a-z0-9-_.]+$/i.test(repo)) {
64
+ throw new Error(`[forge] invalid GitHub repo "${repo}" — expected owner/repo`);
65
+ }
66
+ }
67
+ function stageDir() {
68
+ const dir = join(tmpdir(), `forge-src-${process.pid}-${Date.now()}`);
69
+ mkdirSync(dir, { recursive: true });
70
+ return dir;
71
+ }
72
+ function parseManifest(dir, fallbackName) {
73
+ const tomlPath = join(dir, "forge.toml");
74
+ if (existsSync(tomlPath)) {
75
+ try {
76
+ const parsed = parse(readFileSync(tomlPath, "utf-8"));
77
+ const pkg = parsed.package;
78
+ if (pkg && typeof pkg.name === "string") {
79
+ const type = typeof pkg.type === "string" ? pkg.type : "skill";
80
+ return {
81
+ name: pkg.name,
82
+ version: typeof pkg.version === "string" ? pkg.version : "0.0.0",
83
+ type,
84
+ description: typeof pkg.description === "string" ? pkg.description : `${pkg.name} (from external source)`,
85
+ };
86
+ }
87
+ }
88
+ catch (e) {
89
+ throw new Error(`[forge] invalid forge.toml in ${dir}: ${e.message}`, { cause: e });
90
+ }
91
+ }
92
+ // Fall back to SKILL.md / agent.md conventions
93
+ const skillMd = join(dir, "SKILL.md");
94
+ const agentMd = join(dir, "agent.md");
95
+ const mdPath = existsSync(skillMd) ? skillMd : existsSync(agentMd) ? agentMd : null;
96
+ if (mdPath) {
97
+ const raw = readFileSync(mdPath, "utf-8");
98
+ const heading = raw.split("\n").map((l) => l.trim()).find((l) => l.startsWith("# "));
99
+ const scope = fallbackName.replace(/[^a-z0-9-]/gi, "").toLowerCase() || "external";
100
+ return {
101
+ name: `external/${basename(dir).replace(/[^a-z0-9-]/gi, "").toLowerCase() || scope}`,
102
+ version: "0.0.0",
103
+ type: mdPath === agentMd ? "agent" : "skill",
104
+ description: heading ? heading.replace(/^#\s+/, "").slice(0, 200) : `${fallbackName} (from external source)`,
105
+ };
106
+ }
107
+ throw new Error(`[forge] no manifest in ${dir}: expected forge.toml, SKILL.md, or agent.md`);
108
+ }
109
+ /** Resolve a local directory source into a staged package. */
110
+ export function resolveLocalSource(absDir) {
111
+ const dir = resolve(absDir.replace(/^~\//, `${process.env.HOME ?? ""}/`));
112
+ if (!existsSync(dir))
113
+ throw new Error(`[forge] local source not found: ${absDir}`);
114
+ const stat = (() => {
115
+ try {
116
+ return { dir: readdirSync(dir).length >= 0 };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ })();
122
+ if (!stat)
123
+ throw new Error(`[forge] local source is not a directory: ${absDir}`);
124
+ const meta = parseManifest(dir, basename(dir));
125
+ const stage = stageDir();
126
+ // NOTE: raw fs.cpSync silently yields empty dirs under non-ASCII paths.
127
+ copyDirRecursive(dir, stage);
128
+ return {
129
+ kind: "local",
130
+ source: `local:${absDir}`,
131
+ ...meta,
132
+ dir: stage,
133
+ cleanup: () => rmSync(stage, { recursive: true, force: true }),
134
+ };
135
+ }
136
+ /** Shallow-clone a git URL (or owner/repo) into a staged package. */
137
+ export function resolveGitSource(repo, want) {
138
+ const url = repo.includes("://") || repo.startsWith("git@") ? repo : `https://github.com/${repo}.git`;
139
+ if (!/^(https?:\/\/|git@|ssh:\/\/)/.test(url)) {
140
+ throw new Error(`[forge] invalid git source "${repo}"`);
141
+ }
142
+ const kind = url.includes("github.com") ? "github" : "git";
143
+ const stage = stageDir();
144
+ try {
145
+ const args = want && want !== "HEAD"
146
+ ? ["clone", "--depth", "1", "--branch", want, url, stage]
147
+ : ["clone", "--depth", "1", url, stage];
148
+ execFileSync("git", args, { stdio: "pipe" });
149
+ }
150
+ catch (e) {
151
+ rmSync(stage, { recursive: true, force: true });
152
+ throw new Error(`[forge] git clone failed for ${url}${want ? ` (ref ${want})` : ""}: ${e.message}\n` +
153
+ `[forge] Check the URL/ref and that 'git' is installed.`, { cause: e });
154
+ }
155
+ const shortName = (repo.split("/").pop() ?? "repo").replace(/\.git$/, "");
156
+ const meta = parseManifest(stage, shortName);
157
+ const canonical = !repo.includes("://") && kind === "github" ? `github:${repo}` : url;
158
+ return {
159
+ kind,
160
+ source: want ? `${canonical}@${want}` : canonical,
161
+ ...meta,
162
+ dir: stage,
163
+ cleanup: () => rmSync(stage, { recursive: true, force: true }),
164
+ };
165
+ }
166
+ /** Dispatch any non-registry SourceSpec to its resolver. */
167
+ export function resolveExternalSource(spec) {
168
+ if (spec.kind === "local")
169
+ return resolveLocalSource(spec.ref);
170
+ return resolveGitSource(spec.ref, spec.want);
171
+ }
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
- // forge CLI — v0.1 (Faz 1: gerçek add/remove/list/doctor)
2
+ // forge CLI — v0.1 (Phase 1: real add/remove/list/doctor)
3
3
  import { Command } from "commander";
4
4
  import { existsSync, rmSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { loadPackageDetail, resolveVersion, parsePackageArg, searchPackages } from "./core/registry.js";
8
+ import { parseSourceArg } from "./core/sources.js";
8
9
  import { ensurePackageContent } from "./core/installer.js";
9
10
  import { ensureForgeDirs, readLinks, writeLinks, packageDir, toSlug, listInstalledPackages } from "./core/store.js";
10
11
  import { allAdapters, detectAdapters, addMcpServerToConfig, removeMcpServerFromConfig, readMcpConfig } from "./adapters/index.js";
11
12
  import { runInit } from "./commands/init.js";
12
13
  import { runInstall } from "./commands/install.js";
14
+ import { runSync } from "./commands/sync.js";
13
15
  import { runOutdated, runUpdate } from "./commands/update.js";
14
16
  import { runAudit } from "./commands/audit.js";
15
17
  import { DEP_NAME_RE } from "./core/project.js";
@@ -19,22 +21,42 @@ const program = new Command();
19
21
  program
20
22
  .name("forge")
21
23
  .description("The Homebrew for AI Agents — one CLI for skills, MCPs, plugins, agents")
22
- .version("0.1.1")
24
+ .version("0.1.2")
23
25
  .helpOption("-h, --help", "display help for command");
24
26
  // --- add ---
25
27
  program
26
28
  .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
+ .description("Install a package (registry, github:, git URL, or local path)")
30
+ .argument("<pkg>", "package ref: scope/name[@range], github:owner/repo[#ref], <git-url>[#ref], ./local/path")
29
31
  .option("--global", "install globally (default)")
30
32
  .option("--dry-run", "show what would be installed without writing")
31
33
  .option("--mock", "allow mock content for packages with no verified tarball yet")
34
+ .option("--skip-scan", "skip the pre-install security scan (not recommended)", false)
32
35
  .action(async (pkgArg, opts) => {
36
+ const { installExternal } = await import("./commands/add-external.js");
37
+ const extOpts = { dryRun: opts.dryRun, mock: opts.mock, skipScan: opts.skipScan };
38
+ const spec = parseSourceArg(pkgArg);
39
+ // Explicit external sources (github:, git URLs, local paths) bypass the registry.
40
+ if (spec.kind !== "registry" && spec.explicit) {
41
+ await installExternal(spec, extOpts);
42
+ return;
43
+ }
44
+ // Bare owner/repo (or scope/name[@range]): registry FIRST, GitHub fallback.
33
45
  const { name, version: requested } = parsePackageArg(pkgArg);
34
46
  if (!DEP_NAME_RE.test(name)) {
35
47
  console.error(`[forge] invalid package name "${name}" — expected scope/name (e.g. anthropics/plan)`);
36
48
  process.exit(1);
37
49
  }
50
+ if (spec.kind !== "registry") {
51
+ try {
52
+ await loadPackageDetail(name);
53
+ }
54
+ catch {
55
+ console.log(`[forge] ${name} not in registry — trying GitHub...`);
56
+ await installExternal({ ...spec, explicit: true }, extOpts);
57
+ return;
58
+ }
59
+ }
38
60
  console.log(`[forge] resolving ${name}${requested ? "@" + requested : ""}...`);
39
61
  let detail, version, versionMeta;
40
62
  try {
@@ -81,7 +103,7 @@ program
81
103
  let failed = 0;
82
104
  for (const adapter of adapters) {
83
105
  try {
84
- await adapter.install(slug, srcDir, detail.type);
106
+ await adapter.install(slug, srcDir, detail.type, { version, description: detail.description });
85
107
  console.log(` ✓ ${adapter.displayName} → ${adapter.skillDir(slug)}`);
86
108
  // MCP: inject into mcp config
87
109
  if (detail.type === "mcp" && versionMeta.mcp) {
@@ -98,7 +120,7 @@ program
98
120
  failed++;
99
121
  }
100
122
  }
101
- // Epoch 1e: herhangi bir adapter hatasında exit 1
123
+ // Epoch 1e: exit 1 on any adapter failure
102
124
  if (failed > 0) {
103
125
  console.log(`\n[forge] ✗ ${failed} harness(es) failed — installed on ${adapters.length - failed}/${adapters.length}`);
104
126
  process.exitCode = 1;
@@ -115,7 +137,10 @@ program
115
137
  const depResolved = await resolveVersion(depName, depRange);
116
138
  const depSrc = await ensurePackageContent(depName, depResolved.version, depResolved.detail, depResolved.versionMeta, { allowMock: opts.mock });
117
139
  for (const adapter of adapters) {
118
- await adapter.install(toSlug(depName), depSrc, depResolved.detail.type);
140
+ await adapter.install(toSlug(depName), depSrc, depResolved.detail.type, {
141
+ version: depResolved.version,
142
+ description: depResolved.detail.description,
143
+ });
119
144
  }
120
145
  console.log(` ✓ dep ${depName}@${depResolved.version}`);
121
146
  }
@@ -161,7 +186,6 @@ program
161
186
  const version = record?.version ?? "unknown";
162
187
  // Remove from adapters
163
188
  const adapters = allAdapters; // try all to be thorough
164
- let cleaned = false;
165
189
  for (const adapter of adapters) {
166
190
  const wasInstalled = await adapter.isInstalled(slug);
167
191
  if (wasInstalled) {
@@ -171,7 +195,6 @@ program
171
195
  await adapter.uninstall(slug, "mcp");
172
196
  await adapter.uninstall(slug, "agent");
173
197
  console.log(` ✓ removed from ${adapter.displayName}`);
174
- cleaned = true;
175
198
  }
176
199
  catch (e) {
177
200
  console.warn(` ✗ ${adapter.displayName}: ${e.message}`);
@@ -184,7 +207,7 @@ program
184
207
  try {
185
208
  removeMcpServerFromConfig(cfgPath, slug);
186
209
  }
187
- catch { }
210
+ catch { /* stale MCP entries are best-effort cleanup */ }
188
211
  }
189
212
  }
190
213
  // Remove from store (keep cache? remove package dir)
@@ -194,13 +217,13 @@ program
194
217
  rmSync(dir, { recursive: true, force: true });
195
218
  console.log(`[forge] removed store: ${dir}`);
196
219
  }
197
- delete links[name];
220
+ Reflect.deleteProperty(links, name);
198
221
  writeLinks(links);
199
222
  }
200
223
  else {
201
224
  // Epoch 1d: always clean links entry if it exists (even partial)
202
225
  if (links[name]) {
203
- delete links[name];
226
+ Reflect.deleteProperty(links, name);
204
227
  writeLinks(links);
205
228
  }
206
229
  // try to clean any version dir matching slug
@@ -323,7 +346,6 @@ program
323
346
  console.log(` ! missing MCP config: ${name} on ${adapterName} → ${cfgPath}`);
324
347
  if (opts.fix && existsSync(liveDir)) {
325
348
  try {
326
- const detail = await loadPackageDetail(name);
327
349
  const resolved = await resolveVersion(name, rec.version);
328
350
  if (resolved.versionMeta.mcp) {
329
351
  addMcpServerToConfig(cfgPath, rec.slug, resolved.versionMeta.mcp);
@@ -351,7 +373,7 @@ program
351
373
  if (opts.fix) {
352
374
  const src = liveDir;
353
375
  if (existsSync(src)) {
354
- await adapter.install(rec.slug, src, rec.type ?? "skill");
376
+ await adapter.install(rec.slug, src, rec.type ?? "skill", { version: rec.version });
355
377
  console.log(` → fixed`);
356
378
  fixed++;
357
379
  }
@@ -457,6 +479,15 @@ program
457
479
  .action(async (opts) => {
458
480
  await runInstall({ frozen: opts.frozen, mock: opts.mock });
459
481
  });
482
+ // --- sync ---
483
+ program
484
+ .command("sync")
485
+ .description("One-command team sync: skills + rules + MCP servers + agent roles (e.g. forge sync)")
486
+ .option("--mock", "allow mock content for packages with no verified tarball yet", false)
487
+ .option("--skip-scan", "skip the pre-install security scan (not recommended)", false)
488
+ .action(async (opts) => {
489
+ await runSync({ mock: opts.mock, skipScan: opts.skipScan });
490
+ });
460
491
  // --- outdated ---
461
492
  program
462
493
  .command("outdated")
@@ -473,10 +504,46 @@ program
473
504
  .action(async (pkg, opts) => {
474
505
  await runUpdate(pkg, { mock: opts.mock });
475
506
  });
476
- // --- audit (Faz 10 iskelet, Faz 22'de tam) ---
507
+ // --- tui ---
508
+ program
509
+ .command("tui")
510
+ .description("Launch interactive TUI dashboard (no args = same as npx forge)")
511
+ .action(async () => {
512
+ const { runTui } = await import("./commands/tui.js");
513
+ await runTui();
514
+ });
515
+ // --- test ---
516
+ program
517
+ .command("test")
518
+ .description("Test a package against the adapter matrix (dry-run install)")
519
+ .argument("<pkg>", "package name or path")
520
+ .option("--mock", "allow mock content for packages with no verified tarball yet", false)
521
+ .action(async (pkg, opts) => {
522
+ const { runTest } = await import("./commands/test.js");
523
+ await runTest(pkg, { mock: opts.mock });
524
+ });
525
+ // --- pack ---
526
+ program
527
+ .command("pack")
528
+ .description("Package the current directory into a verified tarball")
529
+ .option("--check", "validate only — do not write tarball", false)
530
+ .action(async (opts) => {
531
+ const { runPack } = await import("./commands/pack.js");
532
+ await runPack({ check: opts.check });
533
+ });
534
+ // --- verify ---
535
+ program
536
+ .command("verify")
537
+ .description("Verify a package: schema, permissions, security scan")
538
+ .argument("<pkg>", "package name or path")
539
+ .action(async (pkg) => {
540
+ const { runVerify } = await import("./commands/verify.js");
541
+ await runVerify(pkg);
542
+ });
543
+ // --- audit (Phase 10 skeleton, full DB in Phase 22) ---
477
544
  program
478
545
  .command("audit")
479
- .description("Audit installed packages (skeleton — full DB in Faz 22)")
546
+ .description("Audit installed packages (skeleton — full DB in Phase 22)")
480
547
  .option("--json", "output JSON", false)
481
548
  .action(async (opts) => {
482
549
  await runAudit({ json: opts.json });