@vovy-ai/host-detect 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +237 -0
- package/package.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vovy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every path-touching function takes an explicit `DetectEnv` instead of reading
|
|
3
|
+
* `process.env`/`os.homedir()` directly. That's what lets tests redirect `home`/`cwd`
|
|
4
|
+
* to a throwaway `os.tmpdir()` and snapshot exactly what would be written, without ever
|
|
5
|
+
* touching the real machine's dotfiles.
|
|
6
|
+
*/
|
|
7
|
+
interface DetectEnv {
|
|
8
|
+
home: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
platform: NodeJS.Platform;
|
|
11
|
+
}
|
|
12
|
+
type SkillScope = "user" | "project";
|
|
13
|
+
interface McpServerEntry {
|
|
14
|
+
/** Server id/key as it should appear in the host's config, e.g. "vovy". */
|
|
15
|
+
id: string;
|
|
16
|
+
command: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
}
|
|
19
|
+
interface HostAdapter {
|
|
20
|
+
/** Stable id, used with `--host <id>` and in test fixtures. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Human-readable name shown in CLI output. */
|
|
23
|
+
label: string;
|
|
24
|
+
/**
|
|
25
|
+
* Best-effort signal that this host tool is installed/configured on this machine.
|
|
26
|
+
* Should never throw — return false on any uncertainty rather than guessing.
|
|
27
|
+
*/
|
|
28
|
+
detect(env: DetectEnv): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Absolute path Vovy should write the skill identified by `skillId` to, for this host
|
|
31
|
+
* at the given scope. Hosts differ on convention — some use a folder-per-skill with a
|
|
32
|
+
* fixed filename (Claude Code/Codex: `<dir>/<skillId>/SKILL.md`), others use one flat
|
|
33
|
+
* file per skill (Cline/Windsurf: `<dir>/<skillId>.md`) — so this returns the full path
|
|
34
|
+
* rather than exposing a directory + fixed filename that wouldn't fit both shapes.
|
|
35
|
+
*/
|
|
36
|
+
skillFilePath(env: DetectEnv, scope: SkillScope, skillId: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Path to this host's MCP server config file, or null if this host is skills-only
|
|
39
|
+
* (no MCP registration support/needed).
|
|
40
|
+
*/
|
|
41
|
+
mcpConfigPath(env: DetectEnv, scope: SkillScope): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Merge `entry` into the raw contents of an existing config file (`existing`, undefined
|
|
44
|
+
* if the file doesn't exist yet) and return the full new file contents to write.
|
|
45
|
+
* Must be idempotent — merging the same entry twice produces the same result as once.
|
|
46
|
+
*/
|
|
47
|
+
mergeMcpConfig(existing: string | undefined, entry: McpServerEntry): string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Adapters that are fully verified against the host's official documentation and are
|
|
51
|
+
* safe to auto-detect by default.
|
|
52
|
+
*/
|
|
53
|
+
declare const VERIFIED_ADAPTER_IDS: readonly ["claude-code", "codex"];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Claude Code — Agent Skills at `~/.claude/skills/<id>/SKILL.md` (personal, user scope)
|
|
57
|
+
* or `.claude/skills/<id>/SKILL.md` (project scope, committed to the repo). Project-scoped
|
|
58
|
+
* MCP servers register in a `.mcp.json` file at the project root.
|
|
59
|
+
* https://code.claude.com/docs/en/skills
|
|
60
|
+
*/
|
|
61
|
+
declare const claudeCodeAdapter: HostAdapter;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Codex CLI — Agent Skills at `.agents/skills/<id>/SKILL.md`, scanned from the current
|
|
65
|
+
* directory up to the repo root for project scope, plus a user-level location for
|
|
66
|
+
* personal skills. MCP servers register as `[mcp_servers.<id>]` tables in `config.toml`.
|
|
67
|
+
* https://developers.openai.com/codex/skills
|
|
68
|
+
*
|
|
69
|
+
* The project-scope skill path is directly confirmed by Codex's own docs. The user-scope
|
|
70
|
+
* skill path below (`~/.codex/skills/`) mirrors the confirmed location of `config.toml`
|
|
71
|
+
* but has not been independently verified against a live install — flagged in
|
|
72
|
+
* docs/host-support-matrix.md as a good first contribution to confirm/fix.
|
|
73
|
+
*/
|
|
74
|
+
declare const codexAdapter: HostAdapter;
|
|
75
|
+
/**
|
|
76
|
+
* Removes a `[mcp_servers.<id>]` table block from a Codex `config.toml`. Deliberately
|
|
77
|
+
* conservative: only strips the block this adapter would itself have written, leaving
|
|
78
|
+
* everything else in a hand-edited TOML file untouched. Returns `null` if the table isn't
|
|
79
|
+
* present, so the caller can skip writing.
|
|
80
|
+
*/
|
|
81
|
+
declare function removeCodexMcpEntry(existing: string | undefined, id: string): string | null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cursor — project rules live at `.cursor/rules/<id>.mdc`; MCP servers register in
|
|
85
|
+
* `.cursor/mcp.json` (project scope) or `~/.cursor/mcp.json` (global scope), both using
|
|
86
|
+
* the same `{ "mcpServers": {...} }` shape as Claude Code. https://docs.cursor.com/context/mcp
|
|
87
|
+
*
|
|
88
|
+
* BEST EFFORT: Cursor also ships a newer native "Skills" concept alongside Rules as of
|
|
89
|
+
* mid-2026, but its exact on-disk convention wasn't independently confirmed during
|
|
90
|
+
* research. This adapter targets the older, well-documented `.mdc` Rules mechanism,
|
|
91
|
+
* which Cursor's own agent loop reliably picks up. If you can confirm Cursor's Skills
|
|
92
|
+
* path, that's a great first PR — see docs/host-support-matrix.md.
|
|
93
|
+
*/
|
|
94
|
+
declare const cursorAdapter: HostAdapter;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Cline — project instructions live as flat markdown files under `.clinerules/`, one
|
|
98
|
+
* file per concern rather than Claude/Codex's folder-per-skill convention; Cline's own
|
|
99
|
+
* agent loop reads every file in that directory as always-on context.
|
|
100
|
+
*
|
|
101
|
+
* BEST EFFORT: Cline is a VS Code extension, not a standalone CLI/config-dir tool, so
|
|
102
|
+
* there's no reliable filesystem signal to auto-detect it's installed — `detect()` here
|
|
103
|
+
* always returns false, meaning Cline is only set up via an explicit `--host cline` flag,
|
|
104
|
+
* never silently. MCP registration is intentionally unimplemented for v0.1 (Cline's MCP
|
|
105
|
+
* config lives inside VS Code's extension storage, which varies by OS/VS Code variant
|
|
106
|
+
* and wasn't independently confirmed) — the `.clinerules/` skill file alone is enough for
|
|
107
|
+
* Cline's agent loop to pick up Vovy's skills. See docs/host-support-matrix.md.
|
|
108
|
+
*/
|
|
109
|
+
declare const clineAdapter: HostAdapter;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Windsurf — project rules live as flat markdown files under `.windsurf/rules/`, read by
|
|
113
|
+
* Cascade (Windsurf's agent) as always-on context, similar in shape to Cline's convention.
|
|
114
|
+
* Global config, including MCP servers, lives under `~/.codeium/windsurf/`.
|
|
115
|
+
*
|
|
116
|
+
* BEST EFFORT: the exact MCP config filename/shape below was not independently confirmed
|
|
117
|
+
* against a live Windsurf install during research — flagged in
|
|
118
|
+
* docs/host-support-matrix.md as a good first contribution to verify/fix.
|
|
119
|
+
*/
|
|
120
|
+
declare const windsurfAdapter: HostAdapter;
|
|
121
|
+
|
|
122
|
+
/** Every adapter Vovy ships, in the order they're listed in CLI output. */
|
|
123
|
+
declare const ADAPTERS: HostAdapter[];
|
|
124
|
+
declare function getAdapter(id: string): HostAdapter;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Inverse of `mergeJsonMcpConfig`: removes only the `mcpServers.<id>` entry, leaving every
|
|
128
|
+
* other key untouched. Returns `null` when there's nothing to do (file missing, not valid
|
|
129
|
+
* JSON, or the entry was never present) so callers can skip writing rather than risk
|
|
130
|
+
* clobbering a file they don't understand.
|
|
131
|
+
*/
|
|
132
|
+
declare function removeJsonMcpEntry(existing: string | undefined, id: string): string | null;
|
|
133
|
+
|
|
134
|
+
type WriteAction = "created" | "updated" | "unchanged";
|
|
135
|
+
interface WriteResult {
|
|
136
|
+
path: string;
|
|
137
|
+
action: WriteAction;
|
|
138
|
+
/** True if this result came from a dry run — `action` reflects what *would* happen,
|
|
139
|
+
* nothing was actually written to disk. */
|
|
140
|
+
dryRun: boolean;
|
|
141
|
+
}
|
|
142
|
+
interface WriteSkillOptions {
|
|
143
|
+
adapter: HostAdapter;
|
|
144
|
+
env: DetectEnv;
|
|
145
|
+
scope: SkillScope;
|
|
146
|
+
skillId: string;
|
|
147
|
+
content: string;
|
|
148
|
+
dryRun?: boolean;
|
|
149
|
+
}
|
|
150
|
+
declare function writeSkillFile(opts: WriteSkillOptions): WriteResult;
|
|
151
|
+
interface WriteMcpConfigOptions {
|
|
152
|
+
adapter: HostAdapter;
|
|
153
|
+
env: DetectEnv;
|
|
154
|
+
scope: SkillScope;
|
|
155
|
+
entry: McpServerEntry;
|
|
156
|
+
dryRun?: boolean;
|
|
157
|
+
}
|
|
158
|
+
declare function writeMcpConfig(opts: WriteMcpConfigOptions): WriteResult | null;
|
|
159
|
+
|
|
160
|
+
export { ADAPTERS, type DetectEnv, type HostAdapter, type McpServerEntry, type SkillScope, VERIFIED_ADAPTER_IDS, type WriteAction, type WriteMcpConfigOptions, type WriteResult, type WriteSkillOptions, claudeCodeAdapter, clineAdapter, codexAdapter, cursorAdapter, getAdapter, removeCodexMcpEntry, removeJsonMcpEntry, windsurfAdapter, writeMcpConfig, writeSkillFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/adapters/claude-code.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
// src/json-mcp-config.ts
|
|
6
|
+
function mergeJsonMcpConfig(existing, entry) {
|
|
7
|
+
let config = {};
|
|
8
|
+
if (existing?.trim()) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(existing);
|
|
11
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12
|
+
config = parsed;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Existing MCP config is not valid JSON \u2014 refusing to overwrite it. Please fix or remove it and re-run `vovy install`."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const existingServers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers) ? config.mcpServers : {};
|
|
21
|
+
config.mcpServers = {
|
|
22
|
+
...existingServers,
|
|
23
|
+
[entry.id]: {
|
|
24
|
+
command: entry.command,
|
|
25
|
+
args: entry.args
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return `${JSON.stringify(config, null, 2)}
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
function removeJsonMcpEntry(existing, id) {
|
|
32
|
+
if (!existing?.trim()) return null;
|
|
33
|
+
let config;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(existing);
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
37
|
+
config = parsed;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const servers = config.mcpServers;
|
|
42
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers) || !(id in servers)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const { [id]: _removed, ...rest } = servers;
|
|
46
|
+
config.mcpServers = Object.keys(rest).length > 0 ? rest : void 0;
|
|
47
|
+
return `${JSON.stringify(config, null, 2)}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/adapters/claude-code.ts
|
|
52
|
+
var claudeCodeAdapter = {
|
|
53
|
+
id: "claude-code",
|
|
54
|
+
label: "Claude Code",
|
|
55
|
+
detect(env) {
|
|
56
|
+
return existsSync(join(env.home, ".claude"));
|
|
57
|
+
},
|
|
58
|
+
skillFilePath(env, scope, skillId) {
|
|
59
|
+
const base = scope === "user" ? join(env.home, ".claude") : join(env.cwd, ".claude");
|
|
60
|
+
return join(base, "skills", skillId, "SKILL.md");
|
|
61
|
+
},
|
|
62
|
+
mcpConfigPath(env, scope) {
|
|
63
|
+
return scope === "user" ? join(env.home, ".claude", "mcp.json") : join(env.cwd, ".mcp.json");
|
|
64
|
+
},
|
|
65
|
+
mergeMcpConfig: mergeJsonMcpConfig
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/adapters/cline.ts
|
|
69
|
+
import "fs";
|
|
70
|
+
import { join as join2 } from "path";
|
|
71
|
+
var clineAdapter = {
|
|
72
|
+
id: "cline",
|
|
73
|
+
label: "Cline",
|
|
74
|
+
detect(_env) {
|
|
75
|
+
return false;
|
|
76
|
+
},
|
|
77
|
+
skillFilePath(env, scope, skillId) {
|
|
78
|
+
const base = scope === "user" ? join2(env.home, ".clinerules") : join2(env.cwd, ".clinerules");
|
|
79
|
+
return join2(base, `${skillId}.md`);
|
|
80
|
+
},
|
|
81
|
+
mcpConfigPath(_env, _scope) {
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
mergeMcpConfig(_existing) {
|
|
85
|
+
throw new Error("Cline MCP config registration is not yet supported \u2014 skill files only.");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/adapters/codex.ts
|
|
90
|
+
import { existsSync as existsSync3 } from "fs";
|
|
91
|
+
import { join as join3 } from "path";
|
|
92
|
+
var codexAdapter = {
|
|
93
|
+
id: "codex",
|
|
94
|
+
label: "Codex CLI",
|
|
95
|
+
detect(env) {
|
|
96
|
+
return existsSync3(join3(env.home, ".codex"));
|
|
97
|
+
},
|
|
98
|
+
skillFilePath(env, scope, skillId) {
|
|
99
|
+
const base = scope === "user" ? join3(env.home, ".codex", "skills") : join3(env.cwd, ".agents", "skills");
|
|
100
|
+
return join3(base, skillId, "SKILL.md");
|
|
101
|
+
},
|
|
102
|
+
mcpConfigPath(env) {
|
|
103
|
+
return join3(env.home, ".codex", "config.toml");
|
|
104
|
+
},
|
|
105
|
+
mergeMcpConfig(existing, entry) {
|
|
106
|
+
const tableHeader = `[mcp_servers.${entry.id}]`;
|
|
107
|
+
const base = existing ?? "";
|
|
108
|
+
const argsToml = entry.args.map((a) => JSON.stringify(a)).join(", ");
|
|
109
|
+
const block = `${tableHeader}
|
|
110
|
+
command = ${JSON.stringify(entry.command)}
|
|
111
|
+
args = [${argsToml}]
|
|
112
|
+
`;
|
|
113
|
+
const found = findTableBlock(base, tableHeader);
|
|
114
|
+
if (found) {
|
|
115
|
+
const currentBlock = base.slice(found.start, found.end);
|
|
116
|
+
if (currentBlock.trim() === block.trim()) return base;
|
|
117
|
+
return base.slice(0, found.start) + block + base.slice(found.end);
|
|
118
|
+
}
|
|
119
|
+
const separator = base.length > 0 && !base.endsWith("\n\n") ? base.endsWith("\n") ? "\n" : "\n\n" : "";
|
|
120
|
+
return `${base}${separator}${block}`;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function findTableBlock(text, header) {
|
|
124
|
+
const start = text.indexOf(header);
|
|
125
|
+
if (start === -1) return null;
|
|
126
|
+
const rest = text.slice(start + header.length);
|
|
127
|
+
const nextHeaderMatch = rest.match(/\n\[/);
|
|
128
|
+
const end = nextHeaderMatch?.index != null ? start + header.length + nextHeaderMatch.index + 1 : text.length;
|
|
129
|
+
return { start, end };
|
|
130
|
+
}
|
|
131
|
+
function removeCodexMcpEntry(existing, id) {
|
|
132
|
+
if (!existing) return null;
|
|
133
|
+
const found = findTableBlock(existing, `[mcp_servers.${id}]`);
|
|
134
|
+
if (!found) return null;
|
|
135
|
+
return existing.slice(0, found.start) + existing.slice(found.end);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/adapters/cursor.ts
|
|
139
|
+
import { existsSync as existsSync4 } from "fs";
|
|
140
|
+
import { join as join4 } from "path";
|
|
141
|
+
var cursorAdapter = {
|
|
142
|
+
id: "cursor",
|
|
143
|
+
label: "Cursor",
|
|
144
|
+
detect(env) {
|
|
145
|
+
return existsSync4(join4(env.home, ".cursor"));
|
|
146
|
+
},
|
|
147
|
+
skillFilePath(env, scope, skillId) {
|
|
148
|
+
const base = scope === "user" ? join4(env.home, ".cursor") : join4(env.cwd, ".cursor");
|
|
149
|
+
return join4(base, "rules", `${skillId}.mdc`);
|
|
150
|
+
},
|
|
151
|
+
mcpConfigPath(env, scope) {
|
|
152
|
+
return scope === "user" ? join4(env.home, ".cursor", "mcp.json") : join4(env.cwd, ".cursor", "mcp.json");
|
|
153
|
+
},
|
|
154
|
+
mergeMcpConfig: mergeJsonMcpConfig
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/adapters/windsurf.ts
|
|
158
|
+
import { existsSync as existsSync5 } from "fs";
|
|
159
|
+
import { join as join5 } from "path";
|
|
160
|
+
var windsurfAdapter = {
|
|
161
|
+
id: "windsurf",
|
|
162
|
+
label: "Windsurf",
|
|
163
|
+
detect(env) {
|
|
164
|
+
return existsSync5(join5(env.home, ".codeium", "windsurf"));
|
|
165
|
+
},
|
|
166
|
+
skillFilePath(env, scope, skillId) {
|
|
167
|
+
const base = scope === "user" ? join5(env.home, ".codeium", "windsurf") : join5(env.cwd, ".windsurf");
|
|
168
|
+
return join5(base, "rules", `${skillId}.md`);
|
|
169
|
+
},
|
|
170
|
+
mcpConfigPath(env, _scope) {
|
|
171
|
+
return join5(env.home, ".codeium", "windsurf", "mcp_config.json");
|
|
172
|
+
},
|
|
173
|
+
mergeMcpConfig: mergeJsonMcpConfig
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/adapters/index.ts
|
|
177
|
+
var ADAPTERS = [
|
|
178
|
+
claudeCodeAdapter,
|
|
179
|
+
codexAdapter,
|
|
180
|
+
cursorAdapter,
|
|
181
|
+
clineAdapter,
|
|
182
|
+
windsurfAdapter
|
|
183
|
+
];
|
|
184
|
+
function getAdapter(id) {
|
|
185
|
+
const adapter = ADAPTERS.find((a) => a.id === id);
|
|
186
|
+
if (!adapter) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Unknown host id "${id}". Known hosts: ${ADAPTERS.map((a) => a.id).join(", ")}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return adapter;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/types.ts
|
|
195
|
+
var VERIFIED_ADAPTER_IDS = ["claude-code", "codex"];
|
|
196
|
+
|
|
197
|
+
// src/write.ts
|
|
198
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
199
|
+
import { dirname } from "path";
|
|
200
|
+
function writeSkillFile(opts) {
|
|
201
|
+
const path = opts.adapter.skillFilePath(opts.env, opts.scope, opts.skillId);
|
|
202
|
+
const existing = existsSync6(path) ? readFileSync(path, "utf8") : void 0;
|
|
203
|
+
const action = existing === opts.content ? "unchanged" : existing === void 0 ? "created" : "updated";
|
|
204
|
+
const dryRun = opts.dryRun ?? false;
|
|
205
|
+
if (!dryRun && action !== "unchanged") {
|
|
206
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
207
|
+
writeFileSync(path, opts.content, "utf8");
|
|
208
|
+
}
|
|
209
|
+
return { path, action, dryRun };
|
|
210
|
+
}
|
|
211
|
+
function writeMcpConfig(opts) {
|
|
212
|
+
const path = opts.adapter.mcpConfigPath(opts.env, opts.scope);
|
|
213
|
+
if (!path) return null;
|
|
214
|
+
const existing = existsSync6(path) ? readFileSync(path, "utf8") : void 0;
|
|
215
|
+
const next = opts.adapter.mergeMcpConfig(existing, opts.entry);
|
|
216
|
+
const action = existing === next ? "unchanged" : existing === void 0 ? "created" : "updated";
|
|
217
|
+
const dryRun = opts.dryRun ?? false;
|
|
218
|
+
if (!dryRun && action !== "unchanged") {
|
|
219
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
220
|
+
writeFileSync(path, next, "utf8");
|
|
221
|
+
}
|
|
222
|
+
return { path, action, dryRun };
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
ADAPTERS,
|
|
226
|
+
VERIFIED_ADAPTER_IDS,
|
|
227
|
+
claudeCodeAdapter,
|
|
228
|
+
clineAdapter,
|
|
229
|
+
codexAdapter,
|
|
230
|
+
cursorAdapter,
|
|
231
|
+
getAdapter,
|
|
232
|
+
removeCodexMcpEntry,
|
|
233
|
+
removeJsonMcpEntry,
|
|
234
|
+
windsurfAdapter,
|
|
235
|
+
writeMcpConfig,
|
|
236
|
+
writeSkillFile
|
|
237
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vovy-ai/host-detect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Per-host detection and safe config writing for Vovy: Claude Code, Codex CLI, Cursor, Cline, Windsurf.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"tsup": "^8.3.5",
|
|
14
|
+
"typescript": "^5.7.2",
|
|
15
|
+
"vitest": "^2.1.8"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
19
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
}
|
|
23
|
+
}
|