cursor-template 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.
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parse } from 'smol-toml';
4
+ import { readManifest } from './manifest.js';
5
+ import { readCodexConfig, readCursorMcp } from './mcp.js';
6
+ import { componentDest } from './paths.js';
7
+ import { errorMessage } from '../utils/errors.js';
8
+ import { isWritableDir, listChildDirs, listChildFiles, pathExists } from '../utils/fs.js';
9
+ export async function doctor({ target, roots }) {
10
+ const issues = [];
11
+ const notes = [];
12
+ for (const [label, dir] of [
13
+ ['skills', roots.skills],
14
+ ['commands', roots.commands],
15
+ ['agents', roots.agents],
16
+ ]) {
17
+ if (await isWritableDir(dir))
18
+ notes.push(`writable ${label}: ${dir}`);
19
+ else
20
+ issues.push(`not writable ${label}: ${dir}`);
21
+ }
22
+ const manifest = await readManifest(roots.manifest);
23
+ const skillNames = new Set();
24
+ for (const dir of await listChildDirs(roots.skills)) {
25
+ skillNames.add(path.basename(dir));
26
+ if (!(await pathExists(path.join(dir, 'SKILL.md'))))
27
+ issues.push(`missing SKILL.md: ${dir}`);
28
+ }
29
+ const agentExt = target === 'codex' ? '.toml' : '.md';
30
+ for (const file of await listChildFiles(roots.agents, agentExt)) {
31
+ const text = await fs.readFile(file, 'utf8');
32
+ if (!text.trim()) {
33
+ issues.push(`empty agent file: ${file}`);
34
+ continue;
35
+ }
36
+ if (target === 'codex') {
37
+ try {
38
+ const doc = parse(text);
39
+ for (const key of ['name', 'description', 'developer_instructions']) {
40
+ if (!doc[key])
41
+ issues.push(`agent missing ${key}: ${file}`);
42
+ }
43
+ }
44
+ catch (error) {
45
+ issues.push(`agent TOML parse error: ${file}: ${errorMessage(error)}`);
46
+ }
47
+ }
48
+ }
49
+ if (await pathExists(roots.mcpFile)) {
50
+ const text = await fs.readFile(roots.mcpFile, 'utf8');
51
+ try {
52
+ if (target === 'cursor')
53
+ readCursorMcp(text);
54
+ else
55
+ readCodexConfig(text);
56
+ notes.push(`mcp config ok: ${roots.mcpFile}`);
57
+ }
58
+ catch (error) {
59
+ issues.push(`mcp parse error: ${roots.mcpFile}: ${errorMessage(error)}`);
60
+ }
61
+ }
62
+ else {
63
+ notes.push(`mcp config absent: ${roots.mcpFile}`);
64
+ }
65
+ for (const entry of Object.values(manifest.components)) {
66
+ if (entry.type === 'skill' || (target === 'codex' && entry.type === 'command')) {
67
+ if (!skillNames.has(entry.name))
68
+ issues.push(`missing installed ${entry.type}: ${entry.id}`);
69
+ }
70
+ else if (entry.type !== 'mcp') {
71
+ const dest = componentDest({ target, roots, type: entry.type, name: entry.name });
72
+ if (!(await pathExists(dest.path)))
73
+ issues.push(`missing installed ${entry.type}: ${entry.id}`);
74
+ }
75
+ }
76
+ return { issues, notes };
77
+ }
@@ -0,0 +1,29 @@
1
+ import type { Catalog, ComponentRecord, ComponentType, InstallRoots, Target } from './types.js';
2
+ export declare function installRecord({ target, roots, record, yes, dryRun, catalog, confirmImpl, }: {
3
+ target: Target;
4
+ roots: InstallRoots;
5
+ record: ComponentRecord;
6
+ yes?: boolean;
7
+ dryRun?: boolean;
8
+ catalog: Catalog;
9
+ confirmImpl?: (question: string) => Promise<boolean>;
10
+ }): Promise<true | 'skipped' | false>;
11
+ export declare function removeInstalled({ target, roots, type, id, yes, dryRun, confirmImpl, }: {
12
+ target: Target;
13
+ roots: InstallRoots;
14
+ type: ComponentType;
15
+ id: string;
16
+ yes?: boolean;
17
+ dryRun?: boolean;
18
+ confirmImpl?: (question: string) => Promise<boolean>;
19
+ }): Promise<boolean>;
20
+ export declare function sourceLine(record: ComponentRecord): string;
21
+ export declare function installById(options: {
22
+ target: Target;
23
+ roots: InstallRoots;
24
+ type: ComponentType;
25
+ id: string;
26
+ yes?: boolean;
27
+ dryRun?: boolean;
28
+ catalog: Catalog;
29
+ }): Promise<true | 'skipped' | false>;
@@ -0,0 +1,206 @@
1
+ import fs from 'node:fs/promises';
2
+ import { agentMarkdownToToml, cursorAgentMarkdown, wrapCommandAsSkill } from './adapters.js';
3
+ import { findComponent, readManifest, removeComponent, upsertComponent, writeManifest } from './manifest.js';
4
+ import { applyCodexMcp, applyCursorMcp, codexMcpConflicts, cursorMcpConflicts, envKeysFromServers, extractMcpServers, readCodexConfig, readCursorMcp, removeCodexServers, removeCursorServers, } from './mcp.js';
5
+ import { componentDest } from './paths.js';
6
+ import { githubBlobUrl } from '../services/github.js';
7
+ import { assertSafeId } from '../utils/ids.js';
8
+ import { backupOnce, confirm, isExecutableRel, pathExists, safeJoin, writeBytes } from '../utils/fs.js';
9
+ async function readTextIfExists(file) {
10
+ try {
11
+ return await fs.readFile(file, 'utf8');
12
+ }
13
+ catch (error) {
14
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
15
+ return '';
16
+ throw error;
17
+ }
18
+ }
19
+ async function shouldReplace(dest, confirmImpl, dryRun) {
20
+ if (!(await pathExists(dest.path)))
21
+ return true;
22
+ const ok = await confirmImpl(`Overwrite ${dest.path}? (y/N) `);
23
+ if (!ok) {
24
+ console.log(`skipped ${dest.path}`);
25
+ return false;
26
+ }
27
+ if (!dryRun && dest.kind === 'dir')
28
+ await fs.rm(dest.path, { recursive: true, force: true });
29
+ return true;
30
+ }
31
+ export async function installRecord({ target, roots, record, yes = false, dryRun = false, catalog, confirmImpl = (question) => confirm(question, { yes, dryRun }), }) {
32
+ const dest = componentDest({ target, roots, type: record.type, name: record.name });
33
+ if (record.type === 'skill') {
34
+ if (!record.files.some((file) => file.rel === 'SKILL.md'))
35
+ throw new Error(`SKILL.md not found in skill "${record.id}"`);
36
+ if (!(await shouldReplace(dest, confirmImpl, dryRun)))
37
+ return 'skipped';
38
+ for (const file of record.files) {
39
+ const buf = await catalog.readFile(file.repoPath);
40
+ if (!buf)
41
+ throw new Error(`Missing skill file ${file.repoPath}`);
42
+ await writeBytes(safeJoin(dest.path, file.rel), buf, { executable: isExecutableRel(file.rel), dryRun });
43
+ }
44
+ console.log(`${dryRun ? 'would install' : 'installed'} skill ${record.id}`);
45
+ console.log(` ${dest.path}`);
46
+ if (!dryRun)
47
+ await remember(roots, { type: 'skill', id: record.id, name: record.name });
48
+ return true;
49
+ }
50
+ if (record.type === 'command') {
51
+ if (!record.repoPath)
52
+ throw new Error(`Command "${record.id}" has no source file`);
53
+ const buf = await catalog.readFile(record.repoPath);
54
+ if (!buf)
55
+ throw new Error(`Command "${record.id}" not found`);
56
+ const markdown = buf.toString('utf8');
57
+ if (!(await shouldReplace(dest, confirmImpl, dryRun)))
58
+ return 'skipped';
59
+ if (target === 'cursor') {
60
+ await writeBytes(dest.path, markdown.endsWith('\n') ? markdown : `${markdown}\n`, { dryRun });
61
+ }
62
+ else {
63
+ const wrapped = wrapCommandAsSkill(markdown, record.name);
64
+ await writeBytes(safeJoin(dest.path, 'SKILL.md'), wrapped.skill, { dryRun });
65
+ await writeBytes(safeJoin(dest.path, 'agents/openai.yaml'), wrapped.openaiYaml, { dryRun });
66
+ }
67
+ console.log(`${dryRun ? 'would install' : 'installed'} command ${record.id}`);
68
+ console.log(` ${dest.path}`);
69
+ if (!dryRun)
70
+ await remember(roots, { type: 'command', id: record.id, name: record.name });
71
+ return true;
72
+ }
73
+ if (record.type === 'agent') {
74
+ if (!record.repoPath)
75
+ throw new Error(`Agent "${record.id}" has no source file`);
76
+ const buf = await catalog.readFile(record.repoPath);
77
+ if (!buf)
78
+ throw new Error(`Agent "${record.id}" not found`);
79
+ const markdown = buf.toString('utf8');
80
+ if (!(await shouldReplace(dest, confirmImpl, dryRun)))
81
+ return 'skipped';
82
+ const output = target === 'cursor' ? cursorAgentMarkdown(markdown) : agentMarkdownToToml(markdown, record.name);
83
+ await writeBytes(dest.path, output, { dryRun });
84
+ console.log(`${dryRun ? 'would install' : 'installed'} agent ${record.id}`);
85
+ console.log(` ${dest.path}`);
86
+ if (!dryRun)
87
+ await remember(roots, { type: 'agent', id: record.id, name: record.name });
88
+ return true;
89
+ }
90
+ if (record.type === 'mcp') {
91
+ if (!record.repoPath)
92
+ throw new Error(`MCP "${record.id}" has no source file`);
93
+ const buf = await catalog.readFile(record.repoPath);
94
+ if (!buf)
95
+ throw new Error(`MCP "${record.id}" not found`);
96
+ const servers = extractMcpServers(JSON.parse(buf.toString('utf8')));
97
+ const existing = await readTextIfExists(dest.path);
98
+ if (!(await mergeMcp({ target, dest: dest.path, existing, servers, dryRun, confirmImpl })))
99
+ return false;
100
+ const keys = envKeysFromServers(servers);
101
+ console.log(`${dryRun ? 'would install' : 'installed'} mcp ${record.id}`);
102
+ console.log(` ${dest.path} (${Object.keys(servers).join(', ')})`);
103
+ if (keys.length)
104
+ console.log(` env: ${keys.join(', ')}`);
105
+ if (!dryRun)
106
+ await remember(roots, { type: 'mcp', id: record.id, name: record.name, mcpServers: Object.keys(servers) });
107
+ return true;
108
+ }
109
+ throw new Error(`Unknown component type "${record.type}"`);
110
+ }
111
+ async function mergeMcp({ target, dest, existing, servers, dryRun, confirmImpl, }) {
112
+ if (target === 'cursor') {
113
+ const doc = readCursorMcp(existing);
114
+ const conflicts = cursorMcpConflicts(doc, servers);
115
+ const overwrite = new Set();
116
+ for (const id of conflicts) {
117
+ if (await confirmImpl(`Overwrite MCP server "${id}" in ${dest}? (y/N) `))
118
+ overwrite.add(id);
119
+ else
120
+ console.log(`kept existing MCP server ${id}`);
121
+ }
122
+ if (dryRun) {
123
+ for (const id of Object.keys(servers)) {
124
+ if (conflicts.includes(id) && !overwrite.has(id))
125
+ continue;
126
+ console.log(`would merge MCP server ${id} into ${dest}`);
127
+ }
128
+ return true;
129
+ }
130
+ await writeBytes(dest, applyCursorMcp(doc, servers, overwrite));
131
+ return true;
132
+ }
133
+ const doc = readCodexConfig(existing);
134
+ const conflicts = codexMcpConflicts(doc, servers);
135
+ const overwrite = new Set();
136
+ for (const id of conflicts) {
137
+ if (await confirmImpl(`Overwrite MCP server "${id}" in ${dest}? (y/N) `))
138
+ overwrite.add(id);
139
+ else
140
+ console.log(`kept existing MCP server ${id}`);
141
+ }
142
+ if (dryRun) {
143
+ for (const id of Object.keys(servers)) {
144
+ if (conflicts.includes(id) && !overwrite.has(id))
145
+ continue;
146
+ console.log(`would merge MCP server ${id} into ${dest}`);
147
+ }
148
+ return true;
149
+ }
150
+ if (existing.trim())
151
+ await backupOnce(dest);
152
+ const next = applyCodexMcp(doc, servers, overwrite);
153
+ await writeBytes(dest, next.endsWith('\n') ? next : `${next}\n`);
154
+ return true;
155
+ }
156
+ async function remember(roots, entry) {
157
+ const manifest = await readManifest(roots.manifest);
158
+ upsertComponent(manifest, entry);
159
+ await writeManifest(roots.manifest, manifest);
160
+ }
161
+ export async function removeInstalled({ target, roots, type, id, yes = false, dryRun = false, confirmImpl = (question) => confirm(question, { yes, dryRun }), }) {
162
+ const manifest = await readManifest(roots.manifest);
163
+ const entry = findComponent(manifest, type, id);
164
+ if (!entry) {
165
+ console.log(`${type} "${id}" is not installed`);
166
+ return false;
167
+ }
168
+ const dest = componentDest({ target, roots, type: entry.type, name: entry.name });
169
+ const ok = await confirmImpl(`Remove ${entry.id} from ${dest.path}? (y/N) `);
170
+ if (!ok && !yes && !dryRun) {
171
+ console.log(`skipped ${entry.id}`);
172
+ return false;
173
+ }
174
+ if (dryRun) {
175
+ console.log(`would remove ${entry.type} ${entry.id}`);
176
+ console.log(` ${dest.path}`);
177
+ return true;
178
+ }
179
+ if (entry.type === 'mcp') {
180
+ const existing = await readTextIfExists(dest.path);
181
+ if (existing.trim()) {
182
+ if (target === 'cursor')
183
+ await writeBytes(dest.path, removeCursorServers(readCursorMcp(existing), entry.mcpServers || []));
184
+ else {
185
+ const next = removeCodexServers(readCodexConfig(existing), entry.mcpServers || []);
186
+ await writeBytes(dest.path, next.endsWith('\n') ? next : `${next}\n`);
187
+ }
188
+ }
189
+ }
190
+ else if (dest.kind === 'dir')
191
+ await fs.rm(dest.path, { recursive: true, force: true });
192
+ else
193
+ await fs.rm(dest.path, { force: true });
194
+ removeComponent(manifest, entry);
195
+ await writeManifest(roots.manifest, manifest);
196
+ console.log(`removed ${entry.type} ${entry.id}`);
197
+ return true;
198
+ }
199
+ export function sourceLine(record) {
200
+ return githubBlobUrl(record.primaryPath);
201
+ }
202
+ export async function installById(options) {
203
+ assertSafeId(options.id);
204
+ const record = await options.catalog.resolve(options.type, options.id);
205
+ return installRecord({ ...options, record });
206
+ }
@@ -0,0 +1,14 @@
1
+ import type { ComponentType, InstallRoots, Target } from './types.js';
2
+ export interface InstalledItem {
3
+ type: ComponentType;
4
+ id: string;
5
+ name: string;
6
+ path: string;
7
+ mcpServers: string[];
8
+ present: boolean;
9
+ tracked: boolean;
10
+ }
11
+ export declare function collectInstalled({ target, roots }: {
12
+ target: Target;
13
+ roots: InstallRoots;
14
+ }): Promise<InstalledItem[]>;
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readManifest } from './manifest.js';
4
+ import { readCodexConfig, readCursorMcp } from './mcp.js';
5
+ import { componentDest } from './paths.js';
6
+ import { listChildDirs, listChildFiles, pathExists } from '../utils/fs.js';
7
+ export async function collectInstalled({ target, roots }) {
8
+ const manifest = await readManifest(roots.manifest);
9
+ const items = [];
10
+ const seen = new Set();
11
+ for (const entry of Object.values(manifest.components)) {
12
+ const dest = componentDest({ target, roots, type: entry.type, name: entry.name });
13
+ items.push({
14
+ type: entry.type,
15
+ id: entry.id,
16
+ name: entry.name,
17
+ path: dest.path,
18
+ mcpServers: entry.mcpServers || [],
19
+ present: await pathExists(dest.path),
20
+ tracked: true,
21
+ });
22
+ seen.add(`${entry.type}:${entry.name}`);
23
+ }
24
+ for (const dir of await listChildDirs(roots.skills)) {
25
+ const name = path.basename(dir);
26
+ if (seen.has(`skill:${name}`) || seen.has(`command:${name}`))
27
+ continue;
28
+ items.push({ type: 'skill', id: name, name, path: dir, mcpServers: [], present: true, tracked: false });
29
+ }
30
+ if (target === 'cursor') {
31
+ for (const file of await listChildFiles(roots.commands, '.md')) {
32
+ const name = path.basename(file, '.md');
33
+ if (seen.has(`command:${name}`))
34
+ continue;
35
+ items.push({ type: 'command', id: name, name, path: file, mcpServers: [], present: true, tracked: false });
36
+ }
37
+ }
38
+ const agentExt = target === 'codex' ? '.toml' : '.md';
39
+ for (const file of await listChildFiles(roots.agents, agentExt)) {
40
+ const name = path.basename(file, agentExt);
41
+ if (seen.has(`agent:${name}`))
42
+ continue;
43
+ items.push({ type: 'agent', id: name, name, path: file, mcpServers: [], present: true, tracked: false });
44
+ }
45
+ const knownServers = new Set(items.flatMap((item) => item.mcpServers));
46
+ if (await pathExists(roots.mcpFile)) {
47
+ const text = await fs.readFile(roots.mcpFile, 'utf8');
48
+ const servers = target === 'cursor'
49
+ ? Object.keys(readCursorMcp(text).mcpServers || {})
50
+ : Object.keys(readCodexConfig(text).mcp_servers || {});
51
+ for (const name of servers) {
52
+ if (knownServers.has(name))
53
+ continue;
54
+ items.push({ type: 'mcp', id: name, name, path: roots.mcpFile, mcpServers: [name], present: true, tracked: false });
55
+ }
56
+ }
57
+ return items.sort((a, b) => `${a.type}:${a.id}`.localeCompare(`${b.type}:${b.id}`));
58
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentType, Manifest, ManifestEntry } from './types.js';
2
+ export declare function emptyManifest(): Manifest;
3
+ export declare function manifestKey(type: ComponentType, id: string): string;
4
+ export declare function readManifest(file: string): Promise<Manifest>;
5
+ export declare function writeManifest(file: string, manifest: Manifest): Promise<void>;
6
+ export declare function upsertComponent(manifest: Manifest, entry: Omit<ManifestEntry, 'mcpServers'> & {
7
+ mcpServers?: string[];
8
+ }): Manifest;
9
+ export declare function findComponent(manifest: Manifest, type: ComponentType, idOrName: string): ManifestEntry | null;
10
+ export declare function removeComponent(manifest: Manifest, entry: ManifestEntry): Manifest;
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { errorMessage } from '../utils/errors.js';
4
+ function isEnoent(error) {
5
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
6
+ }
7
+ export function emptyManifest() {
8
+ return { version: 1, components: {} };
9
+ }
10
+ export function manifestKey(type, id) {
11
+ return `${type}:${id}`;
12
+ }
13
+ export async function readManifest(file) {
14
+ try {
15
+ const text = await fs.readFile(file, 'utf8');
16
+ const data = JSON.parse(text);
17
+ if (!data || typeof data !== 'object' || Array.isArray(data))
18
+ return emptyManifest();
19
+ if (!data.components || typeof data.components !== 'object')
20
+ data.components = {};
21
+ return data;
22
+ }
23
+ catch (error) {
24
+ if (isEnoent(error))
25
+ return emptyManifest();
26
+ throw new Error(`Cannot read install manifest ${file}: ${errorMessage(error)}`, { cause: error });
27
+ }
28
+ }
29
+ export async function writeManifest(file, manifest) {
30
+ await fs.mkdir(path.dirname(file), { recursive: true });
31
+ await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
32
+ }
33
+ export function upsertComponent(manifest, entry) {
34
+ manifest.components[manifestKey(entry.type, entry.id)] = {
35
+ type: entry.type,
36
+ id: entry.id,
37
+ name: entry.name,
38
+ mcpServers: entry.mcpServers || [],
39
+ };
40
+ return manifest;
41
+ }
42
+ export function findComponent(manifest, type, idOrName) {
43
+ const direct = manifest.components[manifestKey(type, idOrName)];
44
+ if (direct)
45
+ return direct;
46
+ const matches = Object.values(manifest.components).filter((entry) => {
47
+ if (entry.type !== type)
48
+ return false;
49
+ if (entry.id === idOrName || entry.name === idOrName)
50
+ return true;
51
+ return Array.isArray(entry.mcpServers) && entry.mcpServers.includes(idOrName);
52
+ });
53
+ if (matches.length === 1)
54
+ return matches[0] ?? null;
55
+ if (matches.length > 1) {
56
+ throw new Error(`${type} "${idOrName}" matches multiple installs: ${matches.map((item) => item.id).join(', ')}`);
57
+ }
58
+ return null;
59
+ }
60
+ export function removeComponent(manifest, entry) {
61
+ delete manifest.components[manifestKey(entry.type, entry.id)];
62
+ return manifest;
63
+ }
@@ -0,0 +1,22 @@
1
+ export type McpServers = Record<string, Record<string, unknown>>;
2
+ type CursorDoc = {
3
+ mcpServers: McpServers;
4
+ [key: string]: unknown;
5
+ };
6
+ type CodexDoc = {
7
+ mcp_servers?: McpServers;
8
+ [key: string]: unknown;
9
+ };
10
+ export declare function stable(value: unknown): string;
11
+ export declare function extractMcpServers(json: unknown): McpServers;
12
+ export declare function envKeysFromServers(servers: McpServers): string[];
13
+ export declare function toCodexServer(cfg: Record<string, unknown>): Record<string, unknown>;
14
+ export declare function readCursorMcp(existingText: string): CursorDoc;
15
+ export declare function cursorMcpConflicts(doc: CursorDoc, incoming: McpServers): string[];
16
+ export declare function applyCursorMcp(doc: CursorDoc, incoming: McpServers, overwriteIds: Set<string> | string[]): string;
17
+ export declare function removeCursorServers(doc: CursorDoc, ids: string[]): string;
18
+ export declare function readCodexConfig(existingText: string): CodexDoc;
19
+ export declare function codexMcpConflicts(doc: CodexDoc, incoming: McpServers): string[];
20
+ export declare function applyCodexMcp(doc: CodexDoc, incoming: McpServers, overwriteIds: Set<string> | string[]): string;
21
+ export declare function removeCodexServers(doc: CodexDoc, ids: string[]): string;
22
+ export {};
@@ -0,0 +1,130 @@
1
+ import { parse, stringify } from 'smol-toml';
2
+ const CODEX_KEYS = ['command', 'args', 'url', 'env', 'headers', 'bearer_token_env_var'];
3
+ export function stable(value) {
4
+ return JSON.stringify(sortValue(value));
5
+ }
6
+ function sortValue(value) {
7
+ if (Array.isArray(value))
8
+ return value.map(sortValue);
9
+ if (value && typeof value === 'object') {
10
+ return Object.keys(value)
11
+ .sort()
12
+ .reduce((acc, key) => {
13
+ acc[key] = sortValue(value[key]);
14
+ return acc;
15
+ }, {});
16
+ }
17
+ return value;
18
+ }
19
+ export function extractMcpServers(json) {
20
+ const record = json && typeof json === 'object' && !Array.isArray(json) ? json : null;
21
+ const nested = record?.mcpServers;
22
+ const source = nested && typeof nested === 'object' && !Array.isArray(nested) ? nested : record;
23
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
24
+ throw new Error('MCP catalog file has no mcpServers object');
25
+ }
26
+ const servers = {};
27
+ for (const [id, cfg] of Object.entries(source)) {
28
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
29
+ continue;
30
+ const next = { ...cfg };
31
+ delete next.description;
32
+ servers[id] = next;
33
+ }
34
+ if (!Object.keys(servers).length)
35
+ throw new Error('MCP catalog file did not contain any servers');
36
+ return servers;
37
+ }
38
+ export function envKeysFromServers(servers) {
39
+ const keys = [];
40
+ for (const cfg of Object.values(servers)) {
41
+ const env = cfg.env;
42
+ if (env && typeof env === 'object' && !Array.isArray(env))
43
+ keys.push(...Object.keys(env));
44
+ }
45
+ return [...new Set(keys)];
46
+ }
47
+ export function toCodexServer(cfg) {
48
+ const out = {};
49
+ for (const key of CODEX_KEYS) {
50
+ if (cfg[key] !== undefined)
51
+ out[key] = cfg[key];
52
+ }
53
+ return out;
54
+ }
55
+ export function readCursorMcp(existingText) {
56
+ if (!existingText || !existingText.trim())
57
+ return { mcpServers: {} };
58
+ const doc = JSON.parse(existingText);
59
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
60
+ throw new Error('mcp.json must be a JSON object');
61
+ const record = doc;
62
+ if (!record.mcpServers || typeof record.mcpServers !== 'object' || Array.isArray(record.mcpServers)) {
63
+ record.mcpServers = {};
64
+ }
65
+ return record;
66
+ }
67
+ export function cursorMcpConflicts(doc, incoming) {
68
+ const conflicts = [];
69
+ for (const [id, cfg] of Object.entries(incoming)) {
70
+ const prev = doc.mcpServers[id];
71
+ if (prev && stable(prev) !== stable(cfg))
72
+ conflicts.push(id);
73
+ }
74
+ return conflicts;
75
+ }
76
+ export function applyCursorMcp(doc, incoming, overwriteIds) {
77
+ const allow = overwriteIds instanceof Set ? overwriteIds : new Set(overwriteIds);
78
+ for (const [id, cfg] of Object.entries(incoming)) {
79
+ if (doc.mcpServers[id] && stable(doc.mcpServers[id]) !== stable(cfg) && !allow.has(id))
80
+ continue;
81
+ doc.mcpServers[id] = cfg;
82
+ }
83
+ return `${JSON.stringify(doc, null, 2)}\n`;
84
+ }
85
+ export function removeCursorServers(doc, ids) {
86
+ for (const id of ids)
87
+ delete doc.mcpServers[id];
88
+ return `${JSON.stringify(doc, null, 2)}\n`;
89
+ }
90
+ export function readCodexConfig(existingText) {
91
+ if (!existingText || !existingText.trim())
92
+ return { mcp_servers: {} };
93
+ const doc = parse(existingText);
94
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
95
+ return {};
96
+ if (!doc.mcp_servers || typeof doc.mcp_servers !== 'object' || Array.isArray(doc.mcp_servers)) {
97
+ doc.mcp_servers = {};
98
+ }
99
+ return doc;
100
+ }
101
+ export function codexMcpConflicts(doc, incoming) {
102
+ const conflicts = [];
103
+ const servers = doc.mcp_servers ?? {};
104
+ for (const [id, cfg] of Object.entries(incoming)) {
105
+ const prev = servers[id];
106
+ if (prev && stable(prev) !== stable(toCodexServer(cfg)))
107
+ conflicts.push(id);
108
+ }
109
+ return conflicts;
110
+ }
111
+ export function applyCodexMcp(doc, incoming, overwriteIds) {
112
+ const allow = overwriteIds instanceof Set ? overwriteIds : new Set(overwriteIds);
113
+ if (!doc.mcp_servers)
114
+ doc.mcp_servers = {};
115
+ for (const [id, cfg] of Object.entries(incoming)) {
116
+ const mapped = toCodexServer(cfg);
117
+ const prev = doc.mcp_servers[id];
118
+ if (prev && stable(prev) !== stable(mapped) && !allow.has(id))
119
+ continue;
120
+ doc.mcp_servers[id] = mapped;
121
+ }
122
+ return stringify(doc);
123
+ }
124
+ export function removeCodexServers(doc, ids) {
125
+ if (!doc.mcp_servers)
126
+ doc.mcp_servers = {};
127
+ for (const id of ids)
128
+ delete doc.mcp_servers[id];
129
+ return stringify(doc);
130
+ }
@@ -0,0 +1,13 @@
1
+ import type { ComponentType, Dest, InstallRoots, Scope, Target } from './types.js';
2
+ export declare function destRoots({ target, scope, directory, home, }: {
3
+ target: Target;
4
+ scope?: Scope;
5
+ directory?: string;
6
+ home?: string;
7
+ }): InstallRoots;
8
+ export declare function componentDest({ target, roots, type, name, }: {
9
+ target: Target;
10
+ roots: InstallRoots;
11
+ type: ComponentType;
12
+ name: string;
13
+ }): Dest;