@evolvedqube/gmcp 0.1.0-alpha.5

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,84 @@
1
+ import inquirer from 'inquirer';
2
+ import { fetchServers } from '../api-client.js';
3
+ import { filterRemoteOnlyServers } from '../transform.js';
4
+ import { addCommand } from './add.js';
5
+ const EXIT_OPTION = '✓ Done (exit)';
6
+ const SHOW_MORE_OPTION = '→ Show next 10 servers';
7
+ const PAGE_SIZE = 10;
8
+ /**
9
+ * Browse and install MCP servers from the registry
10
+ */
11
+ export async function marketCommand() {
12
+ try {
13
+ console.log('Fetching servers from MCP Registry...');
14
+ // Fetch all servers from API
15
+ const allServers = await fetchServers();
16
+ // Filter out remote-only servers
17
+ const localServers = filterRemoteOnlyServers(allServers);
18
+ if (localServers.length === 0) {
19
+ console.log('No servers available.');
20
+ return;
21
+ }
22
+ // Deduplicate by server name (API may return multiple entries for same server with different packages)
23
+ const uniqueServers = Array.from(new Map(localServers.map(server => [server.name, server])).values());
24
+ // Sort alphabetically
25
+ const sortedServers = [...uniqueServers].sort((a, b) => a.name.localeCompare(b.name));
26
+ console.log(`\nFound ${sortedServers.length} unique server(s):\n`);
27
+ let currentPage = 0;
28
+ // Interactive loop with pagination
29
+ while (true) {
30
+ const startIndex = currentPage * PAGE_SIZE;
31
+ const endIndex = Math.min(startIndex + PAGE_SIZE, sortedServers.length);
32
+ const currentPageServers = sortedServers.slice(startIndex, endIndex);
33
+ const hasMore = endIndex < sortedServers.length;
34
+ console.log(`\nShowing ${startIndex + 1}-${endIndex} of ${sortedServers.length} servers:\n`);
35
+ // Format choices for current page
36
+ const choices = currentPageServers.map(server => ({
37
+ name: `${server.name} - ${server.description}`,
38
+ value: server.name,
39
+ }));
40
+ // Build options list
41
+ const allChoices = [
42
+ { name: EXIT_OPTION, value: EXIT_OPTION },
43
+ ...choices,
44
+ ];
45
+ // Add "Show more" option if there are more results
46
+ if (hasMore) {
47
+ allChoices.push({ name: SHOW_MORE_OPTION, value: SHOW_MORE_OPTION });
48
+ }
49
+ const answer = await inquirer.prompt([
50
+ {
51
+ type: 'rawlist',
52
+ name: 'server',
53
+ message: 'Select an MCP server to install:',
54
+ choices: allChoices,
55
+ pageSize: 15,
56
+ },
57
+ ]);
58
+ // Handle exit
59
+ if (answer.server === EXIT_OPTION) {
60
+ return;
61
+ }
62
+ // Handle show more
63
+ if (answer.server === SHOW_MORE_OPTION) {
64
+ currentPage++;
65
+ continue;
66
+ }
67
+ // Install selected server
68
+ try {
69
+ await addCommand(answer.server, {});
70
+ }
71
+ catch (error) {
72
+ console.error('Installation failed:', error instanceof Error ? error.message : String(error));
73
+ }
74
+ console.log(''); // Add spacing
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (error instanceof Error) {
79
+ console.error(`Error: ${error.message}`);
80
+ process.exit(1);
81
+ }
82
+ throw error;
83
+ }
84
+ }
@@ -0,0 +1,71 @@
1
+ import { readUserConfig } from '../config.js';
2
+ import { getAdapter } from '../adapters/index.js';
3
+ function padRight(str, width) {
4
+ return str + ' '.repeat(Math.max(0, width - str.length));
5
+ }
6
+ export async function platformsCommand() {
7
+ // Task 2.1: Load config
8
+ const userConfig = readUserConfig();
9
+ // Tasks 2.2 & 2.3: Handle missing or empty config
10
+ if (!userConfig || !userConfig.platforms || userConfig.platforms.length === 0) {
11
+ console.log('No platforms configured. Run `gmcp init` first.');
12
+ process.exit(1);
13
+ }
14
+ // Task 2.4: Extract platforms array
15
+ const configuredPlatforms = userConfig.platforms;
16
+ // Task 4.2: Platform ID to human-readable name mapping
17
+ const platformNames = {
18
+ 'claude-code': 'Claude Code',
19
+ 'copilot-cli': 'Copilot CLI',
20
+ 'windsurf': 'Windsurf',
21
+ };
22
+ const tableRows = [];
23
+ // Tasks 3.1-3.4: Process each configured platform
24
+ for (const platformId of configuredPlatforms) {
25
+ const humanName = platformNames[platformId] || platformId;
26
+ // Task 3.2: Get adapter for this platform
27
+ const adapter = getAdapter(platformId);
28
+ let detectedStatus;
29
+ if (!adapter) {
30
+ // Task 4.4: Handle unrecognized platform
31
+ detectedStatus = 'Unknown platform';
32
+ }
33
+ else {
34
+ // Task 3.3: Call adapter's detect() method with error handling
35
+ try {
36
+ // Task 3.4: Wrap in try-catch for graceful error handling
37
+ const isDetected = adapter.detect();
38
+ // Task 4.3: Format with symbols
39
+ detectedStatus = isDetected ? '✓' : '✗';
40
+ }
41
+ catch (error) {
42
+ // Task 4.4: Handle adapter errors
43
+ detectedStatus = 'Error';
44
+ }
45
+ }
46
+ tableRows.push({
47
+ platform: humanName,
48
+ configured: '✓',
49
+ detected: detectedStatus,
50
+ });
51
+ }
52
+ // Task 4.5: Print formatted table
53
+ const headers = ['Platform', 'Configured', 'Detected'];
54
+ const rows = [headers, ...tableRows.map(r => [r.platform, r.configured, r.detected])];
55
+ // Calculate column widths
56
+ const colWidths = headers.map((_, colIndex) => {
57
+ return Math.max(...rows.map(row => row[colIndex].length));
58
+ });
59
+ // Print table
60
+ console.log('\nConfigured Platforms:\n');
61
+ // Print header
62
+ const headerRow = headers.map((header, i) => padRight(header, colWidths[i])).join(' ');
63
+ console.log(headerRow);
64
+ console.log('─'.repeat(headerRow.length));
65
+ // Print data rows
66
+ for (let i = 1; i < rows.length; i++) {
67
+ const row = rows[i].map((cell, j) => padRight(cell, colWidths[j])).join(' ');
68
+ console.log(row);
69
+ }
70
+ console.log('');
71
+ }
@@ -0,0 +1,60 @@
1
+ import inquirer from 'inquirer';
2
+ import { readUserConfig } from '../config.js';
3
+ import { getAdapter } from '../adapters/index.js';
4
+ export async function removeCommand(name, options) {
5
+ const userConfig = readUserConfig();
6
+ if (!userConfig) {
7
+ console.error('No platforms configured. Run `gmcp init` first.');
8
+ process.exit(1);
9
+ }
10
+ let targetPlatforms = userConfig.platforms;
11
+ if (options.only) {
12
+ const onlyPlatforms = options.only.split(',').map(p => p.trim()).filter(p => p.length > 0);
13
+ const unconfigured = onlyPlatforms.filter(p => !userConfig.platforms.includes(p));
14
+ if (unconfigured.length > 0) {
15
+ console.warn(`Warning: ${unconfigured.join(', ')} not configured. Run \`gmcp init\` to add them.`);
16
+ }
17
+ targetPlatforms = onlyPlatforms.filter(p => userConfig.platforms.includes(p));
18
+ if (targetPlatforms.length === 0) {
19
+ console.error('No valid platforms specified.');
20
+ process.exit(1);
21
+ }
22
+ }
23
+ const installedOn = [];
24
+ for (const platformName of targetPlatforms) {
25
+ const adapter = getAdapter(platformName);
26
+ if (!adapter)
27
+ continue;
28
+ const servers = adapter.readServers();
29
+ if (servers.has(name)) {
30
+ installedOn.push(platformName);
31
+ }
32
+ }
33
+ if (installedOn.length === 0) {
34
+ console.log(`${name} is not installed on any platform.`);
35
+ return;
36
+ }
37
+ console.log(`${name} is installed on: ${installedOn.join(', ')}`);
38
+ const answer = await inquirer.prompt([
39
+ {
40
+ type: 'confirm',
41
+ name: 'confirm',
42
+ message: `Remove ${name} from ${installedOn.length} platform(s)?`,
43
+ default: false,
44
+ },
45
+ ]);
46
+ if (!answer.confirm) {
47
+ console.log('Cancelled.');
48
+ return;
49
+ }
50
+ let removedCount = 0;
51
+ for (const platformName of installedOn) {
52
+ const adapter = getAdapter(platformName);
53
+ if (!adapter)
54
+ continue;
55
+ adapter.removeServer(name);
56
+ console.log(`- ${platformName}: removed`);
57
+ removedCount++;
58
+ }
59
+ console.log(`\nRemoved ${name} from ${removedCount} platform(s).`);
60
+ }
@@ -0,0 +1,86 @@
1
+ import inquirer from 'inquirer';
2
+ import { searchServers } from '../api-client.js';
3
+ import { filterRemoteOnlyServers } from '../transform.js';
4
+ import { addCommand } from './add.js';
5
+ const EXIT_OPTION = '✓ Done (exit)';
6
+ const SHOW_MORE_OPTION = '→ Show next 10 servers';
7
+ const PAGE_SIZE = 10;
8
+ /**
9
+ * Search for MCP servers by name and optionally install
10
+ */
11
+ export async function searchCommand(query) {
12
+ try {
13
+ console.log(`Searching for "${query}"...`);
14
+ // Search using API
15
+ const results = await searchServers(query);
16
+ // Filter out remote-only servers
17
+ const localServers = filterRemoteOnlyServers(results);
18
+ if (localServers.length === 0) {
19
+ console.log('No servers found.');
20
+ return;
21
+ }
22
+ // Deduplicate by server name (API may return multiple entries for same server with different packages)
23
+ const uniqueServers = Array.from(new Map(localServers.map(server => [server.name, server])).values());
24
+ // Sort alphabetically
25
+ const sortedServers = [...uniqueServers].sort((a, b) => a.name.localeCompare(b.name));
26
+ console.log(`\nFound ${sortedServers.length} server(s) matching '${query}':\n`);
27
+ let currentPage = 0;
28
+ // Interactive loop with pagination
29
+ while (true) {
30
+ const startIndex = currentPage * PAGE_SIZE;
31
+ const endIndex = Math.min(startIndex + PAGE_SIZE, sortedServers.length);
32
+ const currentPageServers = sortedServers.slice(startIndex, endIndex);
33
+ const hasMore = endIndex < sortedServers.length;
34
+ if (currentPage > 0) {
35
+ console.log(`\nShowing ${startIndex + 1}-${endIndex} of ${sortedServers.length} servers:\n`);
36
+ }
37
+ // Format choices for current page
38
+ const choices = currentPageServers.map(server => ({
39
+ name: `${server.name} - ${server.description}`,
40
+ value: server.name,
41
+ }));
42
+ // Build options list
43
+ const allChoices = [
44
+ { name: EXIT_OPTION, value: EXIT_OPTION },
45
+ ...choices,
46
+ ];
47
+ // Add "Show more" option if there are more results
48
+ if (hasMore) {
49
+ allChoices.push({ name: SHOW_MORE_OPTION, value: SHOW_MORE_OPTION });
50
+ }
51
+ const answer = await inquirer.prompt([
52
+ {
53
+ type: 'rawlist',
54
+ name: 'server',
55
+ message: 'Select an MCP server to install (or exit):',
56
+ choices: allChoices,
57
+ pageSize: 15,
58
+ },
59
+ ]);
60
+ // Handle exit
61
+ if (answer.server === EXIT_OPTION) {
62
+ return;
63
+ }
64
+ // Handle show more
65
+ if (answer.server === SHOW_MORE_OPTION) {
66
+ currentPage++;
67
+ continue;
68
+ }
69
+ // Install selected server
70
+ try {
71
+ await addCommand(answer.server, {});
72
+ }
73
+ catch (error) {
74
+ console.error('Installation failed:', error instanceof Error ? error.message : String(error));
75
+ }
76
+ console.log(''); // Add spacing
77
+ }
78
+ }
79
+ catch (error) {
80
+ if (error instanceof Error) {
81
+ console.error(`Error: ${error.message}`);
82
+ process.exit(1);
83
+ }
84
+ throw error;
85
+ }
86
+ }
@@ -0,0 +1,84 @@
1
+ import inquirer from 'inquirer';
2
+ import { readUserConfig } from '../config.js';
3
+ import { getAdapter } from '../adapters/index.js';
4
+ export async function syncCommand() {
5
+ const userConfig = readUserConfig();
6
+ if (!userConfig) {
7
+ console.error('No platforms configured. Run `gmcp init` first.');
8
+ process.exit(1);
9
+ }
10
+ const allServers = new Map();
11
+ for (const platformName of userConfig.platforms) {
12
+ const adapter = getAdapter(platformName);
13
+ if (!adapter)
14
+ continue;
15
+ const servers = adapter.readServers();
16
+ for (const [name, entry] of servers.entries()) {
17
+ if (!allServers.has(name)) {
18
+ allServers.set(name, new Map());
19
+ }
20
+ allServers.get(name).set(platformName, entry);
21
+ }
22
+ }
23
+ const drift = [];
24
+ for (const [serverName, platforms] of allServers.entries()) {
25
+ const missingFrom = userConfig.platforms.filter(p => !platforms.has(p));
26
+ if (missingFrom.length > 0) {
27
+ const sourcePlatform = Array.from(platforms.keys())[0];
28
+ const sourceEntry = platforms.get(sourcePlatform);
29
+ drift.push({
30
+ serverName,
31
+ missingFrom,
32
+ sourceEntry,
33
+ sourcePlatform,
34
+ });
35
+ }
36
+ }
37
+ if (drift.length === 0) {
38
+ console.log('All platforms are in sync.');
39
+ return;
40
+ }
41
+ console.log(`\nFound ${drift.length} server(s) with drift:\n`);
42
+ for (const entry of drift) {
43
+ console.log(`${entry.serverName} — missing from: ${entry.missingFrom.join(', ')}`);
44
+ }
45
+ const choices = drift.map(d => ({
46
+ name: `${d.serverName} → ${d.missingFrom.join(', ')}`,
47
+ value: d.serverName,
48
+ checked: true,
49
+ }));
50
+ const answers = await inquirer.prompt([
51
+ {
52
+ type: 'checkbox',
53
+ name: 'servers',
54
+ message: 'Select servers to sync:',
55
+ choices,
56
+ },
57
+ ]);
58
+ if (answers.servers.length === 0) {
59
+ console.log('No servers selected.');
60
+ return;
61
+ }
62
+ let syncedCount = 0;
63
+ for (const serverName of answers.servers) {
64
+ const driftEntry = drift.find(d => d.serverName === serverName);
65
+ for (const platformName of driftEntry.missingFrom) {
66
+ const adapter = getAdapter(platformName);
67
+ if (!adapter)
68
+ continue;
69
+ const mockEntry = {
70
+ name: serverName,
71
+ description: 'Synced from another platform',
72
+ runtime: driftEntry.sourceEntry.command,
73
+ args: driftEntry.sourceEntry.args,
74
+ env: [],
75
+ tags: [],
76
+ };
77
+ const env = driftEntry.sourceEntry.env || {};
78
+ adapter.addServer(serverName, mockEntry, env);
79
+ console.log(`- ${serverName} → ${platformName}: synced`);
80
+ syncedCount++;
81
+ }
82
+ }
83
+ console.log(`\nSynced ${syncedCount} server(s) across platforms.`);
84
+ }
@@ -0,0 +1,34 @@
1
+ import { spawn, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ async function getInstalledVersion() {
5
+ try {
6
+ const { stdout } = await execAsync('npm list -g gmcp --depth=0 --json');
7
+ const data = JSON.parse(stdout);
8
+ return data.dependencies?.gmcp?.version || null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export async function updateCommand() {
15
+ console.log('Updating gmcp...\n');
16
+ const child = spawn('npm', ['update', '-g', 'gmcp'], {
17
+ stdio: 'inherit',
18
+ shell: true,
19
+ });
20
+ child.on('close', async (code) => {
21
+ if (code === 0) {
22
+ console.log('\ngmcp has been updated successfully.');
23
+ // Display the installed version
24
+ const version = await getInstalledVersion();
25
+ if (version) {
26
+ console.log(`Installed version: ${version}`);
27
+ }
28
+ }
29
+ else {
30
+ console.error('\nFailed to update gmcp.');
31
+ process.exit(code || 1);
32
+ }
33
+ });
34
+ }
package/dist/config.js ADDED
@@ -0,0 +1,34 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ const GMCP_DIR = path.join(os.homedir(), '.gmcp');
5
+ const CONFIG_FILE = path.join(GMCP_DIR, 'config.json');
6
+ export function ensureGmcpDir() {
7
+ if (!fs.existsSync(GMCP_DIR)) {
8
+ fs.mkdirSync(GMCP_DIR, { recursive: true });
9
+ }
10
+ }
11
+ export function readUserConfig() {
12
+ if (!fs.existsSync(CONFIG_FILE)) {
13
+ return null;
14
+ }
15
+ try {
16
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
17
+ return JSON.parse(content);
18
+ }
19
+ catch (error) {
20
+ throw new Error(`Failed to read config at ${CONFIG_FILE}: ${error}`);
21
+ }
22
+ }
23
+ export function writeUserConfig(config) {
24
+ ensureGmcpDir();
25
+ try {
26
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
27
+ }
28
+ catch (error) {
29
+ throw new Error(`Failed to write config at ${CONFIG_FILE}: ${error}`);
30
+ }
31
+ }
32
+ export function isInitialized() {
33
+ return fs.existsSync(CONFIG_FILE);
34
+ }
@@ -0,0 +1,35 @@
1
+ import inquirer from 'inquirer';
2
+ /**
3
+ * Prompt user to select between multiple package types (npm vs Docker)
4
+ * Returns the selected RegistryEntry
5
+ */
6
+ export async function promptForPackageType(entries) {
7
+ // If only one package type, return it without prompting
8
+ if (entries.length === 1) {
9
+ return entries[0];
10
+ }
11
+ // Build choices for inquirer
12
+ const choices = entries.map((entry, index) => {
13
+ const label = entry.runtime === 'npx' ? 'npm (via npx)' : 'Docker (via OCI)';
14
+ return {
15
+ name: `${index + 1}. ${label}`,
16
+ value: index,
17
+ };
18
+ });
19
+ console.log('\nThis server offers multiple runtimes:');
20
+ const answer = await inquirer.prompt([
21
+ {
22
+ type: 'rawlist',
23
+ name: 'selection',
24
+ message: 'Select runtime:',
25
+ choices,
26
+ validate: (input) => {
27
+ if (input >= 0 && input < entries.length) {
28
+ return true;
29
+ }
30
+ return 'Please select a valid option';
31
+ },
32
+ },
33
+ ]);
34
+ return entries[answer.selection];
35
+ }
@@ -0,0 +1,57 @@
1
+ import inquirer from 'inquirer';
2
+ /**
3
+ * Display results with pagination (20 per page)
4
+ * Shows "Show more? (y/n)" prompt when there are more results
5
+ */
6
+ export async function displayWithPagination(items, pageSize = 20, contextMessage) {
7
+ if (items.length === 0) {
8
+ console.log('No servers found.');
9
+ return;
10
+ }
11
+ let currentIndex = 0;
12
+ while (currentIndex < items.length) {
13
+ const endIndex = Math.min(currentIndex + pageSize, items.length);
14
+ const currentPage = items.slice(currentIndex, endIndex);
15
+ // Display header
16
+ if (contextMessage) {
17
+ console.log(`\nShowing ${currentIndex + 1}-${endIndex} of ${items.length} ${contextMessage}:`);
18
+ }
19
+ else {
20
+ console.log(`\nShowing ${currentIndex + 1}-${endIndex} of ${items.length} servers:`);
21
+ }
22
+ // Display items
23
+ console.log('');
24
+ currentPage.forEach((item, index) => {
25
+ const number = currentIndex + index + 1;
26
+ const description = truncateDescription(item.description);
27
+ console.log(`${number}. ${item.name} - ${description}`);
28
+ });
29
+ // Move to next page
30
+ currentIndex = endIndex;
31
+ // If more results exist, prompt to continue
32
+ if (currentIndex < items.length) {
33
+ console.log('');
34
+ const answer = await inquirer.prompt([
35
+ {
36
+ type: 'confirm',
37
+ name: 'showMore',
38
+ message: 'Show more?',
39
+ default: true,
40
+ },
41
+ ]);
42
+ if (!answer.showMore) {
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ console.log('');
48
+ }
49
+ /**
50
+ * Truncate description to 80 characters if needed
51
+ */
52
+ function truncateDescription(description, maxLength = 80) {
53
+ if (description.length <= maxLength) {
54
+ return description;
55
+ }
56
+ return description.substring(0, maxLength - 3) + '...';
57
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Transform API server data to internal RegistryEntry format
3
+ * Returns an array because one server can have multiple package types
4
+ * Filters out remote-only servers (no local packages)
5
+ */
6
+ export function transformToRegistryEntries(apiServer) {
7
+ const entries = [];
8
+ // Skip servers with no packages or empty packages array
9
+ if (!apiServer.packages || apiServer.packages.length === 0) {
10
+ return entries;
11
+ }
12
+ // Transform each package to a RegistryEntry
13
+ for (const pkg of apiServer.packages) {
14
+ // Only support npm and oci package types
15
+ if (pkg.registryType !== 'npm' && pkg.registryType !== 'oci') {
16
+ continue;
17
+ }
18
+ const entry = {
19
+ name: apiServer.name,
20
+ description: apiServer.description || '',
21
+ runtime: transformRuntime(pkg.registryType),
22
+ args: transformArgs(pkg.registryType, pkg.identifier),
23
+ env: transformEnvVars(pkg.environmentVariables || []),
24
+ tags: [],
25
+ };
26
+ entries.push(entry);
27
+ }
28
+ return entries;
29
+ }
30
+ /**
31
+ * Transform registryType to runtime command
32
+ */
33
+ function transformRuntime(registryType) {
34
+ switch (registryType) {
35
+ case 'npm':
36
+ return 'npx';
37
+ case 'oci':
38
+ return 'docker';
39
+ }
40
+ }
41
+ /**
42
+ * Transform package identifier to command arguments
43
+ */
44
+ function transformArgs(registryType, identifier) {
45
+ switch (registryType) {
46
+ case 'npm':
47
+ return ['-y', identifier];
48
+ case 'oci':
49
+ return ['run', '-i', '--rm', identifier];
50
+ }
51
+ }
52
+ /**
53
+ * Transform API environment variables to RegistryEntry format
54
+ */
55
+ function transformEnvVars(apiEnvVars) {
56
+ return apiEnvVars.map(env => ({
57
+ name: env.name,
58
+ description: env.description,
59
+ required: env.isRequired || false,
60
+ }));
61
+ }
62
+ /**
63
+ * Check if server has local packages (npm or oci)
64
+ */
65
+ export function hasLocalPackages(apiServer) {
66
+ if (!apiServer.packages || apiServer.packages.length === 0) {
67
+ return false;
68
+ }
69
+ // Check if any package is npm or oci
70
+ return apiServer.packages.some(pkg => pkg.registryType === 'npm' || pkg.registryType === 'oci');
71
+ }
72
+ /**
73
+ * Filter out servers that only offer remote transport (HTTP/SSE)
74
+ * Returns only servers with local packages (npm/oci)
75
+ */
76
+ export function filterRemoteOnlyServers(apiServers) {
77
+ return apiServers.filter(hasLocalPackages);
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/utils.js ADDED
@@ -0,0 +1,33 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ export function readJsonFile(filePath) {
5
+ if (!fs.existsSync(filePath)) {
6
+ return {};
7
+ }
8
+ try {
9
+ const content = fs.readFileSync(filePath, 'utf-8');
10
+ return JSON.parse(content);
11
+ }
12
+ catch (error) {
13
+ throw new Error(`Failed to parse JSON from ${filePath}: ${error}`);
14
+ }
15
+ }
16
+ export function writeJsonFile(filePath, data) {
17
+ const dir = path.dirname(filePath);
18
+ if (!fs.existsSync(dir)) {
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ }
21
+ try {
22
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Failed to write JSON to ${filePath}: ${error}`);
26
+ }
27
+ }
28
+ export function expandHomeDir(filePath) {
29
+ if (filePath.startsWith('~/')) {
30
+ return path.join(os.homedir(), filePath.slice(2));
31
+ }
32
+ return filePath;
33
+ }