@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,31 +1,60 @@
|
|
|
1
|
-
// cli/src/adapters/claude.ts — Claude Code harness
|
|
1
|
+
// cli/src/adapters/claude.ts — Claude Code harness (2026 format)
|
|
2
|
+
//
|
|
3
|
+
// Skills/agents: type-aware dirs under ~/.claude/ (skills/, agents/, ...).
|
|
4
|
+
// Rules (declarative): merged forge blocks in <project>/CLAUDE.md — user
|
|
5
|
+
// content outside markers is never touched (see core/merge.ts).
|
|
6
|
+
// MCP: user-scope ~/.claude.json (mcpServers key — behavior config in
|
|
7
|
+
// settings.json is left alone).
|
|
2
8
|
import { existsSync } from "fs";
|
|
3
9
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
10
|
+
import { removeLinkOrDir, targetDirFor, uninstallSkillFiles, installSkillFiles } from "./types.js";
|
|
11
|
+
import { testHome, pointerBody, sharedList, sharedIsInstalled, syncBlockFile, unsyncBlockFile, shouldSyncRules, } from "./base.js";
|
|
12
|
+
function skillPaths() {
|
|
13
|
+
const base = join(testHome(), ".claude");
|
|
14
|
+
return {
|
|
15
|
+
skillBases: () => [join(base, "skills")],
|
|
16
|
+
installBase: () => join(base, "skills"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function claudeMdPath(projectDir) {
|
|
20
|
+
return join(projectDir, "CLAUDE.md");
|
|
21
|
+
}
|
|
22
|
+
function syncClaudeMd(projectDir, pkgSlug, meta) {
|
|
23
|
+
const file = claudeMdPath(projectDir);
|
|
24
|
+
if (!shouldSyncRules(projectDir, file))
|
|
25
|
+
return;
|
|
26
|
+
syncBlockFile(file, pkgSlug, meta?.version ?? "0.0.0", pointerBody({
|
|
27
|
+
slug: pkgSlug,
|
|
28
|
+
version: meta?.version ?? "",
|
|
29
|
+
description: meta?.description ?? `${pkgSlug} (installed via Forge)`,
|
|
30
|
+
skillPath: join(testHome(), ".claude", "skills", pkgSlug),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
6
33
|
export const claudeAdapter = {
|
|
7
34
|
name: "claude-code",
|
|
8
35
|
displayName: "Claude Code",
|
|
9
|
-
version: "0.
|
|
10
|
-
detect: () => existsSync(join(
|
|
11
|
-
skillDir: (slug) => join(
|
|
36
|
+
version: "0.2.0",
|
|
37
|
+
detect: () => existsSync(join(testHome(), ".claude")),
|
|
38
|
+
skillDir: (slug) => join(testHome(), ".claude", "skills", slug),
|
|
12
39
|
// Epoch 1c: Claude user-scope MCP = ~/.claude.json (not settings.json which is behavior config)
|
|
13
|
-
mcpConfigPath: () => join(
|
|
14
|
-
async install(pkgSlug, srcDir, type) {
|
|
15
|
-
// Epoch 1d: type-aware install —
|
|
16
|
-
const base = join(
|
|
40
|
+
mcpConfigPath: () => join(testHome(), ".claude.json"),
|
|
41
|
+
async install(pkgSlug, srcDir, type, meta) {
|
|
42
|
+
// Epoch 1d: type-aware install — each type goes to its own directory
|
|
43
|
+
const base = join(testHome(), ".claude");
|
|
17
44
|
installSkillFiles("claude-code", pkgSlug, srcDir, targetDirFor(base, type));
|
|
45
|
+
syncClaudeMd(process.cwd(), pkgSlug, meta);
|
|
18
46
|
},
|
|
19
47
|
async uninstall(pkgSlug, type) {
|
|
20
48
|
void type;
|
|
21
|
-
uninstallSkillFiles(pkgSlug, join(
|
|
22
|
-
const agentPath = join(
|
|
49
|
+
uninstallSkillFiles(pkgSlug, join(testHome(), ".claude", "skills"));
|
|
50
|
+
const agentPath = join(testHome(), ".claude", "agents", pkgSlug);
|
|
23
51
|
removeLinkOrDir(agentPath);
|
|
52
|
+
unsyncBlockFile(claudeMdPath(process.cwd()), pkgSlug);
|
|
24
53
|
},
|
|
25
54
|
async list() {
|
|
26
|
-
return
|
|
55
|
+
return sharedList(skillPaths());
|
|
27
56
|
},
|
|
28
57
|
async isInstalled(pkgSlug) {
|
|
29
|
-
return
|
|
58
|
+
return sharedIsInstalled(skillPaths(), pkgSlug);
|
|
30
59
|
},
|
|
31
60
|
};
|
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
// cli/src/adapters/codex.ts — Codex harness
|
|
1
|
+
// cli/src/adapters/codex.ts — Codex harness (shared file-adapter base)
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { testHome, sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, } from "./base.js";
|
|
5
|
+
import { syncAgentsMd, unsyncAgentsMd } from "./agents-md.js";
|
|
6
|
+
function paths() {
|
|
7
|
+
const base = join(testHome(), ".codex", "skills");
|
|
8
|
+
return { skillBases: () => [base], installBase: () => base };
|
|
9
|
+
}
|
|
6
10
|
export const codexAdapter = {
|
|
7
11
|
name: "codex",
|
|
8
12
|
displayName: "Codex",
|
|
9
|
-
version: "0.
|
|
10
|
-
detect: () => existsSync(join(
|
|
11
|
-
skillDir: (slug) => join(
|
|
12
|
-
mcpConfigPath: () => join(
|
|
13
|
-
async install(pkgSlug, srcDir,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
version: "0.2.0",
|
|
14
|
+
detect: () => existsSync(join(testHome(), ".codex")),
|
|
15
|
+
skillDir: (slug) => join(paths().installBase(), slug),
|
|
16
|
+
mcpConfigPath: () => join(testHome(), ".codex", "mcp.json"),
|
|
17
|
+
async install(pkgSlug, srcDir, _type, meta) {
|
|
18
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
19
|
+
syncAgentsMd(process.cwd(), pkgSlug, join(paths().installBase(), pkgSlug), meta);
|
|
16
20
|
},
|
|
17
21
|
async uninstall(pkgSlug, _type) {
|
|
18
|
-
|
|
22
|
+
sharedUninstall(paths(), pkgSlug);
|
|
23
|
+
unsyncAgentsMd(process.cwd(), pkgSlug);
|
|
19
24
|
},
|
|
20
25
|
async list() {
|
|
21
|
-
|
|
22
|
-
if (!existsSync(dir))
|
|
23
|
-
return [];
|
|
24
|
-
const { readdirSync } = await import("fs");
|
|
25
|
-
return readdirSync(dir, { withFileTypes: true }).filter((d) => (d.isDirectory() || d.isSymbolicLink()) && !d.name.startsWith(".")).map((d) => d.name);
|
|
26
|
+
return sharedList(paths());
|
|
26
27
|
},
|
|
27
28
|
async isInstalled(pkgSlug) {
|
|
28
|
-
return
|
|
29
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
29
30
|
},
|
|
30
31
|
};
|
|
@@ -1,42 +1,56 @@
|
|
|
1
|
-
// cli/src/adapters/cursor.ts — Cursor harness
|
|
2
|
-
|
|
1
|
+
// cli/src/adapters/cursor.ts — Cursor harness (2026 format)
|
|
2
|
+
//
|
|
3
|
+
// Skills: <scope>/skills/<slug>/ (legacy dir, kept for back-compat).
|
|
4
|
+
// Rules (modern): <scope>/rules/<slug>.mdc with frontmatter
|
|
5
|
+
// (description, globs, alwaysApply) — Cursor reads these natively.
|
|
6
|
+
// MCP: injected into <scope>/mcp.json (via mcpConfigPath).
|
|
7
|
+
// Scope = project .cursor/ when present, else ~/.cursor/.
|
|
8
|
+
import { existsSync, rmSync } from "fs";
|
|
3
9
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
10
|
+
import { testHome, instructionBody, sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, writeManagedFile, } from "./base.js";
|
|
11
|
+
function scopeBase() {
|
|
12
|
+
const proj = join(process.cwd(), ".cursor");
|
|
13
|
+
if (existsSync(proj))
|
|
14
|
+
return proj;
|
|
15
|
+
return join(testHome(), ".cursor");
|
|
16
|
+
}
|
|
17
|
+
function paths() {
|
|
18
|
+
const base = scopeBase();
|
|
19
|
+
return {
|
|
20
|
+
skillBases: () => [join(process.cwd(), ".cursor", "skills"), join(testHome(), ".cursor", "skills")],
|
|
21
|
+
installBase: () => join(base, "skills"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function ruleFile(scope, pkgSlug) {
|
|
25
|
+
return join(scope, "rules", `${pkgSlug}.mdc`);
|
|
26
|
+
}
|
|
27
|
+
function ruleContent(pkgSlug, srcDir, meta) {
|
|
28
|
+
const description = meta?.description ?? `${pkgSlug} (installed via Forge)`;
|
|
29
|
+
const version = meta?.version ?? "0.0.0";
|
|
30
|
+
const body = instructionBody(srcDir, `${pkgSlug}@${version} — ${description}`);
|
|
31
|
+
return `---\ndescription: ${description}\nglobs:\nalwaysApply: false\n---\n<!-- FORGE:START id="${pkgSlug}" version="${version}" -->\n${body}<!-- FORGE:END id="${pkgSlug}" -->\n`;
|
|
32
|
+
}
|
|
6
33
|
export const cursorAdapter = {
|
|
7
34
|
name: "cursor",
|
|
8
35
|
displayName: "Cursor",
|
|
9
|
-
version: "0.
|
|
10
|
-
detect: () => existsSync(join(process.cwd(), ".cursor")) || existsSync(join(
|
|
11
|
-
skillDir: (slug) =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (existsSync(join(process.cwd(), ".cursor")))
|
|
18
|
-
return join(process.cwd(), ".cursor", "mcp.json");
|
|
19
|
-
return join(homedir(), ".cursor", "mcp.json");
|
|
20
|
-
},
|
|
21
|
-
async install(pkgSlug, srcDir, type) {
|
|
22
|
-
void type;
|
|
23
|
-
const base = existsSync(join(process.cwd(), ".cursor")) ? join(process.cwd(), ".cursor", "skills") : join(homedir(), ".cursor", "skills");
|
|
24
|
-
installSkillFiles("cursor", pkgSlug, srcDir, base);
|
|
36
|
+
version: "0.2.0",
|
|
37
|
+
detect: () => existsSync(join(process.cwd(), ".cursor")) || existsSync(join(testHome(), ".cursor")),
|
|
38
|
+
skillDir: (slug) => join(paths().installBase(), slug),
|
|
39
|
+
mcpConfigPath: () => join(scopeBase(), "mcp.json"),
|
|
40
|
+
async install(pkgSlug, srcDir, _type, meta) {
|
|
41
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
42
|
+
// Modern multi-rule format alongside the legacy skills dir.
|
|
43
|
+
writeManagedFile(ruleFile(scopeBase(), pkgSlug), pkgSlug, ruleContent(pkgSlug, srcDir, meta));
|
|
25
44
|
},
|
|
26
45
|
async uninstall(pkgSlug, _type) {
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
sharedUninstall(paths(), pkgSlug);
|
|
47
|
+
rmSync(ruleFile(join(process.cwd(), ".cursor"), pkgSlug), { force: true });
|
|
48
|
+
rmSync(ruleFile(join(testHome(), ".cursor"), pkgSlug), { force: true });
|
|
29
49
|
},
|
|
30
50
|
async list() {
|
|
31
|
-
|
|
32
|
-
for (const b of bases) {
|
|
33
|
-
if (existsSync(b))
|
|
34
|
-
return listDirNames(b);
|
|
35
|
-
}
|
|
36
|
-
return [];
|
|
51
|
+
return sharedList(paths());
|
|
37
52
|
},
|
|
38
53
|
async isInstalled(pkgSlug) {
|
|
39
|
-
|
|
40
|
-
return bases.some((b) => existsSync(join(b, pkgSlug)));
|
|
54
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
41
55
|
},
|
|
42
56
|
};
|
|
@@ -1,46 +1,44 @@
|
|
|
1
|
-
// cli/src/adapters/dsh.ts — DeepSeek harness
|
|
1
|
+
// cli/src/adapters/dsh.ts — DeepSeek harness (shared file-adapter base)
|
|
2
|
+
//
|
|
3
|
+
// Epoch 1c: DSH adapter is marked deprecated/legacy.
|
|
4
|
+
// The DeepSeek harness has no "skills" concept; it uses a cordis.yml + TS plugin model.
|
|
5
|
+
// Kept for backwards compatibility, surfaced as "community-maintained, untested".
|
|
2
6
|
import { existsSync } from "fs";
|
|
3
7
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { testHome, sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, } from "./base.js";
|
|
9
|
+
import { syncAgentsMd, unsyncAgentsMd } from "./agents-md.js";
|
|
10
|
+
function inProject() {
|
|
11
|
+
return existsSync(join(process.cwd(), ".dsh"));
|
|
12
|
+
}
|
|
13
|
+
function paths() {
|
|
14
|
+
return {
|
|
15
|
+
skillBases: () => [join(process.cwd(), ".dsh", "skills"), join(testHome(), ".dsh", "skills")],
|
|
16
|
+
installBase: () => (inProject() ? join(process.cwd(), ".dsh", "skills") : join(testHome(), ".dsh", "skills")),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
9
19
|
export const dshAdapter = {
|
|
10
20
|
name: "dsh",
|
|
11
21
|
displayName: "DeepSeek (DSH, community/untested)",
|
|
12
|
-
version: "0.
|
|
13
|
-
detect: () => existsSync(join(
|
|
14
|
-
skillDir: (slug) =>
|
|
15
|
-
if (existsSync(join(process.cwd(), ".dsh")))
|
|
16
|
-
return join(process.cwd(), ".dsh", "skills", slug);
|
|
17
|
-
return join(homedir(), ".dsh", "skills", slug);
|
|
18
|
-
},
|
|
22
|
+
version: "0.2.0",
|
|
23
|
+
detect: () => existsSync(join(testHome(), ".dsh")) || inProject(),
|
|
24
|
+
skillDir: (slug) => join(paths().installBase(), slug),
|
|
19
25
|
mcpConfigPath: () => {
|
|
20
|
-
if (
|
|
26
|
+
if (inProject())
|
|
21
27
|
return join(process.cwd(), ".dsh", "mcp.json");
|
|
22
|
-
return join(
|
|
28
|
+
return join(testHome(), ".dsh", "mcp.json");
|
|
23
29
|
},
|
|
24
|
-
async install(pkgSlug, srcDir,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
installSkillFiles("dsh", pkgSlug, srcDir, base);
|
|
30
|
+
async install(pkgSlug, srcDir, _type, meta) {
|
|
31
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
32
|
+
syncAgentsMd(process.cwd(), pkgSlug, join(paths().installBase(), pkgSlug), meta);
|
|
28
33
|
},
|
|
29
34
|
async uninstall(pkgSlug, _type) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
uninstallSkillFiles(pkgSlug, b);
|
|
35
|
+
sharedUninstall(paths(), pkgSlug);
|
|
36
|
+
unsyncAgentsMd(process.cwd(), pkgSlug);
|
|
33
37
|
},
|
|
34
38
|
async list() {
|
|
35
|
-
|
|
36
|
-
for (const b of bases) {
|
|
37
|
-
if (existsSync(b))
|
|
38
|
-
return listDirNames(b);
|
|
39
|
-
}
|
|
40
|
-
return [];
|
|
39
|
+
return sharedList(paths());
|
|
41
40
|
},
|
|
42
41
|
async isInstalled(pkgSlug) {
|
|
43
|
-
|
|
44
|
-
return bases.some((b) => existsSync(join(b, pkgSlug)));
|
|
42
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
45
43
|
},
|
|
46
44
|
};
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
// cli/src/adapters/generic.ts — Generic fallback (./.forge/packages)
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, } from "./base.js";
|
|
4
|
+
function paths() {
|
|
5
|
+
const base = join(process.cwd(), ".forge", "packages");
|
|
6
|
+
return { skillBases: () => [base], installBase: () => base };
|
|
7
|
+
}
|
|
5
8
|
export const genericAdapter = {
|
|
6
9
|
name: "generic",
|
|
7
10
|
displayName: "Generic (.forge)",
|
|
8
11
|
detect: () => true, // always as fallback
|
|
9
12
|
skillDir: (slug) => join(process.cwd(), ".forge", "packages", slug),
|
|
10
13
|
mcpConfigPath: () => join(process.cwd(), ".forge", "mcp.json"),
|
|
11
|
-
async install(pkgSlug, srcDir, _type) {
|
|
12
|
-
|
|
14
|
+
async install(pkgSlug, srcDir, _type, _meta) {
|
|
15
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
13
16
|
},
|
|
14
17
|
async uninstall(pkgSlug, _type) {
|
|
15
|
-
|
|
18
|
+
sharedUninstall(paths(), pkgSlug);
|
|
16
19
|
},
|
|
17
20
|
async list() {
|
|
18
|
-
return
|
|
21
|
+
return sharedList(paths());
|
|
19
22
|
},
|
|
20
23
|
async isInstalled(pkgSlug) {
|
|
21
|
-
return
|
|
24
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
22
25
|
},
|
|
23
26
|
};
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
// cli/src/adapters/opencode.ts — OpenCode harness
|
|
1
|
+
// cli/src/adapters/opencode.ts — OpenCode harness (shared file-adapter base)
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { testHome, sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, } from "./base.js";
|
|
5
|
+
import { syncAgentsMd, unsyncAgentsMd } from "./agents-md.js";
|
|
6
|
+
function inProject() {
|
|
7
|
+
return existsSync(join(process.cwd(), "opencode.json"));
|
|
8
|
+
}
|
|
9
|
+
function paths() {
|
|
10
|
+
return {
|
|
11
|
+
skillBases: () => [join(process.cwd(), ".opencode", "skills"), join(testHome(), ".config", "opencode", "skills")],
|
|
12
|
+
installBase: () => inProject() ? join(process.cwd(), ".opencode", "skills") : join(testHome(), ".config", "opencode", "skills"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
6
15
|
export const opencodeAdapter = {
|
|
7
16
|
name: "opencode",
|
|
8
17
|
displayName: "OpenCode",
|
|
9
|
-
version: "0.
|
|
10
|
-
detect: () =>
|
|
11
|
-
skillDir: (slug) => join(
|
|
18
|
+
version: "0.2.0",
|
|
19
|
+
detect: () => inProject() || existsSync(join(testHome(), ".config", "opencode", "opencode.json")),
|
|
20
|
+
skillDir: (slug) => join(paths().installBase(), slug),
|
|
12
21
|
mcpConfigPath: () => {
|
|
13
22
|
const proj = join(process.cwd(), "opencode.json");
|
|
14
23
|
if (existsSync(proj))
|
|
15
24
|
return proj;
|
|
16
|
-
return join(
|
|
25
|
+
return join(testHome(), ".config", "opencode", "opencode.json");
|
|
17
26
|
},
|
|
18
|
-
async install(pkgSlug, srcDir,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const base = isProject ? join(process.cwd(), ".opencode", "skills") : join(homedir(), ".config", "opencode", "skills");
|
|
22
|
-
installSkillFiles("opencode", pkgSlug, srcDir, base);
|
|
27
|
+
async install(pkgSlug, srcDir, _type, meta) {
|
|
28
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
29
|
+
syncAgentsMd(process.cwd(), pkgSlug, join(paths().installBase(), pkgSlug), meta);
|
|
23
30
|
},
|
|
24
31
|
async uninstall(pkgSlug, _type) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
uninstallSkillFiles(pkgSlug, base);
|
|
32
|
+
sharedUninstall(paths(), pkgSlug);
|
|
33
|
+
unsyncAgentsMd(process.cwd(), pkgSlug);
|
|
28
34
|
},
|
|
29
35
|
async list() {
|
|
30
|
-
|
|
31
|
-
for (const b of bases) {
|
|
32
|
-
if (existsSync(b))
|
|
33
|
-
return listDirNames(b);
|
|
34
|
-
}
|
|
35
|
-
return [];
|
|
36
|
+
return sharedList(paths());
|
|
36
37
|
},
|
|
37
38
|
async isInstalled(pkgSlug) {
|
|
38
|
-
|
|
39
|
-
return bases.some((b) => existsSync(join(b, pkgSlug)));
|
|
39
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
40
40
|
},
|
|
41
41
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cli/src/adapters/types.ts — Epoch 1c: fail-closed MCP config, Windows junction, skill files
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, copyFileSync, symlinkSync, lstatSync, unlinkSync, renameSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
|
+
import { copyDirRecursive } from "../core/fsutil.js";
|
|
4
5
|
export { forgeHome, packagesDir, packageDir, toSlug } from "../core/store.js";
|
|
5
6
|
export function ensureDir(dir) {
|
|
6
7
|
if (!existsSync(dir))
|
|
@@ -26,7 +27,9 @@ export function trySymlinkOrCopy(src, dest) {
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
catch {
|
|
29
|
-
cpSync
|
|
30
|
+
// NOTE: raw fs.cpSync silently yields empty dirs under non-ASCII
|
|
31
|
+
// Windows home paths — always use copyDirRecursive (see fsutil.ts).
|
|
32
|
+
copyDirRecursive(src, dest);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
export function removeLinkOrDir(target) {
|
|
@@ -59,13 +62,13 @@ export function readMcpConfig(configPath) {
|
|
|
59
62
|
}
|
|
60
63
|
catch (e) {
|
|
61
64
|
throw new Error(`[forge] MCP config at ${configPath} contains invalid JSON: ${e.message}\n` +
|
|
62
|
-
`[forge] Refusing to modify. Fix the config manually or rename it, then retry
|
|
65
|
+
`[forge] Refusing to modify. Fix the config manually or rename it, then retry.`, { cause: e });
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
|
-
export function writeMcpConfig(configPath, data
|
|
68
|
+
export function writeMcpConfig(configPath, data) {
|
|
66
69
|
ensureDir(dirname(configPath));
|
|
67
70
|
backupFileIfExists(configPath);
|
|
68
|
-
// Epoch 1d:
|
|
71
|
+
// Epoch 1d: atomic write — write to temp file, then rename (crash-safe)
|
|
69
72
|
const tmp = configPath + ".tmp." + Date.now();
|
|
70
73
|
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
71
74
|
renameSync(tmp, configPath);
|
|
@@ -87,10 +90,10 @@ export function backupFileIfExists(configPath) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
export function addMcpServerToConfig(configPath, name, mcp) {
|
|
90
|
-
// Epoch 1e:
|
|
93
|
+
// Epoch 1e: create the file when missing (fresh-machine support)
|
|
91
94
|
let cfg = readMcpConfig(configPath);
|
|
92
95
|
if (cfg === null) {
|
|
93
|
-
//
|
|
96
|
+
// File missing or empty — create a fresh config
|
|
94
97
|
cfg = {};
|
|
95
98
|
}
|
|
96
99
|
if (!cfg["mcpServers"])
|
|
@@ -109,11 +112,11 @@ export function removeMcpServerFromConfig(configPath, name) {
|
|
|
109
112
|
return; // file doesn't exist or invalid — nothing to remove
|
|
110
113
|
const servers = cfg["mcpServers"];
|
|
111
114
|
if (servers && name in servers) {
|
|
112
|
-
|
|
115
|
+
Reflect.deleteProperty(servers, name);
|
|
113
116
|
writeMcpConfig(configPath, cfg);
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
|
-
// Epoch 1d: type-aware
|
|
119
|
+
// Epoch 1d: type-aware target directory
|
|
117
120
|
export function targetDirFor(base, type) {
|
|
118
121
|
const dirMap = {
|
|
119
122
|
skill: "skills",
|
|
@@ -1,43 +1,63 @@
|
|
|
1
|
-
// cli/src/adapters/windsurf.ts — Windsurf harness (
|
|
2
|
-
|
|
1
|
+
// cli/src/adapters/windsurf.ts — Windsurf harness (2026 format)
|
|
2
|
+
//
|
|
3
|
+
// Skills: <scope>/skills/<slug>/ (kept for back-compat).
|
|
4
|
+
// Rules (Cascade AI): merged forge blocks in <project>/.windsurfrules —
|
|
5
|
+
// user content outside markers is never touched (see core/merge.ts).
|
|
6
|
+
// MCP: synced to <project>/.windsurf/mcp_config.json, else the global
|
|
7
|
+
// ~/.codeium/windsurf/mcp_config.json (via mcpConfigPath).
|
|
8
|
+
import { existsSync, rmSync } from "fs";
|
|
3
9
|
import { join } from "path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
10
|
+
import { testHome, pointerBody, sharedList, sharedIsInstalled, sharedUninstall, sharedInstall, syncBlockFile, unsyncBlockFile, shouldSyncRules, } from "./base.js";
|
|
11
|
+
function inProject() {
|
|
12
|
+
return existsSync(join(process.cwd(), ".windsurf"));
|
|
13
|
+
}
|
|
14
|
+
function paths() {
|
|
15
|
+
return {
|
|
16
|
+
skillBases: () => [join(process.cwd(), ".windsurf", "skills"), join(testHome(), ".windsurf", "skills")],
|
|
17
|
+
installBase: () => inProject() ? join(process.cwd(), ".windsurf", "skills") : join(testHome(), ".windsurf", "skills"),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function rulesPath() {
|
|
21
|
+
return join(process.cwd(), ".windsurfrules");
|
|
22
|
+
}
|
|
23
|
+
function syncRules(projectDir, pkgSlug, meta) {
|
|
24
|
+
const file = join(projectDir, ".windsurfrules");
|
|
25
|
+
if (!shouldSyncRules(projectDir, file))
|
|
26
|
+
return;
|
|
27
|
+
syncBlockFile(file, pkgSlug, meta?.version ?? "0.0.0", pointerBody({
|
|
28
|
+
slug: pkgSlug,
|
|
29
|
+
version: meta?.version ?? "",
|
|
30
|
+
description: meta?.description ?? `${pkgSlug} (installed via Forge)`,
|
|
31
|
+
skillPath: join(paths().installBase(), pkgSlug),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
6
34
|
export const windsurfAdapter = {
|
|
7
35
|
name: "windsurf",
|
|
8
36
|
displayName: "Windsurf",
|
|
9
|
-
version: "0.
|
|
10
|
-
detect: () => existsSync(join(process.cwd(), ".windsurf")) ||
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return join(homedir(), ".windsurf", "skills", slug);
|
|
15
|
-
},
|
|
37
|
+
version: "0.2.0",
|
|
38
|
+
detect: () => existsSync(join(process.cwd(), ".windsurf")) ||
|
|
39
|
+
existsSync(join(testHome(), ".windsurf")) ||
|
|
40
|
+
existsSync(join(testHome(), ".codeium", "windsurf")),
|
|
41
|
+
skillDir: (slug) => join(paths().installBase(), slug),
|
|
16
42
|
mcpConfigPath: () => {
|
|
17
|
-
if (
|
|
43
|
+
if (inProject())
|
|
18
44
|
return join(process.cwd(), ".windsurf", "mcp_config.json");
|
|
19
|
-
return join(
|
|
45
|
+
return join(testHome(), ".codeium", "windsurf", "mcp_config.json");
|
|
20
46
|
},
|
|
21
|
-
async install(pkgSlug, srcDir,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
installSkillFiles("windsurf", pkgSlug, srcDir, base);
|
|
47
|
+
async install(pkgSlug, srcDir, _type, meta) {
|
|
48
|
+
sharedInstall(paths(), pkgSlug, srcDir);
|
|
49
|
+
syncRules(process.cwd(), pkgSlug, meta);
|
|
25
50
|
},
|
|
26
51
|
async uninstall(pkgSlug, _type) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
sharedUninstall(paths(), pkgSlug);
|
|
53
|
+
unsyncBlockFile(rulesPath(), pkgSlug);
|
|
54
|
+
// Per-package Cascade rule files, if a previous version created them.
|
|
55
|
+
rmSync(join(process.cwd(), ".windsurf", "rules", `${pkgSlug}.md`), { force: true });
|
|
30
56
|
},
|
|
31
57
|
async list() {
|
|
32
|
-
|
|
33
|
-
for (const b of bases) {
|
|
34
|
-
if (existsSync(b))
|
|
35
|
-
return listDirNames(b);
|
|
36
|
-
}
|
|
37
|
-
return [];
|
|
58
|
+
return sharedList(paths());
|
|
38
59
|
},
|
|
39
60
|
async isInstalled(pkgSlug) {
|
|
40
|
-
|
|
41
|
-
return bases.some((b) => existsSync(join(b, pkgSlug)));
|
|
61
|
+
return sharedIsInstalled(paths(), pkgSlug);
|
|
42
62
|
},
|
|
43
63
|
};
|