godmode 0.0.1 → 0.0.3
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/README.md +34 -0
- package/dist/{add-ZRNIX6DM.js → add-D74TIN3J.js} +14 -11
- package/dist/{chunk-EHS56XXY.js → chunk-3AQ2CZKC.js} +6 -5
- package/dist/chunk-4EU5JX6C.js +160 -0
- package/dist/{chunk-I4WS4MMI.js → chunk-AWGLXW3B.js} +27 -14
- package/dist/chunk-KHFZI7IA.js +745 -0
- package/dist/chunk-SMQBMG24.js +102 -0
- package/dist/chunk-XFFBFBN6.js +16 -0
- package/dist/chunk-YDXTIN53.js +212 -0
- package/dist/chunk-ZTPILA7B.js +403 -0
- package/dist/dist-I5CEQ4AC.js +779 -0
- package/dist/ext-K4XEAZGS.js +102 -0
- package/dist/index.js +340 -469
- package/dist/permissions-E4G66OUZ.js +125 -0
- package/dist/{prompt-DCFFOQ7K.js → prompt-YPAZ5433.js} +12 -6
- package/dist/{mcp-server.js → server-L3L4TKK3.js} +20 -3
- package/dist/settings-3FF5WWEY.js +17 -0
- package/package.json +34 -15
- package/dist/chunk-FBMQ3AC4.js +0 -339
- package/dist/mcp-NZIEMQOM.js +0 -58
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseMcp
|
|
4
|
+
} from "./chunk-3AQ2CZKC.js";
|
|
5
|
+
import {
|
|
6
|
+
AuthStrategy
|
|
7
|
+
} from "./chunk-YDXTIN53.js";
|
|
8
|
+
|
|
9
|
+
// src/builtins.ts
|
|
10
|
+
var BUILTINS = /* @__PURE__ */ new Map([
|
|
11
|
+
["ext", {
|
|
12
|
+
description: "Install and manage godmode extensions",
|
|
13
|
+
run: async (rest) => (await import("./ext-K4XEAZGS.js")).runExt(rest)
|
|
14
|
+
}],
|
|
15
|
+
["agent", {
|
|
16
|
+
description: "Coding-agent workflows",
|
|
17
|
+
run: async (rest) => {
|
|
18
|
+
const { runAgentCommand } = await import("./dist-I5CEQ4AC.js");
|
|
19
|
+
const code = await runAgentCommand(rest);
|
|
20
|
+
if (code !== 0) process.exit(code);
|
|
21
|
+
}
|
|
22
|
+
}],
|
|
23
|
+
["permissions", {
|
|
24
|
+
description: "Inspect godmode permission policy",
|
|
25
|
+
run: async (rest) => (await import("./permissions-E4G66OUZ.js")).runPermissions(rest)
|
|
26
|
+
}]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// src/config.ts
|
|
30
|
+
import { resolve as resolve2, basename, dirname, extname, isAbsolute as isAbsolute2, parse as parsePath, relative } from "path";
|
|
31
|
+
import { homedir } from "os";
|
|
32
|
+
import { existsSync, readFileSync } from "fs";
|
|
33
|
+
import { mkdir, readFile as readFile2, writeFile, readdir, unlink, access, rename, rm } from "fs/promises";
|
|
34
|
+
import { execFileSync } from "child_process";
|
|
35
|
+
import { parse as parseYaml2 } from "yaml";
|
|
36
|
+
|
|
37
|
+
// ../../interfaces/api/src/index.ts
|
|
38
|
+
import { parse as parseYaml } from "yaml";
|
|
39
|
+
async function parseOpenApi(name, config) {
|
|
40
|
+
process.stderr.write(`Fetching ${config.spec}...
|
|
41
|
+
`);
|
|
42
|
+
const res = await fetch(config.spec);
|
|
43
|
+
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
const isJson = text.trimStart().startsWith("{");
|
|
46
|
+
const spec = isJson ? JSON.parse(text) : parseYaml(text);
|
|
47
|
+
if (!spec.paths) throw new Error("No paths found - is this a valid OpenAPI document?");
|
|
48
|
+
if (!config.url) {
|
|
49
|
+
if (spec.servers?.[0]?.url) {
|
|
50
|
+
config.url = spec.servers[0].url;
|
|
51
|
+
} else if (spec.host) {
|
|
52
|
+
const scheme = spec.schemes?.[0] || "https";
|
|
53
|
+
config.url = `${scheme}://${spec.host}${spec.basePath || ""}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const versions = resolveVersions(config, Object.keys(spec.paths));
|
|
57
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head"];
|
|
58
|
+
const routes = [];
|
|
59
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
60
|
+
for (const method of httpMethods) {
|
|
61
|
+
const op = pathItem[method];
|
|
62
|
+
if (!op) continue;
|
|
63
|
+
const ver = versions.find((v) => path.startsWith(v.prefix));
|
|
64
|
+
const version = ver?.name || "";
|
|
65
|
+
const stripped = ver ? path.slice(ver.prefix.length).replace(/^\//, "") : path.slice(1);
|
|
66
|
+
const rawSegments = stripped.split("/").filter(Boolean);
|
|
67
|
+
const segments = rawSegments.map((s) => {
|
|
68
|
+
const isParam = s.startsWith("{") && s.endsWith("}");
|
|
69
|
+
return { value: isParam ? s.slice(1, -1) : s, isParam };
|
|
70
|
+
});
|
|
71
|
+
const tag = op.tags?.[0] || "";
|
|
72
|
+
routes.push({ path, method, summary: op.summary || "", version, tag, segments });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
76
|
+
const resourceDescriptions = {};
|
|
77
|
+
if (spec.tags) {
|
|
78
|
+
const stripHtml = (s) => s.replace(/<[^>]+>/g, "").trim();
|
|
79
|
+
for (const t of spec.tags) {
|
|
80
|
+
if (t.name && t.description) {
|
|
81
|
+
const first = stripHtml(t.description).split(/\.\s/)[0];
|
|
82
|
+
resourceDescriptions[t.name] = first.endsWith(".") ? first : first + ".";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const description = config.description || spec.info?.description || "";
|
|
87
|
+
const specVersion = spec.info?.version || "";
|
|
88
|
+
const versionNames = versions.map((v) => v.name).join(", ");
|
|
89
|
+
process.stderr.write(`Parsed ${routes.length} routes (versions: ${versionNames || "none"})
|
|
90
|
+
`);
|
|
91
|
+
return { name, description, specVersion, config, versions, resourceDescriptions, routes };
|
|
92
|
+
}
|
|
93
|
+
function resolveVersions(config, paths) {
|
|
94
|
+
if (config.versions?.length) {
|
|
95
|
+
return config.versions.map((v) => ({
|
|
96
|
+
prefix: v.prefix.endsWith("/") ? v.prefix.slice(0, -1) : v.prefix,
|
|
97
|
+
name: v.name || v.prefix.replace(/^\//, "")
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
if (config.prefix) {
|
|
101
|
+
const prefix = config.prefix.endsWith("/") ? config.prefix.slice(0, -1) : config.prefix;
|
|
102
|
+
return [{ prefix, name: prefix.replace(/^\//, "") }];
|
|
103
|
+
}
|
|
104
|
+
const versionPattern = /^(\/(?:api\/)?v\d+)/;
|
|
105
|
+
const detected = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const p of paths) {
|
|
107
|
+
const match = p.match(versionPattern);
|
|
108
|
+
if (match) detected.set(match[1], (detected.get(match[1]) || 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
if (detected.size) {
|
|
111
|
+
return [...detected.entries()].sort((a, b) => b[1] - a[1]).map(([prefix]) => ({ prefix, name: prefix.replace(/^\//, "") }));
|
|
112
|
+
}
|
|
113
|
+
return [{ prefix: "", name: "" }];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../../interfaces/graphql/src/index.ts
|
|
117
|
+
import { readFile } from "fs/promises";
|
|
118
|
+
import { isAbsolute, resolve } from "path";
|
|
119
|
+
var INTROSPECTION_QUERY = `{
|
|
120
|
+
__schema {
|
|
121
|
+
queryType { name }
|
|
122
|
+
mutationType { name }
|
|
123
|
+
subscriptionType { name }
|
|
124
|
+
types {
|
|
125
|
+
name kind description
|
|
126
|
+
fields { name description args { name type { name kind ofType { name kind } } } }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}`;
|
|
130
|
+
async function introspect(url, headers) {
|
|
131
|
+
const res = await fetch(url, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
134
|
+
body: JSON.stringify({ query: INTROSPECTION_QUERY })
|
|
135
|
+
});
|
|
136
|
+
if (!res.ok) throw new Error(`Introspection failed: ${res.status} ${res.statusText}`);
|
|
137
|
+
const json = await res.json();
|
|
138
|
+
return json.data?.__schema?.types?.filter((t) => !t.name.startsWith("__")) || [];
|
|
139
|
+
}
|
|
140
|
+
function parseSdl(text) {
|
|
141
|
+
const types = [];
|
|
142
|
+
const typeRegex = /type\s+(\w+)\s*\{([^}]*)}/g;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = typeRegex.exec(text)) !== null) {
|
|
145
|
+
const [, typeName, body] = match;
|
|
146
|
+
const fields = [];
|
|
147
|
+
const fieldRegex = /(\w+)(?:\(([^)]*)\))?\s*:\s*\S+/g;
|
|
148
|
+
let fm;
|
|
149
|
+
while ((fm = fieldRegex.exec(body)) !== null) {
|
|
150
|
+
const args = [];
|
|
151
|
+
if (fm[2]) {
|
|
152
|
+
const argRegex = /(\w+)\s*:\s*(\w+)/g;
|
|
153
|
+
let am;
|
|
154
|
+
while ((am = argRegex.exec(fm[2])) !== null) {
|
|
155
|
+
args.push({ name: am[1], type: { name: am[2], kind: "SCALAR" } });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
fields.push({ name: fm[1], args: args.length ? args : void 0 });
|
|
159
|
+
}
|
|
160
|
+
types.push({ name: typeName, kind: "OBJECT", fields });
|
|
161
|
+
}
|
|
162
|
+
return types;
|
|
163
|
+
}
|
|
164
|
+
async function parseGraphQL(name, config) {
|
|
165
|
+
let types;
|
|
166
|
+
if (config.spec) {
|
|
167
|
+
const isRemote = /^https?:\/\//.test(config.spec);
|
|
168
|
+
if (isRemote) {
|
|
169
|
+
process.stderr.write(`Fetching ${config.spec}...
|
|
170
|
+
`);
|
|
171
|
+
const res = await fetch(config.spec);
|
|
172
|
+
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`);
|
|
173
|
+
types = parseSdl(await res.text());
|
|
174
|
+
} else {
|
|
175
|
+
const specPath = isAbsolute(config.spec) ? config.spec : resolve(process.cwd(), config.spec);
|
|
176
|
+
process.stderr.write(`Loading ${specPath}...
|
|
177
|
+
`);
|
|
178
|
+
types = parseSdl(await readFile(specPath, "utf-8"));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
if (!config.url) throw new Error('GraphQL config needs either "spec" or "url"');
|
|
182
|
+
process.stderr.write(`Introspecting ${config.url}...
|
|
183
|
+
`);
|
|
184
|
+
const headers = { ...config.headers };
|
|
185
|
+
const token = config.auth?.env ? process.env[config.auth.env] : void 0;
|
|
186
|
+
if (token) {
|
|
187
|
+
AuthStrategy.for(config.auth).apply(headers, token);
|
|
188
|
+
}
|
|
189
|
+
types = await introspect(config.url, headers);
|
|
190
|
+
}
|
|
191
|
+
const routes = [];
|
|
192
|
+
for (const type of types) {
|
|
193
|
+
if (!type.fields) continue;
|
|
194
|
+
const isQuery = type.name === "Query" || type.name === "RootQuery";
|
|
195
|
+
const isMutation = type.name === "Mutation" || type.name === "RootMutation";
|
|
196
|
+
if (!isQuery && !isMutation) continue;
|
|
197
|
+
const method = isMutation ? "post" : "get";
|
|
198
|
+
for (const field of type.fields) {
|
|
199
|
+
routes.push({
|
|
200
|
+
path: field.name,
|
|
201
|
+
method,
|
|
202
|
+
summary: field.description || "",
|
|
203
|
+
version: "",
|
|
204
|
+
segments: [{ value: field.name, isParam: false }]
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
209
|
+
process.stderr.write(`Parsed ${routes.length} fields (${types.length} types)
|
|
210
|
+
`);
|
|
211
|
+
return { name, description: config.description || "", specVersion: "", config, versions: [], resourceDescriptions: {}, routes };
|
|
212
|
+
}
|
|
213
|
+
function validateGraphQLFlags(method, query, body, apiName) {
|
|
214
|
+
if (["put", "patch", "delete", "head"].includes(method)) {
|
|
215
|
+
return `GraphQL only supports POST. ${method.toUpperCase()} is not valid.`;
|
|
216
|
+
}
|
|
217
|
+
if (method === "post") {
|
|
218
|
+
process.stderr.write(`Note: POST is implicit for GraphQL, flag not needed.
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
if (Object.keys(query).length || Object.keys(body).length) {
|
|
222
|
+
return `GraphQL uses document syntax, not key=value params.
|
|
223
|
+
godmode ${apiName} '{ field { subfield } }'`;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/spec.ts
|
|
229
|
+
function projectManifest(multi, iface) {
|
|
230
|
+
const data = multi.interfaces[iface];
|
|
231
|
+
if (!data) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`extension '${multi.slug}' does not declare an '${iface}' interface (declared: ${Object.keys(multi.interfaces).join(", ")})`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const config = {
|
|
237
|
+
slug: multi.slug,
|
|
238
|
+
name: multi.name,
|
|
239
|
+
description: multi.description,
|
|
240
|
+
type: iface,
|
|
241
|
+
auth: multi.auth,
|
|
242
|
+
headers: multi.headers,
|
|
243
|
+
..."spec" in data ? { spec: data.spec } : {},
|
|
244
|
+
..."url" in data && data.url ? { url: data.url } : {},
|
|
245
|
+
..."prefix" in data && data.prefix ? { prefix: data.prefix } : {},
|
|
246
|
+
versions: data.versions,
|
|
247
|
+
...iface === "mcp" && data._mcpTools ? { _mcpTools: data._mcpTools } : {}
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
name: multi.name,
|
|
251
|
+
description: multi.description,
|
|
252
|
+
specVersion: data.specVersion,
|
|
253
|
+
config,
|
|
254
|
+
versions: data.versions,
|
|
255
|
+
resourceDescriptions: data.resourceDescriptions,
|
|
256
|
+
routes: data.routes
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
var parsers = {
|
|
260
|
+
api: parseOpenApi,
|
|
261
|
+
graphql: parseGraphQL,
|
|
262
|
+
mcp: parseMcp
|
|
263
|
+
};
|
|
264
|
+
async function compileInterface(iface, name, source) {
|
|
265
|
+
const parser = parsers[iface];
|
|
266
|
+
if (!parser) throw new Error(`Unknown interface '${iface}'`);
|
|
267
|
+
const ifaceSource = source.interfaces[iface];
|
|
268
|
+
if (!ifaceSource) throw new Error(`Interface '${iface}' not declared on '${name}'`);
|
|
269
|
+
const legacyConfig = {
|
|
270
|
+
slug: source.slug || name,
|
|
271
|
+
name: source.name,
|
|
272
|
+
description: source.description,
|
|
273
|
+
type: iface,
|
|
274
|
+
auth: source.auth,
|
|
275
|
+
headers: source.headers,
|
|
276
|
+
..."spec" in ifaceSource && ifaceSource.spec ? { spec: ifaceSource.spec } : {},
|
|
277
|
+
..."url" in ifaceSource && ifaceSource.url ? { url: ifaceSource.url } : {},
|
|
278
|
+
..."prefix" in ifaceSource && ifaceSource.prefix ? { prefix: ifaceSource.prefix } : {},
|
|
279
|
+
versions: ifaceSource.versions
|
|
280
|
+
};
|
|
281
|
+
const flat = await parser(name, legacyConfig);
|
|
282
|
+
const base = {
|
|
283
|
+
type: iface,
|
|
284
|
+
specVersion: flat.specVersion,
|
|
285
|
+
versions: flat.versions,
|
|
286
|
+
resourceDescriptions: flat.resourceDescriptions,
|
|
287
|
+
routes: flat.routes
|
|
288
|
+
};
|
|
289
|
+
if (iface === "api") {
|
|
290
|
+
const s2 = ifaceSource;
|
|
291
|
+
return {
|
|
292
|
+
...base,
|
|
293
|
+
type: "api",
|
|
294
|
+
spec: s2.spec,
|
|
295
|
+
url: flat.config.url,
|
|
296
|
+
prefix: s2.prefix
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (iface === "graphql") {
|
|
300
|
+
const s2 = ifaceSource;
|
|
301
|
+
return {
|
|
302
|
+
...base,
|
|
303
|
+
type: "graphql",
|
|
304
|
+
spec: s2.spec,
|
|
305
|
+
url: flat.config.url
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const s = ifaceSource;
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
type: "mcp",
|
|
312
|
+
url: s.url,
|
|
313
|
+
_mcpTools: flat.config._mcpTools
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/config.ts
|
|
318
|
+
var GODMODE_HOME = resolve2(homedir(), ".godmode");
|
|
319
|
+
var INTERFACE_KEYS = ["api", "graphql", "mcp"];
|
|
320
|
+
function assertSlugFree(slug, scope, packageName) {
|
|
321
|
+
if (BUILTINS.has(slug)) {
|
|
322
|
+
throw new Error(`'${slug}' is a built-in godmode extension; its slug is always in use.`);
|
|
323
|
+
}
|
|
324
|
+
const dir = scopeExtensionsDirSync(scope);
|
|
325
|
+
if (!dir) return;
|
|
326
|
+
const manifestPath = resolve2(dir, `${slug}.json`);
|
|
327
|
+
if (!existsSync(manifestPath)) return;
|
|
328
|
+
let existing;
|
|
329
|
+
try {
|
|
330
|
+
existing = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (existing.packageName === packageName) return;
|
|
335
|
+
throw new Error(
|
|
336
|
+
`'${slug}' is already in use by ${existing.packageName || "an installed extension"}.
|
|
337
|
+
Run 'godmode ext uninstall ${slug}' first.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function isValidNpmPackageName(packageName) {
|
|
341
|
+
return /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(packageName);
|
|
342
|
+
}
|
|
343
|
+
function assertContained(parent, child, label) {
|
|
344
|
+
const root = resolve2(parent);
|
|
345
|
+
const target = resolve2(child);
|
|
346
|
+
const rel = relative(root, target);
|
|
347
|
+
if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) return target;
|
|
348
|
+
throw new Error(`${label} resolves outside ${root}`);
|
|
349
|
+
}
|
|
350
|
+
function packageInstallDir(scopeRoot, packageName) {
|
|
351
|
+
if (!isValidNpmPackageName(packageName)) {
|
|
352
|
+
throw new Error(`Invalid npm package name in installed manifest: ${packageName}`);
|
|
353
|
+
}
|
|
354
|
+
return assertContained(
|
|
355
|
+
resolve2(scopeRoot, "node_modules"),
|
|
356
|
+
resolve2(scopeRoot, "node_modules", packageName),
|
|
357
|
+
`Package path for ${packageName}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
var GITIGNORE_CONTENT = `# godmode \u2014 runtime artifacts, do not commit
|
|
361
|
+
node_modules/
|
|
362
|
+
coding-agents/
|
|
363
|
+
package-lock.json
|
|
364
|
+
`;
|
|
365
|
+
function findProjectGodmode(start = process.cwd()) {
|
|
366
|
+
let dir = resolve2(start);
|
|
367
|
+
const root = parsePath(dir).root;
|
|
368
|
+
while (true) {
|
|
369
|
+
const candidate = resolve2(dir, ".godmode");
|
|
370
|
+
if (existsSync(candidate)) return candidate;
|
|
371
|
+
if (dir === root) return null;
|
|
372
|
+
dir = dirname(dir);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function ensureScopeDir(scope) {
|
|
376
|
+
if (scope === "global") {
|
|
377
|
+
await mkdir(GODMODE_HOME, { recursive: true });
|
|
378
|
+
const legacy = resolve2(GODMODE_HOME, "apis");
|
|
379
|
+
const target2 = resolve2(GODMODE_HOME, "extensions");
|
|
380
|
+
if (await exists(legacy) && !await exists(target2)) {
|
|
381
|
+
await rename(legacy, target2);
|
|
382
|
+
process.stderr.write(`Migrated ${legacy} -> ${target2}
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
await mkdir(target2, { recursive: true });
|
|
386
|
+
return target2;
|
|
387
|
+
}
|
|
388
|
+
let projectRoot = findProjectGodmode();
|
|
389
|
+
if (!projectRoot) {
|
|
390
|
+
projectRoot = resolve2(process.cwd(), ".godmode");
|
|
391
|
+
await mkdir(projectRoot, { recursive: true });
|
|
392
|
+
await writeFile(resolve2(projectRoot, ".gitignore"), GITIGNORE_CONTENT);
|
|
393
|
+
}
|
|
394
|
+
const target = resolve2(projectRoot, "extensions");
|
|
395
|
+
await mkdir(target, { recursive: true });
|
|
396
|
+
return target;
|
|
397
|
+
}
|
|
398
|
+
function scopeExtensionsDirSync(scope) {
|
|
399
|
+
if (scope === "global") {
|
|
400
|
+
const d2 = resolve2(GODMODE_HOME, "extensions");
|
|
401
|
+
return existsSync(d2) ? d2 : null;
|
|
402
|
+
}
|
|
403
|
+
const project = findProjectGodmode();
|
|
404
|
+
if (!project) return null;
|
|
405
|
+
const d = resolve2(project, "extensions");
|
|
406
|
+
return existsSync(d) ? d : null;
|
|
407
|
+
}
|
|
408
|
+
async function exists(path) {
|
|
409
|
+
try {
|
|
410
|
+
await access(path);
|
|
411
|
+
return true;
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function validateSource(raw, origin) {
|
|
417
|
+
if (!raw || typeof raw !== "object") {
|
|
418
|
+
throw new Error(`${origin}: not a valid object`);
|
|
419
|
+
}
|
|
420
|
+
const m = raw;
|
|
421
|
+
if (!m.name || typeof m.name !== "string") {
|
|
422
|
+
throw new Error(`${origin}: missing or invalid 'name'`);
|
|
423
|
+
}
|
|
424
|
+
if (!m.interfaces || typeof m.interfaces !== "object" || Object.keys(m.interfaces).length === 0) {
|
|
425
|
+
throw new Error(`${origin}: 'interfaces' must be an object with at least one key`);
|
|
426
|
+
}
|
|
427
|
+
for (const key of Object.keys(m.interfaces)) {
|
|
428
|
+
if (!INTERFACE_KEYS.includes(key)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`${origin}: unknown interface '${key}' (valid: ${INTERFACE_KEYS.join(", ")})`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return m;
|
|
435
|
+
}
|
|
436
|
+
async function loadSourceFromDir(dir) {
|
|
437
|
+
for (const ext of [".yaml", ".yml", ".json"]) {
|
|
438
|
+
const filePath = resolve2(dir, `manifest${ext}`);
|
|
439
|
+
if (await exists(filePath)) {
|
|
440
|
+
const text = await readFile2(filePath, "utf-8");
|
|
441
|
+
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
442
|
+
return absolutizeSource(validateSource(raw, filePath), dir);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
async function loadSourceFromFile(filePath) {
|
|
448
|
+
if (!await exists(filePath)) return null;
|
|
449
|
+
const ext = extname(filePath);
|
|
450
|
+
if (![".yaml", ".yml", ".json"].includes(ext)) return null;
|
|
451
|
+
const text = await readFile2(filePath, "utf-8");
|
|
452
|
+
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
453
|
+
const dir = dirname(filePath);
|
|
454
|
+
return { source: absolutizeSource(validateSource(raw, filePath), dir), dir };
|
|
455
|
+
}
|
|
456
|
+
function absolutizeSource(source, dir) {
|
|
457
|
+
const interfaces = {};
|
|
458
|
+
if (source.interfaces.api) interfaces.api = { ...source.interfaces.api };
|
|
459
|
+
if (source.interfaces.graphql) interfaces.graphql = { ...source.interfaces.graphql };
|
|
460
|
+
if (source.interfaces.mcp) interfaces.mcp = { ...source.interfaces.mcp };
|
|
461
|
+
const next = {
|
|
462
|
+
...source,
|
|
463
|
+
interfaces
|
|
464
|
+
};
|
|
465
|
+
for (const iface of ["api", "graphql"]) {
|
|
466
|
+
const spec = next.interfaces[iface]?.spec;
|
|
467
|
+
if (spec && !/^[a-z][a-z0-9+.-]*:/i.test(spec) && !isAbsolute2(spec)) {
|
|
468
|
+
next.interfaces[iface].spec = resolve2(dir, spec);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return next;
|
|
472
|
+
}
|
|
473
|
+
async function loadSourceFromPackageDir(pkgDir) {
|
|
474
|
+
const pkgPath = resolve2(pkgDir, "package.json");
|
|
475
|
+
if (!await exists(pkgPath)) return null;
|
|
476
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
477
|
+
const manifestExport = pkg.exports?.["./manifest"];
|
|
478
|
+
const manifestPath = typeof manifestExport === "string" ? manifestExport : manifestExport?.default;
|
|
479
|
+
if (!manifestPath) {
|
|
480
|
+
throw new Error(`${pkgPath}: missing exports["./manifest"]`);
|
|
481
|
+
}
|
|
482
|
+
const loaded = await loadSourceFromFile(assertContained(pkgDir, resolve2(pkgDir, manifestPath), "Manifest export"));
|
|
483
|
+
if (!loaded) {
|
|
484
|
+
throw new Error(`${pkgPath}: exports["./manifest"] does not point to a readable manifest`);
|
|
485
|
+
}
|
|
486
|
+
return loaded.source;
|
|
487
|
+
}
|
|
488
|
+
async function resolveSource(input) {
|
|
489
|
+
const asPath = resolve2(process.cwd(), input);
|
|
490
|
+
const fromFile = await loadSourceFromFile(asPath);
|
|
491
|
+
if (fromFile) {
|
|
492
|
+
const name = fromFile.source.slug || fromFile.source.name || basename(fromFile.dir);
|
|
493
|
+
return { name, source: fromFile.source, dir: fromFile.dir };
|
|
494
|
+
}
|
|
495
|
+
const fromDir = await loadSourceFromDir(asPath);
|
|
496
|
+
if (fromDir) {
|
|
497
|
+
const name = fromDir.slug || fromDir.name || basename(asPath);
|
|
498
|
+
return { name, source: fromDir, dir: asPath };
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const pkgDir = resolve2(import.meta.dirname, "..", "..", "..", "extensions", input);
|
|
502
|
+
const fromPkg = await loadSourceFromDir(pkgDir);
|
|
503
|
+
if (fromPkg) {
|
|
504
|
+
const name = fromPkg.slug || fromPkg.name || input;
|
|
505
|
+
return { name, source: fromPkg, dir: pkgDir };
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
throw new Error(
|
|
510
|
+
`No manifest found: ${input} (expected manifest file, folder containing manifest.yaml, or @godmode-cli/${input})`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
async function addApi(input, scope = "project") {
|
|
514
|
+
const extensionsDir = await ensureScopeDir(scope);
|
|
515
|
+
const resolved = await resolveSource(input).catch(() => null);
|
|
516
|
+
if (resolved) {
|
|
517
|
+
const { name: name2, source } = resolved;
|
|
518
|
+
assertSlugFree(source.slug || name2, scope);
|
|
519
|
+
const ifaceKeys = Object.keys(source.interfaces).filter(
|
|
520
|
+
(k) => INTERFACE_KEYS.includes(k)
|
|
521
|
+
);
|
|
522
|
+
if (ifaceKeys.length > 0) {
|
|
523
|
+
const multi = {
|
|
524
|
+
name: source.name,
|
|
525
|
+
slug: source.slug || name2,
|
|
526
|
+
description: source.description || "",
|
|
527
|
+
source: "local",
|
|
528
|
+
auth: source.auth,
|
|
529
|
+
headers: source.headers,
|
|
530
|
+
interfaces: {}
|
|
531
|
+
};
|
|
532
|
+
for (const iface of ifaceKeys) {
|
|
533
|
+
const data = await compileInterface(iface, name2, source);
|
|
534
|
+
multi.interfaces[iface] = data;
|
|
535
|
+
}
|
|
536
|
+
await writeFile(resolve2(extensionsDir, `${name2}.json`), JSON.stringify(multi, null, 2));
|
|
537
|
+
const ifaceSummary = ifaceKeys.map((k) => {
|
|
538
|
+
const d = multi.interfaces[k];
|
|
539
|
+
return d ? `${k}=${d.routes.length}` : k;
|
|
540
|
+
}).join(", ");
|
|
541
|
+
process.stderr.write(`Registered "${name2}" [${scope}] - interfaces: ${ifaceSummary}
|
|
542
|
+
`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const scopeRoot = scope === "global" ? GODMODE_HOME : dirname(extensionsDir);
|
|
547
|
+
const asPath = resolve2(process.cwd(), input);
|
|
548
|
+
const inputPackageJson = await exists(resolve2(asPath, "package.json")) ? JSON.parse(await readFile2(resolve2(asPath, "package.json"), "utf-8")) : null;
|
|
549
|
+
const packageName = inputPackageJson?.name || (input.startsWith("@") ? input : `@godmode-cli/${input}`);
|
|
550
|
+
const name = resolved?.name || (input.startsWith("@") ? basename(input) : input);
|
|
551
|
+
assertSlugFree(name, scope, packageName);
|
|
552
|
+
const installTarget = resolved?.dir && await exists(resolve2(resolved.dir, "package.json")) ? resolved.dir : inputPackageJson ? asPath : packageName;
|
|
553
|
+
process.stderr.write(`Installing ${name} [${scope}]...
|
|
554
|
+
`);
|
|
555
|
+
try {
|
|
556
|
+
execFileSync("npm", ["install", installTarget, "--prefix", scopeRoot], { stdio: "pipe" });
|
|
557
|
+
} catch (e) {
|
|
558
|
+
const err = e;
|
|
559
|
+
throw new Error(`Failed to install ${name}: ${err.stderr?.toString().trim() || err.message}`);
|
|
560
|
+
}
|
|
561
|
+
const pkgDir = packageInstallDir(scopeRoot, packageName);
|
|
562
|
+
const packageSource = await loadSourceFromPackageDir(pkgDir).catch((error) => {
|
|
563
|
+
const mcpConfigPath = resolve2(pkgDir, ".mcp.json");
|
|
564
|
+
if (existsSync(mcpConfigPath)) return null;
|
|
565
|
+
throw error;
|
|
566
|
+
});
|
|
567
|
+
if (packageSource) {
|
|
568
|
+
const slug = packageSource.slug || name;
|
|
569
|
+
if (slug !== name) assertSlugFree(slug, scope, packageName);
|
|
570
|
+
const ifaceKeys = Object.keys(packageSource.interfaces).filter(
|
|
571
|
+
(k) => INTERFACE_KEYS.includes(k)
|
|
572
|
+
);
|
|
573
|
+
const multi = {
|
|
574
|
+
name: packageSource.name,
|
|
575
|
+
slug,
|
|
576
|
+
description: packageSource.description || "",
|
|
577
|
+
source: "npm",
|
|
578
|
+
packageName,
|
|
579
|
+
auth: packageSource.auth,
|
|
580
|
+
headers: packageSource.headers,
|
|
581
|
+
interfaces: {}
|
|
582
|
+
};
|
|
583
|
+
for (const iface of ifaceKeys) {
|
|
584
|
+
const data = await compileInterface(iface, slug, packageSource);
|
|
585
|
+
multi.interfaces[iface] = data;
|
|
586
|
+
}
|
|
587
|
+
await writeFile(resolve2(extensionsDir, `${slug}.json`), JSON.stringify(multi, null, 2));
|
|
588
|
+
process.stderr.write(`Registered "${slug}" [${scope}] from ${packageName}
|
|
589
|
+
`);
|
|
590
|
+
} else if (await exists(resolve2(pkgDir, ".mcp.json"))) {
|
|
591
|
+
process.stderr.write(`Installed "${name}" (MCP server extension)
|
|
592
|
+
`);
|
|
593
|
+
} else {
|
|
594
|
+
process.stderr.write(`Installed "${name}"
|
|
595
|
+
`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function updateApi(name, scope) {
|
|
599
|
+
const resolvedScope = scope ?? findInstalledScope(name);
|
|
600
|
+
if (!resolvedScope) {
|
|
601
|
+
process.stderr.write(`Extension "${name}" not found
|
|
602
|
+
`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
const existing = findInstalledManifestSync(name);
|
|
606
|
+
await addApi(existing?.packageName ?? name, resolvedScope);
|
|
607
|
+
}
|
|
608
|
+
async function removeApi(name, scope) {
|
|
609
|
+
const resolvedScope = scope ?? findInstalledScope(name);
|
|
610
|
+
if (!resolvedScope) {
|
|
611
|
+
process.stderr.write(`Extension "${name}" not found
|
|
612
|
+
`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const dir = scopeExtensionsDirSync(resolvedScope);
|
|
616
|
+
if (!dir) {
|
|
617
|
+
process.stderr.write(`Extension "${name}" not found
|
|
618
|
+
`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
const manifestPath = resolve2(dir, `${name}.json`);
|
|
622
|
+
let manifestText;
|
|
623
|
+
try {
|
|
624
|
+
manifestText = await readFile2(manifestPath, "utf-8");
|
|
625
|
+
} catch {
|
|
626
|
+
process.stderr.write(`Extension "${name}" not found
|
|
627
|
+
`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
let packageName;
|
|
631
|
+
try {
|
|
632
|
+
const multi = JSON.parse(manifestText);
|
|
633
|
+
packageName = multi.packageName;
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
const packageDir = packageName ? packageInstallDir(resolvedScope === "global" ? GODMODE_HOME : dirname(dir), packageName) : null;
|
|
637
|
+
try {
|
|
638
|
+
await unlink(manifestPath);
|
|
639
|
+
} catch {
|
|
640
|
+
process.stderr.write(`Extension "${name}" not found
|
|
641
|
+
`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
if (packageDir) await rm(packageDir, { recursive: true, force: true });
|
|
645
|
+
process.stderr.write(`Removed "${name}" [${resolvedScope}]
|
|
646
|
+
`);
|
|
647
|
+
}
|
|
648
|
+
function findInstalledScope(name) {
|
|
649
|
+
for (const scope of ["project", "global"]) {
|
|
650
|
+
const dir = scopeExtensionsDirSync(scope);
|
|
651
|
+
if (dir && existsSync(resolve2(dir, `${name}.json`))) return scope;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
async function listApis() {
|
|
656
|
+
const rows = [];
|
|
657
|
+
const seen = /* @__PURE__ */ new Set();
|
|
658
|
+
for (const scope of ["project", "global"]) {
|
|
659
|
+
const dir = scopeExtensionsDirSync(scope);
|
|
660
|
+
if (!dir) continue;
|
|
661
|
+
const files = (await readdir(dir)).filter((f) => f.endsWith(".json"));
|
|
662
|
+
for (const file of files) {
|
|
663
|
+
const m = JSON.parse(await readFile2(resolve2(dir, file), "utf-8"));
|
|
664
|
+
const shadowed = scope === "global" && seen.has(m.slug);
|
|
665
|
+
seen.add(m.slug);
|
|
666
|
+
rows.push({
|
|
667
|
+
scope,
|
|
668
|
+
source: m.source || "local",
|
|
669
|
+
slug: m.slug,
|
|
670
|
+
ifaces: Object.keys(m.interfaces).join(", "),
|
|
671
|
+
routeTotal: Object.values(m.interfaces).reduce((n, d) => n + (d?.routes.length ?? 0), 0),
|
|
672
|
+
description: m.description || "",
|
|
673
|
+
shadowed
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!rows.length) {
|
|
678
|
+
console.log("No extensions installed. Run: godmode ext install <name>");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
for (const r of rows) {
|
|
682
|
+
const tag = `[${r.scope}${r.shadowed ? ", shadowed" : ""}]`;
|
|
683
|
+
const desc = r.description ? ` ${r.description}` : "";
|
|
684
|
+
console.log(` ${r.slug} ${tag} ${r.source} [${r.ifaces}] ${r.routeTotal} routes${desc}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function loadMultiManifest(name) {
|
|
688
|
+
for (const scope of ["project", "global"]) {
|
|
689
|
+
const dir = scopeExtensionsDirSync(scope);
|
|
690
|
+
if (!dir) continue;
|
|
691
|
+
const path = resolve2(dir, `${name}.json`);
|
|
692
|
+
if (!existsSync(path)) continue;
|
|
693
|
+
return JSON.parse(await readFile2(path, "utf-8"));
|
|
694
|
+
}
|
|
695
|
+
process.stderr.write(`Extension "${name}" not found. Run: godmode ext install ${name}
|
|
696
|
+
`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
async function loadManifest(name, iface) {
|
|
700
|
+
const multi = await loadMultiManifest(name);
|
|
701
|
+
const declared = Object.keys(multi.interfaces);
|
|
702
|
+
if (declared.length === 0) {
|
|
703
|
+
process.stderr.write(`Extension "${name}" has no interfaces declared
|
|
704
|
+
`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
const target = iface ?? declared[0];
|
|
708
|
+
if (!multi.interfaces[target]) {
|
|
709
|
+
process.stderr.write(
|
|
710
|
+
`Extension "${name}" does not declare a '${target}' interface (declared: ${declared.join(", ")}).
|
|
711
|
+
`
|
|
712
|
+
);
|
|
713
|
+
process.stderr.write(`Try: godmode ${declared[0]} ${name} --help
|
|
714
|
+
`);
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
return projectManifest(multi, target);
|
|
718
|
+
}
|
|
719
|
+
function findInstalledManifestSync(name) {
|
|
720
|
+
for (const scope of ["project", "global"]) {
|
|
721
|
+
const dir = scopeExtensionsDirSync(scope);
|
|
722
|
+
if (!dir) continue;
|
|
723
|
+
const path = resolve2(dir, `${name}.json`);
|
|
724
|
+
if (!existsSync(path)) continue;
|
|
725
|
+
try {
|
|
726
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
727
|
+
} catch {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export {
|
|
735
|
+
validateGraphQLFlags,
|
|
736
|
+
BUILTINS,
|
|
737
|
+
GODMODE_HOME,
|
|
738
|
+
addApi,
|
|
739
|
+
updateApi,
|
|
740
|
+
removeApi,
|
|
741
|
+
listApis,
|
|
742
|
+
loadMultiManifest,
|
|
743
|
+
loadManifest,
|
|
744
|
+
findInstalledManifestSync
|
|
745
|
+
};
|