@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,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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles, removeLinkOrDir, listDirNames, targetDirFor } from "./types.js";
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.1.0",
10
- detect: () => existsSync(join(homedir(), ".claude")),
11
- skillDir: (slug) => join(homedir(), ".claude", "skills", slug),
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(homedir(), ".claude.json"),
14
- async install(pkgSlug, srcDir, type) {
15
- // Epoch 1d: type-aware install — her tip kendi dizinine gider
16
- const base = join(homedir(), ".claude");
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(homedir(), ".claude", "skills"));
22
- const agentPath = join(homedir(), ".claude", "agents", pkgSlug);
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 listDirNames(join(homedir(), ".claude", "skills"));
55
+ return sharedList(skillPaths());
27
56
  },
28
57
  async isInstalled(pkgSlug) {
29
- return existsSync(join(homedir(), ".claude", "skills", pkgSlug));
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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles } from "./types.js";
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.1.0",
10
- detect: () => existsSync(join(homedir(), ".codex")),
11
- skillDir: (slug) => join(homedir(), ".codex", "skills", slug),
12
- mcpConfigPath: () => join(homedir(), ".codex", "mcp.json"),
13
- async install(pkgSlug, srcDir, type) {
14
- void type;
15
- installSkillFiles("codex", pkgSlug, srcDir, join(homedir(), ".codex", "skills"));
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
- uninstallSkillFiles(pkgSlug, join(homedir(), ".codex", "skills"));
22
+ sharedUninstall(paths(), pkgSlug);
23
+ unsyncAgentsMd(process.cwd(), pkgSlug);
19
24
  },
20
25
  async list() {
21
- const dir = join(homedir(), ".codex", "skills");
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 existsSync(join(homedir(), ".codex", "skills", pkgSlug));
29
+ return sharedIsInstalled(paths(), pkgSlug);
29
30
  },
30
31
  };
@@ -1,42 +1,56 @@
1
- // cli/src/adapters/cursor.ts — Cursor harness
2
- import { existsSync } from "fs";
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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles, listDirNames } from "./types.js";
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.1.0",
10
- detect: () => existsSync(join(process.cwd(), ".cursor")) || existsSync(join(homedir(), ".cursor")),
11
- skillDir: (slug) => {
12
- if (existsSync(join(process.cwd(), ".cursor")))
13
- return join(process.cwd(), ".cursor", "skills", slug);
14
- return join(homedir(), ".cursor", "skills", slug);
15
- },
16
- mcpConfigPath: () => {
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
- const base = existsSync(join(process.cwd(), ".cursor")) ? join(process.cwd(), ".cursor", "skills") : join(homedir(), ".cursor", "skills");
28
- uninstallSkillFiles(pkgSlug, base);
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
- const bases = [join(process.cwd(), ".cursor", "skills"), join(homedir(), ".cursor", "skills")];
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
- const bases = [join(process.cwd(), ".cursor", "skills"), join(homedir(), ".cursor", "skills")];
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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles, listDirNames } from "./types.js";
6
- // Epoch 1c: DSH adapter artık deprecated/legacy olarak işaretlenmiş.
7
- // DeepSeek Harness'ında "skills" kavramı yok; cordis.yml + TS plugin modeli kullanılır.
8
- // Adapter'ı tutuyoruz (geriye uyumluluk) ama "community-maintained, untested" olarak gösteriyoruz.
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.1.0",
13
- detect: () => existsSync(join(homedir(), ".dsh")) || existsSync(join(process.cwd(), ".dsh")),
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 (existsSync(join(process.cwd(), ".dsh")))
26
+ if (inProject())
21
27
  return join(process.cwd(), ".dsh", "mcp.json");
22
- return join(homedir(), ".dsh", "mcp.json");
28
+ return join(testHome(), ".dsh", "mcp.json");
23
29
  },
24
- async install(pkgSlug, srcDir, type) {
25
- void type;
26
- const base = existsSync(join(process.cwd(), ".dsh")) ? join(process.cwd(), ".dsh", "skills") : join(homedir(), ".dsh", "skills");
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
- const bases = [join(process.cwd(), ".dsh", "skills"), join(homedir(), ".dsh", "skills")];
31
- for (const b of bases)
32
- uninstallSkillFiles(pkgSlug, b);
35
+ sharedUninstall(paths(), pkgSlug);
36
+ unsyncAgentsMd(process.cwd(), pkgSlug);
33
37
  },
34
38
  async list() {
35
- const bases = [join(process.cwd(), ".dsh", "skills"), join(homedir(), ".dsh", "skills")];
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
- const bases = [join(process.cwd(), ".dsh", "skills"), join(homedir(), ".dsh", "skills")];
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 { installSkillFiles, uninstallSkillFiles, listDirNames } from "./types.js";
4
- import { existsSync } from "fs";
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
- installSkillFiles("generic", pkgSlug, srcDir, join(process.cwd(), ".forge", "packages"));
14
+ async install(pkgSlug, srcDir, _type, _meta) {
15
+ sharedInstall(paths(), pkgSlug, srcDir);
13
16
  },
14
17
  async uninstall(pkgSlug, _type) {
15
- uninstallSkillFiles(pkgSlug, join(process.cwd(), ".forge", "packages"));
18
+ sharedUninstall(paths(), pkgSlug);
16
19
  },
17
20
  async list() {
18
- return listDirNames(join(process.cwd(), ".forge", "packages"));
21
+ return sharedList(paths());
19
22
  },
20
23
  async isInstalled(pkgSlug) {
21
- return existsSync(join(process.cwd(), ".forge", "packages", pkgSlug));
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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles, listDirNames } from "./types.js";
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.1.0",
10
- detect: () => existsSync(join(process.cwd(), "opencode.json")) || existsSync(join(homedir(), ".config", "opencode", "opencode.json")),
11
- skillDir: (slug) => join(process.cwd(), ".opencode", "skills", slug),
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(homedir(), ".config", "opencode", "opencode.json");
25
+ return join(testHome(), ".config", "opencode", "opencode.json");
17
26
  },
18
- async install(pkgSlug, srcDir, type) {
19
- void type;
20
- const isProject = existsSync(join(process.cwd(), "opencode.json"));
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
- const isProject = existsSync(join(process.cwd(), "opencode.json"));
26
- const base = isProject ? join(process.cwd(), ".opencode", "skills") : join(homedir(), ".config", "opencode", "skills");
27
- uninstallSkillFiles(pkgSlug, base);
32
+ sharedUninstall(paths(), pkgSlug);
33
+ unsyncAgentsMd(process.cwd(), pkgSlug);
28
34
  },
29
35
  async list() {
30
- const bases = [join(process.cwd(), ".opencode", "skills"), join(homedir(), ".config", "opencode", "skills")];
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
- const bases = [join(process.cwd(), ".opencode", "skills"), join(homedir(), ".config", "opencode", "skills")];
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, cpSync, copyFileSync, symlinkSync, lstatSync, unlinkSync, renameSync } from "fs";
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(src, dest, { recursive: true });
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, opts = {}) {
68
+ export function writeMcpConfig(configPath, data) {
66
69
  ensureDir(dirname(configPath));
67
70
  backupFileIfExists(configPath);
68
- // Epoch 1d: Atomic write — temp dosyaya yaz, sonra rename (crash-safe)
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: dosya yoksa oluştur (taze makine desteği)
93
+ // Epoch 1e: create the file when missing (fresh-machine support)
91
94
  let cfg = readMcpConfig(configPath);
92
95
  if (cfg === null) {
93
- // Dosya yok veya boşyeni config oluştur
96
+ // File missing or emptycreate 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
- delete servers[name];
115
+ Reflect.deleteProperty(servers, name);
113
116
  writeMcpConfig(configPath, cfg);
114
117
  }
115
118
  }
116
- // Epoch 1d: type-aware hedef dizin
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 (Faz 12 yeni)
2
- import { existsSync } from "fs";
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 { homedir } from "os";
5
- import { installSkillFiles, uninstallSkillFiles, listDirNames } from "./types.js";
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.1.0",
10
- detect: () => existsSync(join(process.cwd(), ".windsurf")) || existsSync(join(homedir(), ".windsurf")) || existsSync(join(homedir(), ".codeium", "windsurf")),
11
- skillDir: (slug) => {
12
- if (existsSync(join(process.cwd(), ".windsurf")))
13
- return join(process.cwd(), ".windsurf", "skills", slug);
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 (existsSync(join(process.cwd(), ".windsurf")))
43
+ if (inProject())
18
44
  return join(process.cwd(), ".windsurf", "mcp_config.json");
19
- return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
45
+ return join(testHome(), ".codeium", "windsurf", "mcp_config.json");
20
46
  },
21
- async install(pkgSlug, srcDir, type) {
22
- void type;
23
- const base = existsSync(join(process.cwd(), ".windsurf")) ? join(process.cwd(), ".windsurf", "skills") : join(homedir(), ".windsurf", "skills");
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
- const bases = [join(process.cwd(), ".windsurf", "skills"), join(homedir(), ".windsurf", "skills")];
28
- for (const b of bases)
29
- uninstallSkillFiles(pkgSlug, b);
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
- const bases = [join(process.cwd(), ".windsurf", "skills"), join(homedir(), ".windsurf", "skills")];
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
- const bases = [join(process.cwd(), ".windsurf", "skills"), join(homedir(), ".windsurf", "skills")];
41
- return bases.some((b) => existsSync(join(b, pkgSlug)));
61
+ return sharedIsInstalled(paths(), pkgSlug);
42
62
  },
43
63
  };