@vibecodr/cli 0.1.8

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 (49) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +201 -0
  3. package/README.md +66 -0
  4. package/dist/auth/official-client.js +10 -0
  5. package/dist/auth/token-manager.js +424 -0
  6. package/dist/bin/vibecodr-mcp.js +101 -0
  7. package/dist/cli/errors.js +38 -0
  8. package/dist/cli/output.js +52 -0
  9. package/dist/cli/parse.js +84 -0
  10. package/dist/clients/base.js +30 -0
  11. package/dist/clients/codex.js +136 -0
  12. package/dist/clients/cursor.js +91 -0
  13. package/dist/clients/vscode.js +138 -0
  14. package/dist/clients/windsurf.js +81 -0
  15. package/dist/commands/call.js +123 -0
  16. package/dist/commands/config.js +124 -0
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/doctor.js +17 -0
  19. package/dist/commands/install.js +63 -0
  20. package/dist/commands/login.js +41 -0
  21. package/dist/commands/logout.js +26 -0
  22. package/dist/commands/pulse-setup.js +82 -0
  23. package/dist/commands/status.js +64 -0
  24. package/dist/commands/tools.js +82 -0
  25. package/dist/commands/uninstall.js +55 -0
  26. package/dist/core/interactive-input.js +114 -0
  27. package/dist/core/mcp-client.js +82 -0
  28. package/dist/core/renderers.js +34 -0
  29. package/dist/doctor/run.js +132 -0
  30. package/dist/platform/browser.js +79 -0
  31. package/dist/platform/exec.js +23 -0
  32. package/dist/platform/paths.js +36 -0
  33. package/dist/platform/prompt.js +19 -0
  34. package/dist/storage/config-store.js +72 -0
  35. package/dist/storage/file-lock.js +41 -0
  36. package/dist/storage/install-manifest.js +80 -0
  37. package/dist/storage/secret-store.js +301 -0
  38. package/dist/types/auth.js +1 -0
  39. package/dist/types/config.js +21 -0
  40. package/dist/types/install.js +1 -0
  41. package/docs/architecture.md +35 -0
  42. package/docs/auth.md +66 -0
  43. package/docs/clients.md +42 -0
  44. package/docs/commands.md +134 -0
  45. package/docs/contributors.md +68 -0
  46. package/docs/install.md +90 -0
  47. package/docs/licensing.md +20 -0
  48. package/docs/troubleshooting.md +97 -0
  49. package/package.json +40 -0
@@ -0,0 +1,114 @@
1
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
2
+ function schemaType(schema) {
3
+ const type = schema?.["type"];
4
+ return typeof type === "string" ? type : undefined;
5
+ }
6
+ function describe(path, schema, required) {
7
+ const type = schemaType(schema);
8
+ const enumRaw = schema?.["enum"];
9
+ const enumValues = Array.isArray(enumRaw) ? ` [${enumRaw.map(String).join(", ")}]` : "";
10
+ return `${path}${type ? ` (${type})` : ""}${required ? " [required]" : " [optional]"}${enumValues}`;
11
+ }
12
+ async function promptScalar(prompt, path, schema, required) {
13
+ const type = schemaType(schema);
14
+ const raw = await prompt(`${describe(path, schema, required)}: `, { allowEmpty: !required });
15
+ if (!raw)
16
+ return undefined;
17
+ const enumRaw = schema?.["enum"];
18
+ if (Array.isArray(enumRaw) && !enumRaw.map(String).includes(raw)) {
19
+ throw new CliError("input.invalid_enum", `${path} must be one of: ${enumRaw.map(String).join(", ")}`, EXIT_CODES.usage);
20
+ }
21
+ if (type === "number" || type === "integer") {
22
+ const parsed = Number(raw);
23
+ if (!Number.isFinite(parsed)) {
24
+ throw new CliError("input.invalid_number", `${path} must be a number.`, EXIT_CODES.usage);
25
+ }
26
+ return type === "integer" ? Math.trunc(parsed) : parsed;
27
+ }
28
+ if (type === "boolean") {
29
+ const normalized = raw.toLowerCase();
30
+ if (["true", "t", "yes", "y", "1"].includes(normalized))
31
+ return true;
32
+ if (["false", "f", "no", "n", "0"].includes(normalized))
33
+ return false;
34
+ throw new CliError("input.invalid_boolean", `${path} must be true or false.`, EXIT_CODES.usage);
35
+ }
36
+ return raw;
37
+ }
38
+ async function promptArray(prompt, path, schema, required) {
39
+ const itemsRaw = schema?.["items"];
40
+ const items = itemsRaw && typeof itemsRaw === "object" && !Array.isArray(itemsRaw)
41
+ ? itemsRaw
42
+ : undefined;
43
+ const countRaw = await prompt(`${describe(path, schema, required)} count: `, { allowEmpty: !required });
44
+ if (!countRaw)
45
+ return undefined;
46
+ const count = Number(countRaw);
47
+ if (!Number.isInteger(count) || count < 0) {
48
+ throw new CliError("input.invalid_array_count", `${path} count must be a non-negative integer.`, EXIT_CODES.usage);
49
+ }
50
+ const values = [];
51
+ for (let index = 0; index < count; index += 1) {
52
+ values.push(await promptBySchema(prompt, `${path}[${index}]`, items, true));
53
+ }
54
+ return values;
55
+ }
56
+ async function promptObject(prompt, path, schema, required) {
57
+ const propertiesRaw = schema?.["properties"];
58
+ const properties = typeof propertiesRaw === "object" && propertiesRaw
59
+ ? propertiesRaw
60
+ : {};
61
+ const requiredRaw = schema?.["required"];
62
+ const requiredFields = Array.isArray(requiredRaw)
63
+ ? requiredRaw.filter((value) => typeof value === "string")
64
+ : [];
65
+ if (!required && !requiredFields.length && Object.keys(properties).length) {
66
+ const include = await prompt(`Include ${path} object? [y/N]: `, { allowEmpty: true });
67
+ if (!include || ["n", "no"].includes(include.toLowerCase()))
68
+ return undefined;
69
+ }
70
+ const result = {};
71
+ for (const [name, childSchema] of Object.entries(properties)) {
72
+ const childRequired = requiredFields.includes(name);
73
+ const value = await promptBySchema(prompt, `${path}.${name}`, childSchema, childRequired);
74
+ if (value !== undefined) {
75
+ result[name] = value;
76
+ }
77
+ }
78
+ if (!required && Object.keys(result).length === 0)
79
+ return undefined;
80
+ return result;
81
+ }
82
+ export async function promptBySchema(prompt, path, schema, required) {
83
+ const type = schemaType(schema);
84
+ if (type === "object" || (!type && schema?.["properties"])) {
85
+ return await promptObject(prompt, path, schema, required);
86
+ }
87
+ if (type === "array") {
88
+ return await promptArray(prompt, path, schema, required);
89
+ }
90
+ return await promptScalar(prompt, path, schema, required);
91
+ }
92
+ export async function promptObjectBySchema(prompt, toolName, schema) {
93
+ const propertiesRaw = schema?.["properties"];
94
+ const properties = typeof propertiesRaw === "object" && propertiesRaw
95
+ ? propertiesRaw
96
+ : {};
97
+ const requiredRaw = schema?.["required"];
98
+ const requiredFields = Array.isArray(requiredRaw)
99
+ ? requiredRaw.filter((value) => typeof value === "string")
100
+ : [];
101
+ if (requiredFields.length) {
102
+ // Stronger guidance before prompting starts.
103
+ // Users should know exactly which values cannot be skipped.
104
+ process.stderr.write(`Required fields for ${toolName}: ${requiredFields.join(", ")}\n`);
105
+ }
106
+ const result = {};
107
+ for (const [name, childSchema] of Object.entries(properties)) {
108
+ const value = await promptBySchema(prompt, name, childSchema, requiredFields.includes(name));
109
+ if (value !== undefined) {
110
+ result[name] = value;
111
+ }
112
+ }
113
+ return result;
114
+ }
@@ -0,0 +1,82 @@
1
+ import { createRequire } from "node:module";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { extractWWWAuthenticateParams } from "@modelcontextprotocol/sdk/client/auth.js";
4
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
6
+ const require = createRequire(import.meta.url);
7
+ const packageJson = require("../../package.json");
8
+ const packageVersion = typeof packageJson.version === "string" && packageJson.version.length > 0
9
+ ? packageJson.version
10
+ : "0.0.0";
11
+ export const CLIENT_INFO = {
12
+ name: "vibecodr-mcp",
13
+ version: packageVersion
14
+ };
15
+ export class McpRuntimeClient {
16
+ async listTools(serverUrl, accessToken) {
17
+ return await this.withClient(serverUrl, accessToken, async (client) => {
18
+ const result = await client.listTools();
19
+ return result.tools;
20
+ });
21
+ }
22
+ async callTool(serverUrl, accessToken, name, args) {
23
+ return await this.withClient(serverUrl, accessToken, async (client) => {
24
+ return await client.callTool({
25
+ name,
26
+ arguments: args
27
+ });
28
+ });
29
+ }
30
+ async withClient(serverUrl, accessToken, fn) {
31
+ let authChallenge;
32
+ const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
33
+ ...(accessToken ? { requestInit: {
34
+ headers: {
35
+ authorization: `Bearer ${accessToken}`
36
+ }
37
+ } } : {}),
38
+ fetch: async (input, init) => {
39
+ const response = await fetch(input, init);
40
+ if (response.status === 401 || response.status === 403) {
41
+ const challenge = extractWWWAuthenticateParams(response);
42
+ authChallenge = {
43
+ status: response.status,
44
+ scope: challenge.scope,
45
+ error: challenge.error,
46
+ resourceMetadataUrl: challenge.resourceMetadataUrl?.toString()
47
+ };
48
+ }
49
+ return response;
50
+ }
51
+ });
52
+ const client = new Client(CLIENT_INFO, {
53
+ capabilities: {}
54
+ });
55
+ try {
56
+ await client.connect(transport);
57
+ return await fn(client);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
61
+ const requiredScope = authChallenge?.scope;
62
+ const isScopeStepUp = authChallenge?.error === "insufficient_scope" || error.code === 403;
63
+ throw new CliError(isScopeStepUp ? "auth.insufficient_scope" : "auth.required", isScopeStepUp
64
+ ? "The MCP server requires a broader OAuth scope for this operation."
65
+ : "The MCP server requires authentication for this operation.", EXIT_CODES.authRequired, {
66
+ cause: error,
67
+ debugDetails: authChallenge,
68
+ nextStep: requiredScope
69
+ ? `Run vibecodr login --scope "${requiredScope}", or retry interactively to complete CLI MCP OAuth. CLI auth is separate from Codex, editor, ChatGPT, and other MCP client auth.`
70
+ : "Run vibecodr login, or retry interactively to complete CLI MCP OAuth. CLI auth is separate from Codex, editor, ChatGPT, and other MCP client auth."
71
+ });
72
+ }
73
+ throw new CliError("mcp.protocol", "Failed to complete the MCP request.", EXIT_CODES.protocol, {
74
+ cause: error,
75
+ nextStep: "Run vibecodr doctor to inspect auth, discovery, and connectivity."
76
+ });
77
+ }
78
+ finally {
79
+ await transport.close().catch(() => undefined);
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,34 @@
1
+ export function formatJson(value) {
2
+ return JSON.stringify(value, null, 2);
3
+ }
4
+ export function summarizeToolSchema(inputSchema) {
5
+ const requiredRaw = inputSchema?.["required"];
6
+ const required = Array.isArray(requiredRaw)
7
+ ? requiredRaw.filter((value) => typeof value === "string")
8
+ : [];
9
+ const propertiesRaw = inputSchema?.["properties"];
10
+ const properties = typeof propertiesRaw === "object" && propertiesRaw
11
+ ? propertiesRaw
12
+ : {};
13
+ const skeleton = Object.fromEntries(Object.entries(properties).map(([name, schema]) => [name, schema["type"] === "number" || schema["type"] === "integer" ? 0 : schema["type"] === "boolean" ? false : ""]));
14
+ const optional = Object.keys(properties).filter((name) => !required.includes(name));
15
+ return { required, optional, skeleton };
16
+ }
17
+ export function renderToolResult(result) {
18
+ const typed = typeof result === "object" && result ? result : {};
19
+ const lines = [];
20
+ if (Array.isArray(typed.content)) {
21
+ for (const item of typed.content) {
22
+ if (item && typeof item === "object" && "type" in item && item.type === "text") {
23
+ const text = item.text;
24
+ if (typeof text === "string" && text.trim())
25
+ lines.push(text);
26
+ }
27
+ }
28
+ }
29
+ if (lines.length)
30
+ return lines.join("\n\n");
31
+ if (typed.structuredContent !== undefined)
32
+ return formatJson(typed.structuredContent);
33
+ return formatJson(result);
34
+ }
@@ -0,0 +1,132 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { SecretStore } from "../storage/secret-store.js";
5
+ import { TokenManager } from "../auth/token-manager.js";
6
+ import { codexConfigPath, cursorUserConfigPath, vscodeWorkspaceConfigPath, windsurfLegacyConfigPath, windsurfUserConfigPath } from "../platform/paths.js";
7
+ import { browserLauncherAvailable } from "../platform/browser.js";
8
+ import { commandExists } from "../platform/exec.js";
9
+ function detectBrowserLauncher() {
10
+ return browserLauncherAvailable();
11
+ }
12
+ async function pathExists(path) {
13
+ try {
14
+ await access(path);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function writableAncestor(path) {
22
+ let current = resolve(path);
23
+ while (true) {
24
+ try {
25
+ await access(current, fsConstants.W_OK);
26
+ return current;
27
+ }
28
+ catch {
29
+ const parent = dirname(current);
30
+ if (parent === current)
31
+ return undefined;
32
+ current = parent;
33
+ }
34
+ }
35
+ }
36
+ async function configLocationCheck(path, label) {
37
+ const writableBase = await writableAncestor(path);
38
+ if (!writableBase) {
39
+ return {
40
+ id: label,
41
+ status: "warn",
42
+ summary: `${path} is not currently writable from this environment.`
43
+ };
44
+ }
45
+ return {
46
+ id: label,
47
+ status: "pass",
48
+ summary: `${path} is reachable via writable base ${writableBase}.`
49
+ };
50
+ }
51
+ export async function runDoctor(globalOptions, tokenManager, secretStore, targetClient) {
52
+ const checks = [];
53
+ const majorVersion = Number(process.versions.node.split(".")[0] || "0");
54
+ checks.push({
55
+ id: "node-version",
56
+ status: majorVersion >= 22 && majorVersion < 26 ? "pass" : "fail",
57
+ summary: `Node ${process.versions.node} detected.`
58
+ });
59
+ checks.push({
60
+ id: "browser-launcher",
61
+ status: detectBrowserLauncher() ? "pass" : "warn",
62
+ summary: detectBrowserLauncher() ? "A browser launcher is available." : "No browser launcher was detected."
63
+ });
64
+ const secretStoreCheck = await secretStore.checkAvailability();
65
+ checks.push({
66
+ id: "secret-store",
67
+ status: secretStoreCheck.ok ? "pass" : "fail",
68
+ summary: secretStoreCheck.summary
69
+ });
70
+ try {
71
+ const { serverUrl } = await tokenManager.resolveProfile(globalOptions);
72
+ const { profileName } = await tokenManager.resolveProfile(globalOptions);
73
+ const session = await tokenManager.getSession(profileName);
74
+ const discovery = await tokenManager.discover(serverUrl);
75
+ checks.push({
76
+ id: "server-reachability",
77
+ status: "pass",
78
+ summary: `Discovered authorization server ${discovery.authorizationServerUrl}.`
79
+ });
80
+ checks.push({
81
+ id: "pkce-supported",
82
+ status: Array.isArray(discovery.authorizationServerMetadata?.code_challenge_methods_supported)
83
+ && discovery.authorizationServerMetadata.code_challenge_methods_supported.includes("S256")
84
+ ? "pass"
85
+ : "fail",
86
+ summary: Array.isArray(discovery.authorizationServerMetadata?.code_challenge_methods_supported)
87
+ && discovery.authorizationServerMetadata.code_challenge_methods_supported.includes("S256")
88
+ ? "Authorization server advertises PKCE S256."
89
+ : "Authorization server metadata does not advertise PKCE S256."
90
+ });
91
+ checks.push({
92
+ id: "refresh-token",
93
+ status: session?.refreshToken ? "pass" : "warn",
94
+ summary: session?.refreshToken ? "A refresh token is available for the current profile." : "No refresh token is stored for the current profile."
95
+ });
96
+ }
97
+ catch (error) {
98
+ checks.push({
99
+ id: "server-reachability",
100
+ status: "fail",
101
+ summary: error instanceof Error ? error.message : String(error)
102
+ });
103
+ }
104
+ if (targetClient === "codex") {
105
+ checks.push({
106
+ id: "codex-cli",
107
+ status: commandExists("codex") ? "pass" : "warn",
108
+ summary: commandExists("codex") ? "Codex CLI is available." : "Codex CLI is not on PATH."
109
+ });
110
+ checks.push(await configLocationCheck(codexConfigPath(), "codex-config"));
111
+ }
112
+ if (targetClient === "cursor") {
113
+ checks.push(await configLocationCheck(cursorUserConfigPath(), "cursor-config"));
114
+ }
115
+ if (targetClient === "vscode") {
116
+ checks.push({
117
+ id: "vscode-cli",
118
+ status: commandExists("code") ? "pass" : "warn",
119
+ summary: commandExists("code") ? "VS Code CLI is available." : "VS Code CLI is not on PATH."
120
+ });
121
+ checks.push(await configLocationCheck(vscodeWorkspaceConfigPath(process.cwd()), "vscode-workspace-config"));
122
+ }
123
+ if (targetClient === "windsurf") {
124
+ checks.push(await configLocationCheck(windsurfUserConfigPath(), "windsurf-config"));
125
+ checks.push({
126
+ id: "windsurf-legacy-config",
127
+ status: await pathExists(windsurfLegacyConfigPath()) ? "warn" : "pass",
128
+ summary: `Legacy Windsurf plugin path: ${windsurfLegacyConfigPath()}`
129
+ });
130
+ }
131
+ return checks;
132
+ }
@@ -0,0 +1,79 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { accessSync, constants as fsConstants } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ const ALLOWED_BROWSER_URL_PROTOCOLS = new Set(["http:", "https:", "cursor:", "vscode:"]);
6
+ function windowsSystemCommand(name) {
7
+ const systemRoot = process.env["SystemRoot"]?.trim() || "C:\\Windows";
8
+ return join(systemRoot, "System32", name);
9
+ }
10
+ function commandBinaryAvailable(command) {
11
+ if (command.includes("\\") || command.includes("/")) {
12
+ try {
13
+ accessSync(command, fsConstants.F_OK);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ const checker = process.platform === "win32" ? windowsSystemCommand("where.exe") : "which";
21
+ const result = spawnSync(checker, [command], { stdio: "ignore" });
22
+ return result.status === 0;
23
+ }
24
+ export function browserOpenCommandForCurrentPlatform() {
25
+ switch (process.platform) {
26
+ case "win32":
27
+ return { command: windowsSystemCommand("rundll32.exe"), args: ["url.dll,FileProtocolHandler"] };
28
+ case "darwin":
29
+ return { command: "open", args: [] };
30
+ default:
31
+ return { command: "xdg-open", args: [] };
32
+ }
33
+ }
34
+ export function browserLauncherAvailable() {
35
+ const launcher = browserOpenCommandForCurrentPlatform();
36
+ if (!launcher)
37
+ return false;
38
+ if (process.platform === "win32") {
39
+ return commandBinaryAvailable(launcher.command);
40
+ }
41
+ const check = spawnSync(launcher.command, ["--help"], { stdio: "ignore" });
42
+ return check.status === 0 || check.status === 1;
43
+ }
44
+ export async function openExternalUrl(url) {
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(url);
48
+ }
49
+ catch (error) {
50
+ throw new CliError("auth.browser_url_invalid", "Refusing to open an invalid browser URL.", EXIT_CODES.protocol, {
51
+ cause: error,
52
+ nextStep: "Retry with a valid OAuth authorization URL."
53
+ });
54
+ }
55
+ if (!ALLOWED_BROWSER_URL_PROTOCOLS.has(parsed.protocol)) {
56
+ throw new CliError("auth.browser_url_blocked", `Refusing to open unsupported URL scheme ${parsed.protocol}.`, EXIT_CODES.protocol, {
57
+ nextStep: "Only http, https, Cursor, and VS Code install links can be opened automatically."
58
+ });
59
+ }
60
+ const launcher = browserOpenCommandForCurrentPlatform();
61
+ if (!launcher) {
62
+ throw new CliError("auth.browser_unavailable", "No browser launcher is available on this platform.", EXIT_CODES.authRequired);
63
+ }
64
+ await new Promise((resolve, reject) => {
65
+ const args = [...launcher.args, url];
66
+ const child = spawn(launcher.command, args, {
67
+ detached: process.platform !== "win32",
68
+ stdio: "ignore"
69
+ });
70
+ child.once("error", reject);
71
+ child.once("spawn", () => resolve());
72
+ child.unref();
73
+ }).catch((error) => {
74
+ throw new CliError("auth.browser_open_failed", `Failed to open a browser for ${url}.`, EXIT_CODES.authRequired, {
75
+ nextStep: "Retry without --browser open so the CLI prints the URL for manual browser auth.",
76
+ cause: error
77
+ });
78
+ });
79
+ }
@@ -0,0 +1,23 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { join } from "node:path";
3
+ function windowsSystemCommand(name) {
4
+ const systemRoot = process.env["SystemRoot"]?.trim() || "C:\\Windows";
5
+ return join(systemRoot, "System32", name);
6
+ }
7
+ export function commandExists(command) {
8
+ const checker = process.platform === "win32" ? windowsSystemCommand("where.exe") : "which";
9
+ const result = spawnSync(checker, [command], { stdio: "ignore" });
10
+ return result.status === 0;
11
+ }
12
+ export function runCommand(command, args) {
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn(command, args, { stdio: "ignore" });
15
+ child.once("error", reject);
16
+ child.once("exit", (code) => {
17
+ if (code === 0)
18
+ resolve();
19
+ else
20
+ reject(new Error(`${command} exited with code ${code}`));
21
+ });
22
+ });
23
+ }
@@ -0,0 +1,36 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ function windowsAppDataPath() {
4
+ return process.env["APPDATA"] || join(homedir(), "AppData", "Roaming");
5
+ }
6
+ function vibecodrConfigRoot() {
7
+ switch (process.platform) {
8
+ case "win32":
9
+ return join(windowsAppDataPath(), "Vibecodr", "MCP");
10
+ case "darwin":
11
+ return join(homedir(), "Library", "Application Support", "Vibecodr MCP");
12
+ default:
13
+ return join(process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config"), "vibecodr-mcp");
14
+ }
15
+ }
16
+ export function codexConfigPath() {
17
+ return join(homedir(), ".codex", "config.toml");
18
+ }
19
+ export function cursorUserConfigPath() {
20
+ return join(homedir(), ".cursor", "mcp.json");
21
+ }
22
+ export function vscodeWorkspaceConfigPath(rootPath) {
23
+ return join(rootPath, ".vscode", "mcp.json");
24
+ }
25
+ export function windsurfUserConfigPath() {
26
+ return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
27
+ }
28
+ export function windsurfLegacyConfigPath() {
29
+ return join(homedir(), ".codeium", "mcp_config.json");
30
+ }
31
+ export function projectCursorConfigPath(rootPath) {
32
+ return join(rootPath, ".cursor", "mcp.json");
33
+ }
34
+ export function secretStoreDirectory() {
35
+ return join(vibecodrConfigRoot(), "secrets");
36
+ }
@@ -0,0 +1,19 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
4
+ export async function promptText(message, options) {
5
+ const rl = createInterface({ input, output });
6
+ try {
7
+ const value = (await rl.question(message)).trim();
8
+ if (!value && !options?.allowEmpty) {
9
+ throw new CliError("input.required", "A value is required.", EXIT_CODES.usage);
10
+ }
11
+ return value;
12
+ }
13
+ finally {
14
+ rl.close();
15
+ }
16
+ }
17
+ export function isInteractiveTerminal() {
18
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
19
+ }
@@ -0,0 +1,72 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { defaultConfigFile, defaultProfileConfig } from "../types/config.js";
6
+ import { writeFileWithBackup } from "./file-lock.js";
7
+ function windowsAppDataPath() {
8
+ return process.env["APPDATA"] || join(homedir(), "AppData", "Roaming");
9
+ }
10
+ export function defaultConfigPath() {
11
+ if (process.env["VIBECDR_MCP_CONFIG_PATH"])
12
+ return process.env["VIBECDR_MCP_CONFIG_PATH"];
13
+ switch (process.platform) {
14
+ case "win32":
15
+ return join(windowsAppDataPath(), "Vibecodr", "MCP", "config.json");
16
+ case "darwin":
17
+ return join(homedir(), "Library", "Application Support", "Vibecodr MCP", "config.json");
18
+ default:
19
+ return join(process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config"), "vibecodr-mcp", "config.json");
20
+ }
21
+ }
22
+ export class ConfigStore {
23
+ filePath;
24
+ constructor(filePath = defaultConfigPath()) {
25
+ this.filePath = filePath;
26
+ }
27
+ path() {
28
+ return this.filePath;
29
+ }
30
+ async load() {
31
+ try {
32
+ const raw = await readFile(this.filePath, "utf8");
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(raw);
36
+ }
37
+ catch (error) {
38
+ throw new CliError("config.parse_failed", `Config file at ${this.filePath} is not valid JSON.`, EXIT_CODES.config, {
39
+ cause: error,
40
+ nextStep: "Repair or remove the invalid config file, then retry."
41
+ });
42
+ }
43
+ if (parsed.version !== 1 || typeof parsed.currentProfile !== "string" || !parsed.profiles) {
44
+ throw new CliError("config.invalid_shape", `Config file at ${this.filePath} has an unsupported shape.`, EXIT_CODES.config, {
45
+ nextStep: "Repair or remove the invalid config file, then retry."
46
+ });
47
+ }
48
+ return {
49
+ version: 1,
50
+ currentProfile: parsed.currentProfile,
51
+ profiles: Object.fromEntries(Object.entries(parsed.profiles).map(([name, value]) => [name, { ...defaultProfileConfig(), ...value }]))
52
+ };
53
+ }
54
+ catch (error) {
55
+ if (error.code === "ENOENT")
56
+ return defaultConfigFile();
57
+ throw error;
58
+ }
59
+ }
60
+ async save(config) {
61
+ await writeFileWithBackup(this.filePath, JSON.stringify(config, null, 2) + "\n");
62
+ }
63
+ async getProfile(profileName) {
64
+ const config = await this.load();
65
+ const name = profileName || config.currentProfile;
66
+ return {
67
+ name,
68
+ profile: config.profiles[name] || defaultProfileConfig(),
69
+ config
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,41 @@
1
+ import { copyFile, mkdir, open, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
4
+ export class FileLock {
5
+ lockPath;
6
+ constructor(lockPath) {
7
+ this.lockPath = lockPath;
8
+ }
9
+ async withLock(fn) {
10
+ await mkdir(dirname(this.lockPath), { recursive: true });
11
+ let handle;
12
+ try {
13
+ handle = await open(this.lockPath, "wx");
14
+ }
15
+ catch (error) {
16
+ throw new CliError("install.locked", `Another process is already modifying ${this.lockPath}.`, EXIT_CODES.installConflict, {
17
+ cause: error
18
+ });
19
+ }
20
+ try {
21
+ return await fn();
22
+ }
23
+ finally {
24
+ await handle?.close().catch(() => undefined);
25
+ await unlink(this.lockPath).catch(() => undefined);
26
+ }
27
+ }
28
+ }
29
+ export async function writeFileWithBackup(path, content) {
30
+ const lock = new FileLock(`${path}.lock`);
31
+ await lock.withLock(async () => {
32
+ await mkdir(dirname(path), { recursive: true });
33
+ try {
34
+ await copyFile(path, `${path}.bak`);
35
+ }
36
+ catch { }
37
+ const tempPath = `${path}.tmp`;
38
+ await writeFile(tempPath, content, "utf8");
39
+ await rename(tempPath, path);
40
+ });
41
+ }