@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.
- package/CHANGELOG.md +107 -13
- package/README.md +172 -232
- package/dist/cli/src/adapters/agents-md.js +24 -0
- package/dist/cli/src/adapters/base.js +99 -0
- package/dist/cli/src/adapters/claude.js +43 -14
- package/dist/cli/src/adapters/codex.js +18 -17
- package/dist/cli/src/adapters/cursor.js +44 -30
- package/dist/cli/src/adapters/dsh.js +28 -30
- package/dist/cli/src/adapters/generic.js +10 -7
- package/dist/cli/src/adapters/opencode.js +23 -23
- package/dist/cli/src/adapters/types.js +12 -9
- package/dist/cli/src/adapters/windsurf.js +48 -28
- package/dist/cli/src/commands/add-external.js +111 -0
- package/dist/cli/src/commands/audit.js +68 -30
- package/dist/cli/src/commands/init.js +2 -2
- package/dist/cli/src/commands/install.js +90 -15
- package/dist/cli/src/commands/pack.js +119 -0
- package/dist/cli/src/commands/sync.js +181 -0
- package/dist/cli/src/commands/test.js +72 -0
- package/dist/cli/src/commands/tui.js +191 -0
- package/dist/cli/src/commands/update.js +4 -4
- package/dist/cli/src/commands/verify.js +62 -0
- package/dist/cli/src/core/fsutil.js +37 -0
- package/dist/cli/src/core/installer.js +9 -9
- package/dist/cli/src/core/lock.js +97 -10
- package/dist/cli/src/core/merge.js +108 -0
- package/dist/cli/src/core/plugin.js +2 -2
- package/dist/cli/src/core/project.js +136 -4
- package/dist/cli/src/core/registry.js +9 -9
- package/dist/cli/src/core/scan.js +159 -0
- package/dist/cli/src/core/semver.js +7 -7
- package/dist/cli/src/core/sign.js +2 -2
- package/dist/cli/src/core/sources.js +171 -0
- package/dist/cli/src/index.js +83 -16
- package/dist/index.cjs +359 -0
- package/dist/scripts/build-registry.js +2 -4
- package/dist/scripts/publish-verified.js +17 -9
- package/dist/scripts/seed-registry-13lite.js +5 -5
- package/dist/scripts/seed-registry.js +4 -4
- package/dist/scripts/verify-npm-mcps.js +6 -4
- package/package.json +15 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// cli/src/core/semver.ts — Epoch 1c:
|
|
2
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
2
|
-
// RSA key
|
|
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
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// forge CLI — v0.1 (
|
|
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.
|
|
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 (
|
|
28
|
-
.argument("<pkg>", "package name,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
|
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 });
|