@vucinatim/agentic-devtools 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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/SECURITY.md +47 -0
  4. package/adapters/claude/namecheap/README.md +13 -0
  5. package/adapters/claude/npm/README.md +11 -0
  6. package/adapters/claude/railway/README.md +11 -0
  7. package/adapters/codex/namecheap/.codex-plugin/plugin.json +40 -0
  8. package/adapters/codex/namecheap/.mcp.json +21 -0
  9. package/adapters/codex/namecheap/SKILL.md +40 -0
  10. package/adapters/codex/npm/.codex-plugin/plugin.json +41 -0
  11. package/adapters/codex/npm/.mcp.json +18 -0
  12. package/adapters/codex/npm/SKILL.md +54 -0
  13. package/adapters/codex/railway/.codex-plugin/plugin.json +39 -0
  14. package/adapters/codex/railway/.mcp.json +20 -0
  15. package/adapters/codex/railway/SKILL.md +44 -0
  16. package/docs/README.md +14 -0
  17. package/docs/architecture.md +208 -0
  18. package/docs/auth-and-setup-guidelines.md +261 -0
  19. package/docs/migration-plan.md +55 -0
  20. package/docs/open-source-readiness.md +119 -0
  21. package/docs/publishing.md +211 -0
  22. package/docs/testing.md +61 -0
  23. package/docs/usage.md +144 -0
  24. package/package.json +78 -0
  25. package/src/cli.mjs +158 -0
  26. package/src/core/config-store.mjs +106 -0
  27. package/src/core/result.mjs +13 -0
  28. package/src/core/tool-registry.mjs +29 -0
  29. package/src/index.mjs +47 -0
  30. package/src/tools/namecheap/auth.mjs +429 -0
  31. package/src/tools/namecheap/client.mjs +655 -0
  32. package/src/tools/namecheap/mcp.mjs +298 -0
  33. package/src/tools/npm/auth.mjs +367 -0
  34. package/src/tools/npm/client.mjs +317 -0
  35. package/src/tools/npm/mcp.mjs +343 -0
  36. package/src/tools/railway/auth.mjs +402 -0
  37. package/src/tools/railway/client.mjs +388 -0
  38. package/src/tools/railway/mcp.mjs +282 -0
@@ -0,0 +1,106 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ export const CONFIG_ROOT = path.join(os.homedir(), ".config", "agentic-devtools");
8
+
9
+ export const isTruthyEnv = (value) =>
10
+ typeof value === "string" &&
11
+ ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
12
+
13
+ export const pickString = (value, fallback = undefined) => {
14
+ if (typeof value === "string" && value.trim().length > 0) {
15
+ return value.trim();
16
+ }
17
+ return fallback;
18
+ };
19
+
20
+ export const resolveConfigPath = ({ env, envVar, fileName }) => {
21
+ const override = pickString(env?.[envVar]);
22
+ return override ? path.resolve(override) : path.join(CONFIG_ROOT, fileName);
23
+ };
24
+
25
+ export const readJsonConfig = async (filePath) => {
26
+ try {
27
+ return JSON.parse(await readFile(filePath, "utf8"));
28
+ } catch (error) {
29
+ if (error && typeof error === "object" && error.code === "ENOENT") {
30
+ return null;
31
+ }
32
+ throw error;
33
+ }
34
+ };
35
+
36
+ export const readJsonConfigSync = (filePath) => {
37
+ if (!existsSync(filePath)) {
38
+ return null;
39
+ }
40
+
41
+ return JSON.parse(readFileSync(filePath, "utf8"));
42
+ };
43
+
44
+ export const writeJsonConfig = async (filePath, config) => {
45
+ await mkdir(path.dirname(filePath), { recursive: true });
46
+ await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
47
+ await chmod(filePath, 0o600).catch(() => {});
48
+ };
49
+
50
+ export const removeJsonConfig = async (filePath) => {
51
+ await rm(filePath, { force: true });
52
+ };
53
+
54
+ export const escapeHtml = (value) =>
55
+ String(value)
56
+ .replaceAll("&", "&")
57
+ .replaceAll("<", "&lt;")
58
+ .replaceAll(">", "&gt;")
59
+ .replaceAll('"', "&quot;")
60
+ .replaceAll("'", "&#39;");
61
+
62
+ export const parseFormBody = async (request) => {
63
+ const chunks = [];
64
+
65
+ for await (const chunk of request) {
66
+ chunks.push(chunk);
67
+ }
68
+
69
+ const body = Buffer.concat(chunks).toString("utf8");
70
+ const params = new URLSearchParams(body);
71
+
72
+ return Object.fromEntries(params.entries());
73
+ };
74
+
75
+ /* v8 ignore start */
76
+ export const openBrowser = async (url, { skipEnvVar } = {}) => {
77
+ if (skipEnvVar && isTruthyEnv(process.env[skipEnvVar])) {
78
+ return;
79
+ }
80
+
81
+ await new Promise((resolve, reject) => {
82
+ const platform = process.platform;
83
+ let command;
84
+ let args;
85
+
86
+ if (platform === "darwin") {
87
+ command = "open";
88
+ args = [url];
89
+ } else if (platform === "win32") {
90
+ command = "cmd";
91
+ args = ["/c", "start", "", url];
92
+ } else {
93
+ command = "xdg-open";
94
+ args = [url];
95
+ }
96
+
97
+ execFile(command, args, (error) => {
98
+ if (error) {
99
+ reject(error);
100
+ return;
101
+ }
102
+ resolve();
103
+ });
104
+ });
105
+ };
106
+ /* v8 ignore stop */
@@ -0,0 +1,13 @@
1
+ export const createJsonResult = (value) => ({
2
+ content: [
3
+ {
4
+ type: "text",
5
+ text: JSON.stringify(value, null, 2),
6
+ },
7
+ ],
8
+ structuredContent: value,
9
+ });
10
+
11
+ export const printJson = (value) => {
12
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
13
+ };
@@ -0,0 +1,29 @@
1
+ export const tools = {
2
+ namecheap: {
3
+ name: "namecheap",
4
+ description: "Inspect and manage Namecheap domains and DNS.",
5
+ mcpModule: "../tools/namecheap/mcp.mjs",
6
+ },
7
+ railway: {
8
+ name: "railway",
9
+ description: "Inspect Railway projects, environments, and deployments.",
10
+ mcpModule: "../tools/railway/mcp.mjs",
11
+ },
12
+ npm: {
13
+ name: "npm",
14
+ description: "Inspect npm packages, auth, tokens, and publishing setup.",
15
+ mcpModule: "../tools/npm/mcp.mjs",
16
+ },
17
+ };
18
+
19
+ export const listTools = () => Object.values(tools);
20
+
21
+ export const getTool = (name) => {
22
+ const tool = tools[name];
23
+ if (!tool) {
24
+ throw new Error(
25
+ `Unknown tool "${name}". Available tools: ${Object.keys(tools).join(", ")}`,
26
+ );
27
+ }
28
+ return tool;
29
+ };
package/src/index.mjs ADDED
@@ -0,0 +1,47 @@
1
+ export {
2
+ AUTH_CONFIG_PATH,
3
+ CONFIG_ROOT,
4
+ clearStoredAuthConfig,
5
+ connectNamecheap,
6
+ disconnectNamecheap,
7
+ getAuthStatus as getNamecheapAuthStatus,
8
+ getResolvedAuthConfig,
9
+ resolvePublicIpv4,
10
+ runBrowserAuthFlow,
11
+ saveAuthConfig,
12
+ } from "./tools/namecheap/auth.mjs";
13
+ export {
14
+ createNamecheapClient,
15
+ createResolvedNamecheapClient,
16
+ NamecheapApiError,
17
+ } from "./tools/namecheap/client.mjs";
18
+ export {
19
+ createRailwayClient,
20
+ getRailwayAuthStatus,
21
+ RailwayApiError,
22
+ resolveRailwayApiToken,
23
+ } from "./tools/railway/client.mjs";
24
+ export {
25
+ clearStoredRailwayAuthConfig,
26
+ connectRailway,
27
+ disconnectRailway,
28
+ RAILWAY_AUTH_CONFIG_PATH,
29
+ runRailwayBrowserAuthFlow,
30
+ saveRailwayAuthConfig,
31
+ } from "./tools/railway/auth.mjs";
32
+ export {
33
+ clearStoredNpmAuthConfig,
34
+ connectNpm,
35
+ disconnectNpm,
36
+ getNpmAuthStatus,
37
+ NPM_AUTH_CONFIG_PATH,
38
+ resolveNpmAuthConfig,
39
+ runNpmBrowserAuthFlow,
40
+ saveNpmAuthConfig,
41
+ } from "./tools/npm/auth.mjs";
42
+ export {
43
+ createNpmClient,
44
+ encodePackageName,
45
+ NpmRegistryError,
46
+ } from "./tools/npm/client.mjs";
47
+ export { getTool, listTools, tools } from "./core/tool-registry.mjs";
@@ -0,0 +1,429 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isIP } from "node:net";
4
+ import {
5
+ CONFIG_ROOT,
6
+ escapeHtml,
7
+ isTruthyEnv,
8
+ openBrowser,
9
+ parseFormBody,
10
+ pickString,
11
+ readJsonConfig,
12
+ removeJsonConfig,
13
+ resolveConfigPath,
14
+ writeJsonConfig,
15
+ } from "../../core/config-store.mjs";
16
+
17
+ const AUTH_CONFIG_PATH = resolveConfigPath({
18
+ env: process.env,
19
+ envVar: "NAMECHEAP_AUTH_CONFIG_PATH",
20
+ fileName: "namecheap.json",
21
+ });
22
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
23
+
24
+ const resolveStoredConfig = async () => {
25
+ const stored = await readJsonConfig(AUTH_CONFIG_PATH);
26
+ if (stored) {
27
+ return stored;
28
+ }
29
+
30
+ const legacyPath = AUTH_CONFIG_PATH.endsWith("namecheap.json")
31
+ ? AUTH_CONFIG_PATH.replace(/namecheap\.json$/, "namecheap-auth.json")
32
+ : null;
33
+
34
+ return legacyPath ? await readJsonConfig(legacyPath) : null;
35
+ };
36
+
37
+ export const resolvePublicIpv4 = async ({
38
+ fetchImpl = globalThis.fetch,
39
+ timeoutMs = 2500,
40
+ } = {}) => {
41
+ if (typeof fetchImpl !== "function") {
42
+ return null;
43
+ }
44
+
45
+ const controller = new AbortController();
46
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
47
+
48
+ try {
49
+ const response = await fetchImpl("https://api.ipify.org?format=json", {
50
+ signal: controller.signal,
51
+ });
52
+ const payload = await response.json();
53
+ const ip = typeof payload?.ip === "string" ? payload.ip.trim() : "";
54
+ return isIP(ip) === 4 ? ip : null;
55
+ } catch {
56
+ return null;
57
+ } finally {
58
+ clearTimeout(timeoutId);
59
+ }
60
+ };
61
+
62
+ export const getResolvedAuthConfig = async () => {
63
+ const stored = await resolveStoredConfig();
64
+ const sandboxEnv = process.env.NAMECHEAP_API_SANDBOX;
65
+
66
+ const config = {
67
+ apiUser: pickString(process.env.NAMECHEAP_API_USER, stored?.apiUser),
68
+ apiKey: pickString(process.env.NAMECHEAP_API_KEY, stored?.apiKey),
69
+ username: pickString(process.env.NAMECHEAP_USERNAME, stored?.username),
70
+ clientIp: pickString(process.env.NAMECHEAP_CLIENT_IP, stored?.clientIp),
71
+ baseUrl: pickString(process.env.NAMECHEAP_API_BASE_URL, stored?.baseUrl),
72
+ sandbox:
73
+ sandboxEnv != null
74
+ ? isTruthyEnv(sandboxEnv)
75
+ : typeof stored?.sandbox === "boolean"
76
+ ? stored.sandbox
77
+ : false,
78
+ source:
79
+ process.env.NAMECHEAP_API_USER ||
80
+ process.env.NAMECHEAP_API_KEY ||
81
+ process.env.NAMECHEAP_USERNAME ||
82
+ process.env.NAMECHEAP_CLIENT_IP
83
+ ? "env"
84
+ : stored
85
+ ? "file"
86
+ : "none",
87
+ };
88
+
89
+ return config;
90
+ };
91
+
92
+ export const getAuthStatus = async () => {
93
+ const config = await getResolvedAuthConfig();
94
+ const hasCredentials = Boolean(
95
+ config.apiUser && config.apiKey && config.username && config.clientIp,
96
+ );
97
+
98
+ return {
99
+ configured: hasCredentials,
100
+ source: config.source,
101
+ sandbox: config.sandbox,
102
+ baseUrl: config.baseUrl || null,
103
+ hasApiUser: Boolean(config.apiUser),
104
+ hasApiKey: Boolean(config.apiKey),
105
+ hasUsername: Boolean(config.username),
106
+ hasClientIp: Boolean(config.clientIp),
107
+ configPath: AUTH_CONFIG_PATH,
108
+ };
109
+ };
110
+
111
+ export const saveAuthConfig = async ({
112
+ apiUser,
113
+ apiKey,
114
+ username,
115
+ clientIp,
116
+ sandbox = false,
117
+ baseUrl = "",
118
+ } = {}) => {
119
+ const config = {
120
+ apiUser: String(apiUser ?? "").trim(),
121
+ apiKey: String(apiKey ?? "").trim(),
122
+ username: String(username ?? "").trim(),
123
+ clientIp: String(clientIp ?? "").trim(),
124
+ sandbox: Boolean(sandbox),
125
+ baseUrl: String(baseUrl ?? "").trim(),
126
+ savedAt: new Date().toISOString(),
127
+ };
128
+
129
+ for (const [key, value] of Object.entries(config)) {
130
+ if (
131
+ ["apiUser", "apiKey", "username", "clientIp"].includes(key) &&
132
+ (!value || String(value).trim().length === 0)
133
+ ) {
134
+ throw new Error(`Missing ${key} in Namecheap auth config.`);
135
+ }
136
+ }
137
+
138
+ await writeJsonConfig(AUTH_CONFIG_PATH, config);
139
+
140
+ return {
141
+ configPath: AUTH_CONFIG_PATH,
142
+ sandbox: config.sandbox,
143
+ };
144
+ };
145
+
146
+ /* v8 ignore start */
147
+ const renderPage = ({ csrfToken, message = "", defaults = {} }) => `<!doctype html>
148
+ <html lang="en">
149
+ <head>
150
+ <meta charset="utf-8" />
151
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
152
+ <title>Namecheap Setup</title>
153
+ <style>
154
+ :root {
155
+ color-scheme: light;
156
+ --bg: #f5efe8;
157
+ --panel: #fffaf5;
158
+ --text: #1f1a16;
159
+ --muted: #6f6258;
160
+ --accent: #de3723;
161
+ --border: #e5d3c4;
162
+ }
163
+ body {
164
+ margin: 0;
165
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
166
+ background: radial-gradient(circle at top, #fff3ea, var(--bg));
167
+ color: var(--text);
168
+ }
169
+ .wrap {
170
+ max-width: 760px;
171
+ margin: 32px auto;
172
+ padding: 24px;
173
+ }
174
+ .panel {
175
+ background: var(--panel);
176
+ border: 1px solid var(--border);
177
+ border-radius: 20px;
178
+ padding: 24px;
179
+ box-shadow: 0 20px 50px rgba(70, 43, 20, 0.08);
180
+ }
181
+ h1 { margin-top: 0; font-size: 28px; }
182
+ p, li { line-height: 1.5; color: var(--muted); }
183
+ a { color: var(--accent); }
184
+ form { display: grid; gap: 16px; margin-top: 24px; }
185
+ label { display: grid; gap: 6px; font-weight: 600; }
186
+ input[type="text"], input[type="password"] {
187
+ padding: 12px 14px;
188
+ border-radius: 12px;
189
+ border: 1px solid var(--border);
190
+ font: inherit;
191
+ background: white;
192
+ }
193
+ .row { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; }
194
+ .message {
195
+ margin-top: 16px;
196
+ padding: 12px 14px;
197
+ border-radius: 12px;
198
+ background: #fff1ec;
199
+ color: #8f2719;
200
+ }
201
+ button {
202
+ border: 0;
203
+ border-radius: 999px;
204
+ padding: 12px 18px;
205
+ font: inherit;
206
+ font-weight: 700;
207
+ color: white;
208
+ background: var(--accent);
209
+ cursor: pointer;
210
+ width: fit-content;
211
+ }
212
+ code {
213
+ background: #f1e3d6;
214
+ padding: 2px 6px;
215
+ border-radius: 6px;
216
+ }
217
+ @media (max-width: 720px) {
218
+ .row { grid-template-columns: 1fr; }
219
+ }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ <div class="wrap">
224
+ <div class="panel">
225
+ <h1>Connect Namecheap</h1>
226
+ <p>Namecheap does not use OAuth here. You need to generate an API key in Namecheap and whitelist your IPv4 address, then paste the values below.</p>
227
+ ${
228
+ defaults.detectedClientIp
229
+ ? `<p>Detected public IPv4: <code>${escapeHtml(defaults.detectedClientIp)}</code></p>`
230
+ : ""
231
+ }
232
+ <ol>
233
+ <li>Open <a href="https://www.namecheap.com/support/knowledgebase/article.aspx/763/63/what-is-sandbox/" target="_blank" rel="noreferrer">Sandbox setup</a> if you want safe testing first.</li>
234
+ <li>Open <a href="https://ap.www.namecheap.com/settings/tools/apiaccess/" target="_blank" rel="noreferrer">production API access</a> or the Sandbox account’s API access page.</li>
235
+ <li>Enable API access and whitelist the same IPv4 address you enter as <code>Client IP</code>.</li>
236
+ <li>Copy your API user, API key, and username into this form.</li>
237
+ </ol>
238
+ ${
239
+ message
240
+ ? `<div class="message">${escapeHtml(message)}</div>`
241
+ : ""
242
+ }
243
+ <form method="post" action="/save">
244
+ <input type="hidden" name="csrfToken" value="${escapeHtml(csrfToken)}" />
245
+ <div class="row">
246
+ <label>
247
+ API User
248
+ <input name="apiUser" type="text" value="${escapeHtml(defaults.apiUser ?? "")}" required />
249
+ </label>
250
+ <label>
251
+ Username
252
+ <input name="username" type="text" value="${escapeHtml(defaults.username ?? "")}" required />
253
+ </label>
254
+ </div>
255
+ <label>
256
+ API Key
257
+ <input name="apiKey" type="password" value="${escapeHtml(defaults.apiKey ?? "")}" required />
258
+ </label>
259
+ <div class="row">
260
+ <label>
261
+ Client IP (must be whitelisted in Namecheap)
262
+ <input name="clientIp" type="text" value="${escapeHtml(defaults.clientIp ?? "")}" required />
263
+ </label>
264
+ <label>
265
+ API Base URL Override (optional)
266
+ <input name="baseUrl" type="text" value="${escapeHtml(defaults.baseUrl ?? "")}" placeholder="https://api.namecheap.com/xml.response" />
267
+ </label>
268
+ </div>
269
+ <label style="display:flex; align-items:center; gap:10px; font-weight:500;">
270
+ <input name="sandbox" type="checkbox" value="1" ${defaults.sandbox ? "checked" : ""} />
271
+ Use Sandbox account
272
+ </label>
273
+ <button type="submit">Save Namecheap Credentials</button>
274
+ </form>
275
+ </div>
276
+ </div>
277
+ </body>
278
+ </html>`;
279
+
280
+ export const runBrowserAuthFlow = async ({
281
+ defaultSandbox = true,
282
+ fetchImpl = globalThis.fetch,
283
+ validateConnection = true,
284
+ timeoutMs = DEFAULT_TIMEOUT_MS,
285
+ } = {}) => {
286
+ const existing = await getResolvedAuthConfig();
287
+ const detectedClientIp = existing.clientIp
288
+ ? null
289
+ : await resolvePublicIpv4({ fetchImpl });
290
+ const csrfToken = randomUUID();
291
+
292
+ return await new Promise((resolve, reject) => {
293
+ let closed = false;
294
+ let timeoutId;
295
+
296
+ const finish = (callback) => {
297
+ if (closed) {
298
+ return;
299
+ }
300
+ closed = true;
301
+ clearTimeout(timeoutId);
302
+ server.close(() => callback());
303
+ };
304
+
305
+ const server = createServer(async (request, response) => {
306
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
307
+
308
+ if (request.method === "GET" && requestUrl.pathname === "/") {
309
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
310
+ response.end(
311
+ renderPage({
312
+ csrfToken,
313
+ defaults: {
314
+ apiUser: existing.apiUser ?? "",
315
+ username: existing.username ?? "",
316
+ clientIp: existing.clientIp ?? detectedClientIp ?? "",
317
+ detectedClientIp,
318
+ baseUrl: existing.baseUrl ?? "",
319
+ sandbox: existing.sandbox || defaultSandbox,
320
+ },
321
+ }),
322
+ );
323
+ return;
324
+ }
325
+
326
+ if (request.method === "POST" && requestUrl.pathname === "/save") {
327
+ try {
328
+ const body = await parseFormBody(request);
329
+ if (body.csrfToken !== csrfToken) {
330
+ response.writeHead(403, {
331
+ "content-type": "text/html; charset=utf-8",
332
+ });
333
+ response.end("Invalid CSRF token.");
334
+ return;
335
+ }
336
+
337
+ if (validateConnection) {
338
+ const { createNamecheapClient } = await import("./client.mjs");
339
+ const client = createNamecheapClient({
340
+ apiUser: body.apiUser,
341
+ apiKey: body.apiKey,
342
+ username: body.username,
343
+ clientIp: body.clientIp,
344
+ baseUrl: body.baseUrl,
345
+ sandbox: body.sandbox === "1",
346
+ });
347
+ await client.listDomains({ page: 1, pageSize: 1 });
348
+ }
349
+
350
+ const saved = await saveAuthConfig({
351
+ apiUser: body.apiUser,
352
+ apiKey: body.apiKey,
353
+ username: body.username,
354
+ clientIp: body.clientIp,
355
+ baseUrl: body.baseUrl,
356
+ sandbox: body.sandbox === "1",
357
+ });
358
+
359
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
360
+ response.end(`<!doctype html><html><body style="font-family: sans-serif; padding: 24px;"><h1>Namecheap connected</h1><p>Credentials were saved to <code>${escapeHtml(saved.configPath)}</code>. You can close this tab.</p></body></html>`);
361
+ finish(() => resolve(saved));
362
+ } catch (error) {
363
+ response.writeHead(400, {
364
+ "content-type": "text/html; charset=utf-8",
365
+ });
366
+ response.end(
367
+ renderPage({
368
+ csrfToken,
369
+ message:
370
+ error instanceof Error ? error.message : String(error),
371
+ defaults: {
372
+ apiUser: "",
373
+ username: "",
374
+ clientIp: "",
375
+ baseUrl: "",
376
+ sandbox: defaultSandbox,
377
+ },
378
+ }),
379
+ );
380
+ }
381
+ return;
382
+ }
383
+
384
+ response.writeHead(404);
385
+ response.end("Not found");
386
+ });
387
+
388
+ server.listen(0, "127.0.0.1", async () => {
389
+ try {
390
+ const address = server.address();
391
+ if (!address || typeof address === "string") {
392
+ throw new Error("Failed to bind local Namecheap auth server.");
393
+ }
394
+
395
+ const url = `http://127.0.0.1:${address.port}/`;
396
+ await openBrowser(url, { skipEnvVar: "NAMECHEAP_SKIP_BROWSER_OPEN" });
397
+ } catch (error) {
398
+ finish(() => reject(error));
399
+ }
400
+ });
401
+
402
+ timeoutId = setTimeout(() => {
403
+ finish(() =>
404
+ reject(
405
+ new Error(
406
+ "Timed out waiting for Namecheap browser setup to complete.",
407
+ ),
408
+ ),
409
+ );
410
+ }, timeoutMs);
411
+ });
412
+ };
413
+ /* v8 ignore stop */
414
+
415
+ export const clearStoredAuthConfig = async () => {
416
+ await removeJsonConfig(AUTH_CONFIG_PATH);
417
+ };
418
+
419
+ export const connectNamecheap = runBrowserAuthFlow;
420
+
421
+ export const disconnectNamecheap = async () => {
422
+ await clearStoredAuthConfig();
423
+ return {
424
+ disconnected: true,
425
+ configPath: AUTH_CONFIG_PATH,
426
+ };
427
+ };
428
+
429
+ export { AUTH_CONFIG_PATH, CONFIG_ROOT };