@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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/SECURITY.md +47 -0
- package/adapters/claude/namecheap/README.md +13 -0
- package/adapters/claude/npm/README.md +11 -0
- package/adapters/claude/railway/README.md +11 -0
- package/adapters/codex/namecheap/.codex-plugin/plugin.json +40 -0
- package/adapters/codex/namecheap/.mcp.json +21 -0
- package/adapters/codex/namecheap/SKILL.md +40 -0
- package/adapters/codex/npm/.codex-plugin/plugin.json +41 -0
- package/adapters/codex/npm/.mcp.json +18 -0
- package/adapters/codex/npm/SKILL.md +54 -0
- package/adapters/codex/railway/.codex-plugin/plugin.json +39 -0
- package/adapters/codex/railway/.mcp.json +20 -0
- package/adapters/codex/railway/SKILL.md +44 -0
- package/docs/README.md +14 -0
- package/docs/architecture.md +208 -0
- package/docs/auth-and-setup-guidelines.md +261 -0
- package/docs/migration-plan.md +55 -0
- package/docs/open-source-readiness.md +119 -0
- package/docs/publishing.md +211 -0
- package/docs/testing.md +61 -0
- package/docs/usage.md +144 -0
- package/package.json +78 -0
- package/src/cli.mjs +158 -0
- package/src/core/config-store.mjs +106 -0
- package/src/core/result.mjs +13 -0
- package/src/core/tool-registry.mjs +29 -0
- package/src/index.mjs +47 -0
- package/src/tools/namecheap/auth.mjs +429 -0
- package/src/tools/namecheap/client.mjs +655 -0
- package/src/tools/namecheap/mcp.mjs +298 -0
- package/src/tools/npm/auth.mjs +367 -0
- package/src/tools/npm/client.mjs +317 -0
- package/src/tools/npm/mcp.mjs +343 -0
- package/src/tools/railway/auth.mjs +402 -0
- package/src/tools/railway/client.mjs +388 -0
- package/src/tools/railway/mcp.mjs +282 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
escapeHtml,
|
|
5
|
+
openBrowser,
|
|
6
|
+
parseFormBody,
|
|
7
|
+
pickString,
|
|
8
|
+
readJsonConfigSync,
|
|
9
|
+
removeJsonConfig,
|
|
10
|
+
resolveConfigPath,
|
|
11
|
+
writeJsonConfig,
|
|
12
|
+
} from "../../core/config-store.mjs";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_RAILWAY_API_ENDPOINT =
|
|
15
|
+
"https://backboard.railway.com/graphql/v2";
|
|
16
|
+
|
|
17
|
+
export const RAILWAY_AUTH_CONFIG_PATH = resolveConfigPath({
|
|
18
|
+
env: process.env,
|
|
19
|
+
envVar: "RAILWAY_AUTH_CONFIG_PATH",
|
|
20
|
+
fileName: "railway.json",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const shouldReadStoredConfig = (env) =>
|
|
26
|
+
env === process.env || typeof env.RAILWAY_AUTH_CONFIG_PATH === "string";
|
|
27
|
+
|
|
28
|
+
const readStoredRailwayAuthConfig = (env = process.env) => {
|
|
29
|
+
if (!shouldReadStoredConfig(env)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const configPath = resolveConfigPath({
|
|
34
|
+
env,
|
|
35
|
+
envVar: "RAILWAY_AUTH_CONFIG_PATH",
|
|
36
|
+
fileName: "railway.json",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return readJsonConfigSync(configPath);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const saveRailwayAuthConfig = async ({
|
|
43
|
+
token,
|
|
44
|
+
kind = "account",
|
|
45
|
+
defaultProjectId = "",
|
|
46
|
+
endpoint = "",
|
|
47
|
+
} = {}) => {
|
|
48
|
+
const normalizedKind = String(kind ?? "").trim();
|
|
49
|
+
if (!["account", "project"].includes(normalizedKind)) {
|
|
50
|
+
throw new Error("Railway token kind must be account or project.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const config = {
|
|
54
|
+
token: String(token ?? "").trim(),
|
|
55
|
+
kind: normalizedKind,
|
|
56
|
+
defaultProjectId: String(defaultProjectId ?? "").trim(),
|
|
57
|
+
endpoint: String(endpoint ?? "").trim(),
|
|
58
|
+
savedAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!config.token) {
|
|
62
|
+
throw new Error("Missing token in Railway auth config.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await writeJsonConfig(RAILWAY_AUTH_CONFIG_PATH, config);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
configPath: RAILWAY_AUTH_CONFIG_PATH,
|
|
69
|
+
kind: config.kind,
|
|
70
|
+
defaultProjectId: config.defaultProjectId || null,
|
|
71
|
+
endpoint: config.endpoint || DEFAULT_RAILWAY_API_ENDPOINT,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const resolveRailwayApiToken = (env = process.env) => {
|
|
76
|
+
const projectToken = pickString(env.RAILWAY_PROJECT_TOKEN);
|
|
77
|
+
if (projectToken) {
|
|
78
|
+
return {
|
|
79
|
+
token: projectToken,
|
|
80
|
+
kind: "project",
|
|
81
|
+
source: "env:RAILWAY_PROJECT_TOKEN",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const apiToken = pickString(env.RAILWAY_API_TOKEN);
|
|
86
|
+
if (apiToken) {
|
|
87
|
+
return {
|
|
88
|
+
token: apiToken,
|
|
89
|
+
kind: "account",
|
|
90
|
+
source: "env:RAILWAY_API_TOKEN",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const token = pickString(env.RAILWAY_TOKEN);
|
|
95
|
+
if (token) {
|
|
96
|
+
return {
|
|
97
|
+
token,
|
|
98
|
+
kind: "account",
|
|
99
|
+
source: "env:RAILWAY_TOKEN",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stored = readStoredRailwayAuthConfig(env);
|
|
104
|
+
if (stored?.token) {
|
|
105
|
+
return {
|
|
106
|
+
token: stored.token,
|
|
107
|
+
kind: stored.kind === "project" ? "project" : "account",
|
|
108
|
+
source: "file",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
token: null,
|
|
114
|
+
kind: null,
|
|
115
|
+
source: null,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const getRailwayAuthStatus = (env = process.env) => {
|
|
120
|
+
const stored = readStoredRailwayAuthConfig(env);
|
|
121
|
+
const auth = resolveRailwayApiToken(env);
|
|
122
|
+
return {
|
|
123
|
+
configured: Boolean(auth.token),
|
|
124
|
+
kind: auth.kind,
|
|
125
|
+
source: auth.source,
|
|
126
|
+
endpoint:
|
|
127
|
+
pickString(env.RAILWAY_API_ENDPOINT) ||
|
|
128
|
+
pickString(stored?.endpoint) ||
|
|
129
|
+
DEFAULT_RAILWAY_API_ENDPOINT,
|
|
130
|
+
defaultProjectId:
|
|
131
|
+
pickString(env.RAILWAY_PROJECT_ID) ||
|
|
132
|
+
pickString(stored?.defaultProjectId) ||
|
|
133
|
+
null,
|
|
134
|
+
configPath: resolveConfigPath({
|
|
135
|
+
env,
|
|
136
|
+
envVar: "RAILWAY_AUTH_CONFIG_PATH",
|
|
137
|
+
fileName: "railway.json",
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const clearStoredRailwayAuthConfig = async () => {
|
|
143
|
+
await removeJsonConfig(RAILWAY_AUTH_CONFIG_PATH);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const disconnectRailway = async () => {
|
|
147
|
+
await clearStoredRailwayAuthConfig();
|
|
148
|
+
return {
|
|
149
|
+
disconnected: true,
|
|
150
|
+
configPath: RAILWAY_AUTH_CONFIG_PATH,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/* v8 ignore start */
|
|
155
|
+
const renderRailwayPage = ({ csrfToken, message = "", defaults = {} }) => `<!doctype html>
|
|
156
|
+
<html lang="en">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="utf-8" />
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
160
|
+
<title>Railway Setup</title>
|
|
161
|
+
<style>
|
|
162
|
+
:root {
|
|
163
|
+
color-scheme: light;
|
|
164
|
+
--bg: #f6f7f9;
|
|
165
|
+
--panel: #ffffff;
|
|
166
|
+
--text: #17181c;
|
|
167
|
+
--muted: #5d6472;
|
|
168
|
+
--accent: #6d5dfc;
|
|
169
|
+
--border: #dde1ea;
|
|
170
|
+
}
|
|
171
|
+
body {
|
|
172
|
+
margin: 0;
|
|
173
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
174
|
+
background: var(--bg);
|
|
175
|
+
color: var(--text);
|
|
176
|
+
}
|
|
177
|
+
.wrap {
|
|
178
|
+
max-width: 760px;
|
|
179
|
+
margin: 32px auto;
|
|
180
|
+
padding: 24px;
|
|
181
|
+
}
|
|
182
|
+
.panel {
|
|
183
|
+
background: var(--panel);
|
|
184
|
+
border: 1px solid var(--border);
|
|
185
|
+
border-radius: 16px;
|
|
186
|
+
padding: 24px;
|
|
187
|
+
}
|
|
188
|
+
h1 { margin-top: 0; font-size: 28px; }
|
|
189
|
+
p, li { line-height: 1.5; color: var(--muted); }
|
|
190
|
+
a { color: var(--accent); }
|
|
191
|
+
form { display: grid; gap: 16px; margin-top: 24px; }
|
|
192
|
+
label { display: grid; gap: 6px; font-weight: 600; }
|
|
193
|
+
input, select {
|
|
194
|
+
padding: 12px 14px;
|
|
195
|
+
border-radius: 10px;
|
|
196
|
+
border: 1px solid var(--border);
|
|
197
|
+
font: inherit;
|
|
198
|
+
background: white;
|
|
199
|
+
}
|
|
200
|
+
.row { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; }
|
|
201
|
+
.message {
|
|
202
|
+
margin-top: 16px;
|
|
203
|
+
padding: 12px 14px;
|
|
204
|
+
border-radius: 10px;
|
|
205
|
+
background: #fff1ec;
|
|
206
|
+
color: #8f2719;
|
|
207
|
+
}
|
|
208
|
+
button {
|
|
209
|
+
border: 0;
|
|
210
|
+
border-radius: 999px;
|
|
211
|
+
padding: 12px 18px;
|
|
212
|
+
font: inherit;
|
|
213
|
+
font-weight: 700;
|
|
214
|
+
color: white;
|
|
215
|
+
background: var(--accent);
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
width: fit-content;
|
|
218
|
+
}
|
|
219
|
+
code {
|
|
220
|
+
background: #eceef5;
|
|
221
|
+
padding: 2px 6px;
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
}
|
|
224
|
+
@media (max-width: 720px) {
|
|
225
|
+
.row { grid-template-columns: 1fr; }
|
|
226
|
+
}
|
|
227
|
+
</style>
|
|
228
|
+
</head>
|
|
229
|
+
<body>
|
|
230
|
+
<div class="wrap">
|
|
231
|
+
<div class="panel">
|
|
232
|
+
<h1>Connect Railway</h1>
|
|
233
|
+
<p>Paste a Railway token. Project tokens are lowest risk for project inspection. Account tokens enable account identity and project listing.</p>
|
|
234
|
+
<ol>
|
|
235
|
+
<li>Open <a href="https://railway.com/account/tokens" target="_blank" rel="noreferrer">Railway account tokens</a> for an account token.</li>
|
|
236
|
+
<li>For a narrower token, create a project token in the Railway project settings.</li>
|
|
237
|
+
<li>Save it here, then run <code>agentic-devtools test-connection railway</code>.</li>
|
|
238
|
+
</ol>
|
|
239
|
+
${
|
|
240
|
+
message
|
|
241
|
+
? `<div class="message">${escapeHtml(message)}</div>`
|
|
242
|
+
: ""
|
|
243
|
+
}
|
|
244
|
+
<form method="post" action="/save">
|
|
245
|
+
<input type="hidden" name="csrfToken" value="${escapeHtml(csrfToken)}" />
|
|
246
|
+
<label>
|
|
247
|
+
Token
|
|
248
|
+
<input name="token" type="password" value="" required />
|
|
249
|
+
</label>
|
|
250
|
+
<div class="row">
|
|
251
|
+
<label>
|
|
252
|
+
Token Scope
|
|
253
|
+
<select name="kind">
|
|
254
|
+
<option value="project" ${defaults.kind === "project" ? "selected" : ""}>Project token</option>
|
|
255
|
+
<option value="account" ${defaults.kind !== "project" ? "selected" : ""}>Account token</option>
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
<label>
|
|
259
|
+
Default Project ID (optional)
|
|
260
|
+
<input name="defaultProjectId" type="text" value="${escapeHtml(defaults.defaultProjectId ?? "")}" />
|
|
261
|
+
</label>
|
|
262
|
+
</div>
|
|
263
|
+
<label>
|
|
264
|
+
API Endpoint Override (optional)
|
|
265
|
+
<input name="endpoint" type="text" value="${escapeHtml(defaults.endpoint ?? "")}" placeholder="${DEFAULT_RAILWAY_API_ENDPOINT}" />
|
|
266
|
+
</label>
|
|
267
|
+
<button type="submit">Save Railway Token</button>
|
|
268
|
+
</form>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</body>
|
|
272
|
+
</html>`;
|
|
273
|
+
|
|
274
|
+
export const runRailwayBrowserAuthFlow = async ({
|
|
275
|
+
validateConnection = true,
|
|
276
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
277
|
+
} = {}) => {
|
|
278
|
+
const status = getRailwayAuthStatus();
|
|
279
|
+
const csrfToken = randomUUID();
|
|
280
|
+
|
|
281
|
+
return await new Promise((resolve, reject) => {
|
|
282
|
+
let closed = false;
|
|
283
|
+
let timeoutId;
|
|
284
|
+
|
|
285
|
+
const finish = (callback) => {
|
|
286
|
+
if (closed) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
closed = true;
|
|
290
|
+
clearTimeout(timeoutId);
|
|
291
|
+
server.close(() => callback());
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const server = createServer(async (request, response) => {
|
|
295
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
296
|
+
|
|
297
|
+
if (request.method === "GET" && requestUrl.pathname === "/") {
|
|
298
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
299
|
+
response.end(
|
|
300
|
+
renderRailwayPage({
|
|
301
|
+
csrfToken,
|
|
302
|
+
defaults: {
|
|
303
|
+
kind: status.kind ?? "project",
|
|
304
|
+
defaultProjectId: status.defaultProjectId ?? "",
|
|
305
|
+
endpoint:
|
|
306
|
+
status.endpoint === DEFAULT_RAILWAY_API_ENDPOINT
|
|
307
|
+
? ""
|
|
308
|
+
: status.endpoint,
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (request.method === "POST" && requestUrl.pathname === "/save") {
|
|
316
|
+
let body = {};
|
|
317
|
+
try {
|
|
318
|
+
body = await parseFormBody(request);
|
|
319
|
+
if (body.csrfToken !== csrfToken) {
|
|
320
|
+
response.writeHead(403, {
|
|
321
|
+
"content-type": "text/html; charset=utf-8",
|
|
322
|
+
});
|
|
323
|
+
response.end("Invalid CSRF token.");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (validateConnection) {
|
|
328
|
+
const { createRailwayClient } = await import("./client.mjs");
|
|
329
|
+
const client = createRailwayClient({
|
|
330
|
+
env: {
|
|
331
|
+
RAILWAY_PROJECT_TOKEN:
|
|
332
|
+
body.kind === "project" ? String(body.token ?? "") : "",
|
|
333
|
+
RAILWAY_API_TOKEN:
|
|
334
|
+
body.kind === "account" ? String(body.token ?? "") : "",
|
|
335
|
+
RAILWAY_PROJECT_ID: String(body.defaultProjectId ?? ""),
|
|
336
|
+
RAILWAY_API_ENDPOINT: String(body.endpoint ?? ""),
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
if (client.auth.kind === "project") {
|
|
340
|
+
await client.getProjectTokenContext();
|
|
341
|
+
} else {
|
|
342
|
+
await client.getCurrentViewer();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const saved = await saveRailwayAuthConfig({
|
|
347
|
+
token: body.token,
|
|
348
|
+
kind: body.kind,
|
|
349
|
+
defaultProjectId: body.defaultProjectId,
|
|
350
|
+
endpoint: body.endpoint,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
354
|
+
response.end(`<!doctype html><html><body style="font-family: sans-serif; padding: 24px;"><h1>Railway connected</h1><p>Token was saved to <code>${escapeHtml(saved.configPath)}</code>. You can close this tab.</p></body></html>`);
|
|
355
|
+
finish(() => resolve(saved));
|
|
356
|
+
} catch (error) {
|
|
357
|
+
response.writeHead(400, {
|
|
358
|
+
"content-type": "text/html; charset=utf-8",
|
|
359
|
+
});
|
|
360
|
+
response.end(
|
|
361
|
+
renderRailwayPage({
|
|
362
|
+
csrfToken,
|
|
363
|
+
message: error instanceof Error ? error.message : String(error),
|
|
364
|
+
defaults: {
|
|
365
|
+
kind: body.kind,
|
|
366
|
+
defaultProjectId: body.defaultProjectId,
|
|
367
|
+
endpoint: body.endpoint,
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
response.writeHead(404);
|
|
376
|
+
response.end("Not found");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
380
|
+
try {
|
|
381
|
+
const address = server.address();
|
|
382
|
+
if (!address || typeof address === "string") {
|
|
383
|
+
throw new Error("Failed to bind local Railway auth server.");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const url = `http://127.0.0.1:${address.port}/`;
|
|
387
|
+
await openBrowser(url, { skipEnvVar: "RAILWAY_SKIP_BROWSER_OPEN" });
|
|
388
|
+
} catch (error) {
|
|
389
|
+
finish(() => reject(error));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
timeoutId = setTimeout(() => {
|
|
394
|
+
finish(() =>
|
|
395
|
+
reject(new Error("Timed out waiting for Railway browser setup to complete.")),
|
|
396
|
+
);
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
/* v8 ignore stop */
|
|
401
|
+
|
|
402
|
+
export const connectRailway = runRailwayBrowserAuthFlow;
|