godmode 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomas Sivicki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addApi
4
+ } from "./chunk-FBMQ3AC4.js";
5
+ import "./chunk-EHS56XXY.js";
6
+
7
+ // src/commands/add.ts
8
+ async function runAdd(args) {
9
+ if (!args[0] || args[0] === "--help" || args[0] === "-h") {
10
+ console.log(`Add an adapter from a folder, built-in name, or manifest.
11
+
12
+ Usage:
13
+ godmode add <name> Built-in adapter (e.g. stripe, github)
14
+ godmode add <folder> Folder containing manifest.yaml
15
+
16
+ Manifest format (manifest.yaml):
17
+ slug: stripe CLI name (used as "godmode <slug>")
18
+ name: Stripe Display name
19
+ description: Payments API Short description
20
+ type: api api | graphql | mcp
21
+ spec: <url> OpenAPI spec URL or local file (api, graphql)
22
+ url: <base-url> API base URL (api, graphql) or MCP endpoint (mcp)
23
+ auth:
24
+ env: STRIPE_API_KEY Environment variable for auth token
25
+ type: bearer Auth type (bearer, api-key, basic)
26
+ headers:
27
+ X-Custom: value Default headers for every request
28
+
29
+ Types:
30
+ api OpenAPI spec (requires spec + url)
31
+ graphql GraphQL (requires spec or url)
32
+ mcp MCP endpoint (requires url)
33
+
34
+ Examples:
35
+ $ godmode add stripe
36
+ $ godmode add ./my-adapter
37
+ $ godmode add openai`);
38
+ process.exit(args[0] ? 0 : 1);
39
+ }
40
+ await addApi(args[0]);
41
+ }
42
+ export {
43
+ runAdd
44
+ };
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/protocols/mcp.ts
4
+ var requestId = 0;
5
+ async function mcpRequest(url, method, params, headers, sessionId) {
6
+ const body = {
7
+ jsonrpc: "2.0",
8
+ id: ++requestId,
9
+ method,
10
+ params
11
+ };
12
+ const reqHeaders = {
13
+ "Content-Type": "application/json",
14
+ "Accept": "application/json, text/event-stream",
15
+ "MCP-Protocol-Version": "2025-03-26",
16
+ ...headers
17
+ };
18
+ if (sessionId) reqHeaders["Mcp-Session-Id"] = sessionId;
19
+ const res = await fetch(url, {
20
+ method: "POST",
21
+ headers: reqHeaders,
22
+ body: JSON.stringify(body)
23
+ });
24
+ if (!res.ok) {
25
+ const text = await res.text();
26
+ throw new Error(`MCP request failed: ${res.status} ${text.slice(0, 200)}`);
27
+ }
28
+ const newSessionId = res.headers.get("Mcp-Session-Id") || sessionId;
29
+ const contentType = res.headers.get("content-type") || "";
30
+ if (contentType.includes("text/event-stream")) {
31
+ const text = await res.text();
32
+ const lines = text.split("\n");
33
+ for (const line of lines) {
34
+ if (line.startsWith("data: ")) {
35
+ try {
36
+ const data = JSON.parse(line.slice(6));
37
+ if (data.id === body.id) {
38
+ if (data.error) throw new Error(`MCP error: ${data.error.message}`);
39
+ return { result: data.result, sessionId: newSessionId };
40
+ }
41
+ } catch (e) {
42
+ if (e.message?.startsWith("MCP error")) throw e;
43
+ }
44
+ }
45
+ }
46
+ throw new Error("No response found in SSE stream");
47
+ }
48
+ const json = await res.json();
49
+ if (json.error) throw new Error(`MCP error: ${json.error.message}`);
50
+ return { result: json.result, sessionId: newSessionId };
51
+ }
52
+ function buildAuthHeaders(config) {
53
+ const headers = { ...config.headers };
54
+ const token = config.auth?.env ? process.env[config.auth.env] : void 0;
55
+ if (token) {
56
+ const authType = config.auth?.type || "bearer";
57
+ if (authType === "bearer") headers["Authorization"] = `Bearer ${token}`;
58
+ else if (authType === "api-key") headers[config.auth?.header || "X-API-Key"] = token;
59
+ }
60
+ return headers;
61
+ }
62
+ async function parseMcp(name, config) {
63
+ if (!config.url) throw new Error('MCP config needs "url" (MCP endpoint)');
64
+ const headers = buildAuthHeaders(config);
65
+ process.stderr.write(`Connecting to ${config.url}...
66
+ `);
67
+ const init = await mcpRequest(config.url, "initialize", {
68
+ protocolVersion: "2025-03-26",
69
+ capabilities: {},
70
+ clientInfo: { name: "godmode", version: "0.0.1" }
71
+ }, headers);
72
+ const sessionId = init.sessionId;
73
+ await fetch(config.url, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ "MCP-Protocol-Version": "2025-03-26",
78
+ ...headers,
79
+ ...sessionId ? { "Mcp-Session-Id": sessionId } : {}
80
+ },
81
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
82
+ });
83
+ process.stderr.write(`Discovering tools...
84
+ `);
85
+ const toolsResult = await mcpRequest(config.url, "tools/list", {}, headers, sessionId);
86
+ const tools = toolsResult.result?.tools || [];
87
+ const routes = tools.map((tool) => ({
88
+ path: tool.name,
89
+ method: "post",
90
+ // MCP tools are always invocations
91
+ summary: tool.description || "",
92
+ version: "",
93
+ segments: [{ value: tool.name, isParam: false }]
94
+ }));
95
+ routes.sort((a, b) => a.path.localeCompare(b.path));
96
+ process.stderr.write(`Discovered ${routes.length} tools
97
+ `);
98
+ const resourceDescriptions = {};
99
+ for (const tool of tools) {
100
+ if (tool.description) resourceDescriptions[tool.name] = tool.description;
101
+ }
102
+ return {
103
+ name,
104
+ description: config.description || init.result?.serverInfo?.name || "",
105
+ specVersion: init.result?.protocolVersion || "",
106
+ config: { ...config, _mcpTools: tools },
107
+ versions: [],
108
+ resourceDescriptions,
109
+ routes
110
+ };
111
+ }
112
+ function validateMcpFlags(method, query) {
113
+ if (["put", "patch", "delete", "head"].includes(method)) {
114
+ return `MCP tools are invoked via POST. ${method.toUpperCase()} is not valid.`;
115
+ }
116
+ if (Object.keys(query).length) {
117
+ return "MCP tools use key=value params (body), not key==value (query).";
118
+ }
119
+ return null;
120
+ }
121
+ async function executeMcpTool(config, toolName, args, options) {
122
+ const headers = buildAuthHeaders(config);
123
+ if (options.dryRun || options.verbose) {
124
+ process.stderr.write(`CALL ${config.url} \u2192 ${toolName}
125
+ `);
126
+ if (Object.keys(args).length) {
127
+ process.stderr.write(` Args: ${JSON.stringify(args)}
128
+ `);
129
+ }
130
+ if (options.dryRun) return "";
131
+ }
132
+ const init = await mcpRequest(config.url, "initialize", {
133
+ protocolVersion: "2025-03-26",
134
+ capabilities: {},
135
+ clientInfo: { name: "godmode", version: "0.0.1" }
136
+ }, headers);
137
+ const sessionId = init.sessionId;
138
+ await fetch(config.url, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ "MCP-Protocol-Version": "2025-03-26",
143
+ ...headers,
144
+ ...sessionId ? { "Mcp-Session-Id": sessionId } : {}
145
+ },
146
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
147
+ });
148
+ const result = await mcpRequest(config.url, "tools/call", {
149
+ name: toolName,
150
+ arguments: args
151
+ }, headers, sessionId);
152
+ const content = result.result?.content;
153
+ if (Array.isArray(content)) {
154
+ return content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
155
+ }
156
+ return JSON.stringify(result.result, null, 2);
157
+ }
158
+
159
+ export {
160
+ parseMcp,
161
+ validateMcpFlags,
162
+ executeMcpTool
163
+ };
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseMcp
4
+ } from "./chunk-EHS56XXY.js";
5
+
6
+ // src/protocols/graphql.ts
7
+ var INTROSPECTION_QUERY = `{
8
+ __schema {
9
+ queryType { name }
10
+ mutationType { name }
11
+ subscriptionType { name }
12
+ types {
13
+ name kind description
14
+ fields { name description args { name type { name kind ofType { name kind } } } }
15
+ }
16
+ }
17
+ }`;
18
+ async function introspect(url, headers) {
19
+ const res = await fetch(url, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json", ...headers },
22
+ body: JSON.stringify({ query: INTROSPECTION_QUERY })
23
+ });
24
+ if (!res.ok) throw new Error(`Introspection failed: ${res.status} ${res.statusText}`);
25
+ const json = await res.json();
26
+ return json.data?.__schema?.types?.filter((t) => !t.name.startsWith("__")) || [];
27
+ }
28
+ function parseSdl(text) {
29
+ const types = [];
30
+ const typeRegex = /type\s+(\w+)\s*\{([^}]*)}/g;
31
+ let match;
32
+ while ((match = typeRegex.exec(text)) !== null) {
33
+ const [, typeName, body] = match;
34
+ const fields = [];
35
+ const fieldRegex = /(\w+)(?:\(([^)]*)\))?\s*:\s*\S+/g;
36
+ let fm;
37
+ while ((fm = fieldRegex.exec(body)) !== null) {
38
+ const args = [];
39
+ if (fm[2]) {
40
+ const argRegex = /(\w+)\s*:\s*(\w+)/g;
41
+ let am;
42
+ while ((am = argRegex.exec(fm[2])) !== null) {
43
+ args.push({ name: am[1], type: { name: am[2], kind: "SCALAR" } });
44
+ }
45
+ }
46
+ fields.push({ name: fm[1], args: args.length ? args : void 0 });
47
+ }
48
+ types.push({ name: typeName, kind: "OBJECT", fields });
49
+ }
50
+ return types;
51
+ }
52
+ async function parseGraphQL(name, config) {
53
+ let types;
54
+ if (config.spec) {
55
+ process.stderr.write(`Fetching ${config.spec}...
56
+ `);
57
+ const res = await fetch(config.spec);
58
+ if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`);
59
+ types = parseSdl(await res.text());
60
+ } else {
61
+ if (!config.url) throw new Error('GraphQL config needs either "spec" or "url"');
62
+ process.stderr.write(`Introspecting ${config.url}...
63
+ `);
64
+ const headers = { ...config.headers };
65
+ const token = config.auth?.env ? process.env[config.auth.env] : void 0;
66
+ if (token) {
67
+ const authType = config.auth?.type || "bearer";
68
+ if (authType === "bearer") headers["Authorization"] = `Bearer ${token}`;
69
+ else if (authType === "api-key") headers[config.auth?.header || "X-API-Key"] = token;
70
+ }
71
+ types = await introspect(config.url, headers);
72
+ }
73
+ const routes = [];
74
+ for (const type of types) {
75
+ if (!type.fields) continue;
76
+ const isQuery = type.name === "Query" || type.name === "RootQuery";
77
+ const isMutation = type.name === "Mutation" || type.name === "RootMutation";
78
+ if (!isQuery && !isMutation) continue;
79
+ const method = isMutation ? "post" : "get";
80
+ for (const field of type.fields) {
81
+ routes.push({
82
+ path: field.name,
83
+ method,
84
+ summary: field.description || "",
85
+ version: "",
86
+ segments: [{ value: field.name, isParam: false }]
87
+ });
88
+ }
89
+ }
90
+ routes.sort((a, b) => a.path.localeCompare(b.path));
91
+ process.stderr.write(`Parsed ${routes.length} fields (${types.length} types)
92
+ `);
93
+ return { name, description: config.description || "", specVersion: "", config, versions: [], resourceDescriptions: {}, routes };
94
+ }
95
+ function validateGraphQLFlags(method, query, body, apiName) {
96
+ if (["put", "patch", "delete", "head"].includes(method)) {
97
+ return `GraphQL only supports POST. ${method.toUpperCase()} is not valid.`;
98
+ }
99
+ if (method === "post") {
100
+ process.stderr.write(`Note: POST is implicit for GraphQL, flag not needed.
101
+ `);
102
+ }
103
+ if (Object.keys(query).length || Object.keys(body).length) {
104
+ return `GraphQL uses document syntax, not key=value params.
105
+ godmode ${apiName} '{ field { subfield } }'`;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // src/config.ts
111
+ import { resolve, basename } from "path";
112
+ import { homedir } from "os";
113
+ import { mkdir, readFile, writeFile, readdir, unlink, access } from "fs/promises";
114
+ import { execSync } from "child_process";
115
+ import { parse as parseYaml2 } from "yaml";
116
+
117
+ // src/protocols/api.ts
118
+ import { parse as parseYaml } from "yaml";
119
+ async function parseOpenApi(name, config) {
120
+ process.stderr.write(`Fetching ${config.spec}...
121
+ `);
122
+ const res = await fetch(config.spec);
123
+ if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
124
+ const text = await res.text();
125
+ const isJson = text.trimStart().startsWith("{");
126
+ const spec = isJson ? JSON.parse(text) : parseYaml(text);
127
+ if (!spec.paths) throw new Error("No paths found - is this a valid OpenAPI document?");
128
+ if (!config.url) {
129
+ if (spec.servers?.[0]?.url) {
130
+ config.url = spec.servers[0].url;
131
+ } else if (spec.host) {
132
+ const scheme = spec.schemes?.[0] || "https";
133
+ config.url = `${scheme}://${spec.host}${spec.basePath || ""}`;
134
+ }
135
+ }
136
+ const versions = resolveVersions(config, Object.keys(spec.paths));
137
+ const httpMethods = ["get", "post", "put", "patch", "delete", "head"];
138
+ const routes = [];
139
+ for (const [path, pathItem] of Object.entries(spec.paths)) {
140
+ for (const method of httpMethods) {
141
+ const op = pathItem[method];
142
+ if (!op) continue;
143
+ const ver = versions.find((v) => path.startsWith(v.prefix));
144
+ const version = ver?.name || "";
145
+ const stripped = ver ? path.slice(ver.prefix.length).replace(/^\//, "") : path.slice(1);
146
+ const rawSegments = stripped.split("/").filter(Boolean);
147
+ const segments = rawSegments.map((s) => {
148
+ const isParam = s.startsWith("{") && s.endsWith("}");
149
+ return { value: isParam ? s.slice(1, -1) : s, isParam };
150
+ });
151
+ const tag = op.tags?.[0] || "";
152
+ routes.push({ path, method, summary: op.summary || "", version, tag, segments });
153
+ }
154
+ }
155
+ routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
156
+ const resourceDescriptions = {};
157
+ if (spec.tags) {
158
+ const stripHtml = (s) => s.replace(/<[^>]+>/g, "").trim();
159
+ for (const t of spec.tags) {
160
+ if (t.name && t.description) {
161
+ const first = stripHtml(t.description).split(/\.\s/)[0];
162
+ resourceDescriptions[t.name] = first.endsWith(".") ? first : first + ".";
163
+ }
164
+ }
165
+ }
166
+ const description = config.description || spec.info?.description || "";
167
+ const specVersion = spec.info?.version || "";
168
+ const versionNames = versions.map((v) => v.name).join(", ");
169
+ process.stderr.write(`Parsed ${routes.length} routes (versions: ${versionNames || "none"})
170
+ `);
171
+ return { name, description, specVersion, config, versions, resourceDescriptions, routes };
172
+ }
173
+ function resolveVersions(config, paths) {
174
+ if (config.versions?.length) {
175
+ return config.versions.map((v) => ({
176
+ prefix: v.prefix.endsWith("/") ? v.prefix.slice(0, -1) : v.prefix,
177
+ name: v.name || v.prefix.replace(/^\//, "")
178
+ }));
179
+ }
180
+ if (config.prefix) {
181
+ const prefix = config.prefix.endsWith("/") ? config.prefix.slice(0, -1) : config.prefix;
182
+ return [{ prefix, name: prefix.replace(/^\//, "") }];
183
+ }
184
+ const versionPattern = /^(\/(?:api\/)?v\d+)/;
185
+ const detected = /* @__PURE__ */ new Map();
186
+ for (const p of paths) {
187
+ const match = p.match(versionPattern);
188
+ if (match) detected.set(match[1], (detected.get(match[1]) || 0) + 1);
189
+ }
190
+ if (detected.size) {
191
+ return [...detected.entries()].sort((a, b) => b[1] - a[1]).map(([prefix]) => ({ prefix, name: prefix.replace(/^\//, "") }));
192
+ }
193
+ return [{ prefix: "", name: "" }];
194
+ }
195
+
196
+ // src/spec.ts
197
+ var parsers = {
198
+ api: parseOpenApi,
199
+ graphql: parseGraphQL,
200
+ mcp: parseMcp
201
+ };
202
+ async function parseSpec(name, config) {
203
+ const parser = parsers[config.type];
204
+ if (!parser) throw new Error(`Unknown type "${config.type}" - supported: ${Object.keys(parsers).join(", ")}`);
205
+ return parser(name, config);
206
+ }
207
+
208
+ // src/config.ts
209
+ var GODMODE_HOME = process.platform === "linux" && process.env.XDG_CONFIG_HOME ? resolve(process.env.XDG_CONFIG_HOME, "godmode") : resolve(homedir(), ".godmode");
210
+ var APIS_DIR = resolve(GODMODE_HOME, "apis");
211
+ async function ensureDirs() {
212
+ await mkdir(APIS_DIR, { recursive: true });
213
+ }
214
+ async function exists(path) {
215
+ try {
216
+ await access(path);
217
+ return true;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ async function loadManifestFromDir(dir) {
223
+ for (const ext of [".yaml", ".yml", ".json"]) {
224
+ const filePath = resolve(dir, `manifest${ext}`);
225
+ if (await exists(filePath)) {
226
+ const text = await readFile(filePath, "utf-8");
227
+ const config = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
228
+ return { config, ext };
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+ async function resolveConfig(input) {
234
+ const asPath = resolve(process.cwd(), input);
235
+ const fromPath = await loadManifestFromDir(asPath);
236
+ if (fromPath) return { name: fromPath.config.slug || basename(asPath), config: fromPath.config, dir: asPath };
237
+ try {
238
+ const pkgDir = resolve(import.meta.dirname, "..", "..", "..", "adapters", input);
239
+ const fromPkg = await loadManifestFromDir(pkgDir);
240
+ if (fromPkg) return { name: fromPkg.config.slug || input, config: fromPkg.config, dir: pkgDir };
241
+ } catch {
242
+ }
243
+ throw new Error(`No config found: ${input}/manifest.yaml or @godmode-cli/${input}`);
244
+ }
245
+ async function addApi(input) {
246
+ await ensureDirs();
247
+ const resolved = await resolveConfig(input).catch(() => null);
248
+ if (resolved) {
249
+ const { name: name2, config } = resolved;
250
+ const type = config.type;
251
+ if (type && ["api", "graphql", "mcp"].includes(type)) {
252
+ if (type === "api" && !config.spec) throw new Error('Config missing "spec" (OpenAPI spec URL or path)');
253
+ if (type === "graphql" && !config.spec && !config.url) throw new Error('GraphQL config needs "spec" (SDL) or "url" (for introspection)');
254
+ if (type === "mcp" && !config.url) throw new Error('MCP config needs "url" (MCP endpoint)');
255
+ if (!config.url && type === "api") process.stderr.write('Warning: no "url" in config - will try to detect from spec\n');
256
+ const manifest = await parseSpec(name2, config);
257
+ await writeFile(resolve(APIS_DIR, `${name2}.json`), JSON.stringify(manifest, null, 2));
258
+ const urlNote = manifest.config.url ? ` at ${manifest.config.url}` : "";
259
+ process.stderr.write(`Registered "${name2}" - ${manifest.routes.length} routes${urlNote}
260
+ `);
261
+ return;
262
+ }
263
+ }
264
+ const name = resolved?.name || input;
265
+ const installTarget = resolved?.dir && await exists(resolve(resolved.dir, "package.json")) ? resolved.dir : input.startsWith("@") ? input : `@godmode-cli/${input}`;
266
+ process.stderr.write(`Installing ${name}...
267
+ `);
268
+ try {
269
+ execSync(`npm install ${installTarget} --prefix ${GODMODE_HOME}`, { stdio: "pipe" });
270
+ } catch (e) {
271
+ throw new Error(`Failed to install ${name}: ${e.stderr?.toString().trim() || e.message}`);
272
+ }
273
+ const pkgName = `@godmode-cli/${name}`;
274
+ const mcpConfigPath = resolve(GODMODE_HOME, "node_modules", pkgName, ".mcp.json");
275
+ if (await exists(mcpConfigPath)) {
276
+ process.stderr.write(`Installed "${name}" (MCP server adapter)
277
+ `);
278
+ } else {
279
+ process.stderr.write(`Installed "${name}"
280
+ `);
281
+ }
282
+ }
283
+ async function updateApi(name) {
284
+ await ensureDirs();
285
+ const manifest = await loadManifestFromDir(name);
286
+ process.stderr.write(`Updating "${name}"...
287
+ `);
288
+ const updated = await parseSpec(name, manifest.config);
289
+ await writeFile(resolve(APIS_DIR, `${name}.json`), JSON.stringify(updated, null, 2));
290
+ process.stderr.write(`Updated "${name}" - ${updated.routes.length} routes
291
+ `);
292
+ }
293
+ async function removeApi(name) {
294
+ try {
295
+ await unlink(resolve(APIS_DIR, `${name}.json`));
296
+ process.stderr.write(`Removed "${name}"
297
+ `);
298
+ } catch {
299
+ process.stderr.write(`API "${name}" not found
300
+ `);
301
+ process.exit(1);
302
+ }
303
+ }
304
+ async function listApis() {
305
+ await ensureDirs();
306
+ const files = await readdir(APIS_DIR);
307
+ const apis = files.filter((f) => f.endsWith(".json"));
308
+ if (!apis.length) {
309
+ console.log("No APIs registered. Create a <name>.yaml config and run: godmode add <name>");
310
+ return;
311
+ }
312
+ for (const file of apis) {
313
+ const m = JSON.parse(await readFile(resolve(APIS_DIR, file), "utf-8"));
314
+ const ver = m.specVersion ? ` v${m.specVersion}` : "";
315
+ const desc = m.description ? ` ${m.description}` : "";
316
+ const url = m.config.url || "(local)";
317
+ console.log(` ${m.name}${ver} ${url} ${m.routes.length} routes${desc}`);
318
+ }
319
+ }
320
+ async function loadManifest(name) {
321
+ await ensureDirs();
322
+ try {
323
+ return JSON.parse(await readFile(resolve(APIS_DIR, `${name}.json`), "utf-8"));
324
+ } catch {
325
+ process.stderr.write(`API "${name}" not found. Create ${name}.yaml and run: godmode add ${name}
326
+ `);
327
+ process.exit(1);
328
+ }
329
+ }
330
+
331
+ export {
332
+ validateGraphQLFlags,
333
+ GODMODE_HOME,
334
+ addApi,
335
+ updateApi,
336
+ removeApi,
337
+ listApis,
338
+ loadManifest
339
+ };
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/request.ts
4
+ async function executeToString(manifest, match, options) {
5
+ const baseUrl = manifest.config.url;
6
+ if (!baseUrl) {
7
+ throw new Error('No base URL configured. Add "url" to your config file.');
8
+ }
9
+ let path = match.route.path;
10
+ for (const [key, value] of Object.entries(match.params)) {
11
+ path = path.replace(`{${key}}`, encodeURIComponent(value));
12
+ }
13
+ const url = new URL(baseUrl.replace(/\/$/, "") + path);
14
+ for (const [key, value] of Object.entries(options.query)) {
15
+ url.searchParams.set(key, value);
16
+ }
17
+ const headers = { ...manifest.config.headers, ...options.headers };
18
+ const authConfig = manifest.config.auth;
19
+ const token = options.token || (authConfig?.env ? process.env[authConfig.env] : void 0);
20
+ if (authConfig?.env && !token && !options.dryRun) {
21
+ throw new Error(`Missing ${authConfig.env}. Set it in .env or environment, or use --token.`);
22
+ }
23
+ if (token) {
24
+ const authType = authConfig?.type || "bearer";
25
+ if (authType === "bearer") {
26
+ headers["Authorization"] = `Bearer ${token}`;
27
+ } else if (authType === "api-key") {
28
+ headers[authConfig?.header || "X-API-Key"] = token;
29
+ } else if (authType === "basic") {
30
+ headers["Authorization"] = `Basic ${token}`;
31
+ }
32
+ }
33
+ if (options.body && !headers["Content-Type"]) {
34
+ headers["Content-Type"] = "application/json";
35
+ }
36
+ const method = match.route.method.toUpperCase();
37
+ if (options.dryRun || options.verbose) {
38
+ process.stderr.write(`${method} ${url.toString()}
39
+ `);
40
+ for (const [k, v] of Object.entries(headers)) {
41
+ process.stderr.write(` ${k}: ${v}
42
+ `);
43
+ }
44
+ if (options.body) {
45
+ const preview = options.body.length > 200 ? options.body.slice(0, 200) + "..." : options.body;
46
+ process.stderr.write(` Body: ${preview}
47
+ `);
48
+ }
49
+ if (options.dryRun) return "";
50
+ process.stderr.write("\n");
51
+ }
52
+ const response = await fetch(url.toString(), {
53
+ method,
54
+ headers,
55
+ body: options.body || void 0
56
+ });
57
+ if (options.verbose) {
58
+ process.stderr.write(`${response.status} ${response.statusText}
59
+ `);
60
+ response.headers.forEach((v, k) => process.stderr.write(` ${k}: ${v}
61
+ `));
62
+ process.stderr.write("\n");
63
+ }
64
+ const text = await response.text();
65
+ const contentType = response.headers.get("content-type") || "";
66
+ let result;
67
+ if (contentType.includes("json")) {
68
+ try {
69
+ result = JSON.stringify(JSON.parse(text), null, 2);
70
+ } catch {
71
+ result = text;
72
+ }
73
+ } else {
74
+ result = text;
75
+ }
76
+ if (!response.ok) throw new Error(result);
77
+ return result;
78
+ }
79
+ async function execute(manifest, match, options) {
80
+ try {
81
+ const result = await executeToString(manifest, match, options);
82
+ if (result) process.stdout.write(result + "\n");
83
+ } catch (err) {
84
+ process.stderr.write(err.message + "\n");
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ export {
90
+ executeToString,
91
+ execute
92
+ };