godmode 0.0.2 → 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/dist/{add-AJERMPEA.js → add-D74TIN3J.js} +12 -11
- package/dist/{chunk-EXWYJU2I.js → chunk-3AQ2CZKC.js} +5 -4
- package/dist/chunk-4EU5JX6C.js +160 -0
- package/dist/{chunk-GJCJO7YT.js → chunk-AWGLXW3B.js} +24 -11
- package/dist/{chunk-2J2VDCQ4.js → chunk-KHFZI7IA.js} +380 -138
- 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 +262 -1540
- package/dist/permissions-E4G66OUZ.js +125 -0
- package/dist/{prompt-SWOWWAOH.js → prompt-YPAZ5433.js} +12 -6
- package/dist/{server-4YVOAJJG.js → server-L3L4TKK3.js} +19 -2
- package/dist/settings-3FF5WWEY.js +17 -0
- package/package.json +10 -9
|
@@ -1,7 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
parseMcp
|
|
4
|
-
} from "./chunk-
|
|
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
|
+
}
|
|
5
115
|
|
|
6
116
|
// ../../interfaces/graphql/src/index.ts
|
|
7
117
|
import { readFile } from "fs/promises";
|
|
@@ -74,9 +184,7 @@ async function parseGraphQL(name, config) {
|
|
|
74
184
|
const headers = { ...config.headers };
|
|
75
185
|
const token = config.auth?.env ? process.env[config.auth.env] : void 0;
|
|
76
186
|
if (token) {
|
|
77
|
-
|
|
78
|
-
if (authType === "bearer") headers["Authorization"] = `Bearer ${token}`;
|
|
79
|
-
else if (authType === "api-key") headers[config.auth?.header || "X-API-Key"] = token;
|
|
187
|
+
AuthStrategy.for(config.auth).apply(headers, token);
|
|
80
188
|
}
|
|
81
189
|
types = await introspect(config.url, headers);
|
|
82
190
|
}
|
|
@@ -117,92 +225,6 @@ function validateGraphQLFlags(method, query, body, apiName) {
|
|
|
117
225
|
return null;
|
|
118
226
|
}
|
|
119
227
|
|
|
120
|
-
// src/config.ts
|
|
121
|
-
import { resolve as resolve2, basename, dirname, extname } from "path";
|
|
122
|
-
import { homedir } from "os";
|
|
123
|
-
import { mkdir, readFile as readFile2, writeFile, readdir, unlink, access, rename } from "fs/promises";
|
|
124
|
-
import { execSync } from "child_process";
|
|
125
|
-
import { parse as parseYaml2 } from "yaml";
|
|
126
|
-
|
|
127
|
-
// ../../interfaces/api/src/index.ts
|
|
128
|
-
import { parse as parseYaml } from "yaml";
|
|
129
|
-
async function parseOpenApi(name, config) {
|
|
130
|
-
process.stderr.write(`Fetching ${config.spec}...
|
|
131
|
-
`);
|
|
132
|
-
const res = await fetch(config.spec);
|
|
133
|
-
if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
134
|
-
const text = await res.text();
|
|
135
|
-
const isJson = text.trimStart().startsWith("{");
|
|
136
|
-
const spec = isJson ? JSON.parse(text) : parseYaml(text);
|
|
137
|
-
if (!spec.paths) throw new Error("No paths found - is this a valid OpenAPI document?");
|
|
138
|
-
if (!config.url) {
|
|
139
|
-
if (spec.servers?.[0]?.url) {
|
|
140
|
-
config.url = spec.servers[0].url;
|
|
141
|
-
} else if (spec.host) {
|
|
142
|
-
const scheme = spec.schemes?.[0] || "https";
|
|
143
|
-
config.url = `${scheme}://${spec.host}${spec.basePath || ""}`;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const versions = resolveVersions(config, Object.keys(spec.paths));
|
|
147
|
-
const httpMethods = ["get", "post", "put", "patch", "delete", "head"];
|
|
148
|
-
const routes = [];
|
|
149
|
-
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
150
|
-
for (const method of httpMethods) {
|
|
151
|
-
const op = pathItem[method];
|
|
152
|
-
if (!op) continue;
|
|
153
|
-
const ver = versions.find((v) => path.startsWith(v.prefix));
|
|
154
|
-
const version = ver?.name || "";
|
|
155
|
-
const stripped = ver ? path.slice(ver.prefix.length).replace(/^\//, "") : path.slice(1);
|
|
156
|
-
const rawSegments = stripped.split("/").filter(Boolean);
|
|
157
|
-
const segments = rawSegments.map((s) => {
|
|
158
|
-
const isParam = s.startsWith("{") && s.endsWith("}");
|
|
159
|
-
return { value: isParam ? s.slice(1, -1) : s, isParam };
|
|
160
|
-
});
|
|
161
|
-
const tag = op.tags?.[0] || "";
|
|
162
|
-
routes.push({ path, method, summary: op.summary || "", version, tag, segments });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
166
|
-
const resourceDescriptions = {};
|
|
167
|
-
if (spec.tags) {
|
|
168
|
-
const stripHtml = (s) => s.replace(/<[^>]+>/g, "").trim();
|
|
169
|
-
for (const t of spec.tags) {
|
|
170
|
-
if (t.name && t.description) {
|
|
171
|
-
const first = stripHtml(t.description).split(/\.\s/)[0];
|
|
172
|
-
resourceDescriptions[t.name] = first.endsWith(".") ? first : first + ".";
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const description = config.description || spec.info?.description || "";
|
|
177
|
-
const specVersion = spec.info?.version || "";
|
|
178
|
-
const versionNames = versions.map((v) => v.name).join(", ");
|
|
179
|
-
process.stderr.write(`Parsed ${routes.length} routes (versions: ${versionNames || "none"})
|
|
180
|
-
`);
|
|
181
|
-
return { name, description, specVersion, config, versions, resourceDescriptions, routes };
|
|
182
|
-
}
|
|
183
|
-
function resolveVersions(config, paths) {
|
|
184
|
-
if (config.versions?.length) {
|
|
185
|
-
return config.versions.map((v) => ({
|
|
186
|
-
prefix: v.prefix.endsWith("/") ? v.prefix.slice(0, -1) : v.prefix,
|
|
187
|
-
name: v.name || v.prefix.replace(/^\//, "")
|
|
188
|
-
}));
|
|
189
|
-
}
|
|
190
|
-
if (config.prefix) {
|
|
191
|
-
const prefix = config.prefix.endsWith("/") ? config.prefix.slice(0, -1) : config.prefix;
|
|
192
|
-
return [{ prefix, name: prefix.replace(/^\//, "") }];
|
|
193
|
-
}
|
|
194
|
-
const versionPattern = /^(\/(?:api\/)?v\d+)/;
|
|
195
|
-
const detected = /* @__PURE__ */ new Map();
|
|
196
|
-
for (const p of paths) {
|
|
197
|
-
const match = p.match(versionPattern);
|
|
198
|
-
if (match) detected.set(match[1], (detected.get(match[1]) || 0) + 1);
|
|
199
|
-
}
|
|
200
|
-
if (detected.size) {
|
|
201
|
-
return [...detected.entries()].sort((a, b) => b[1] - a[1]).map(([prefix]) => ({ prefix, name: prefix.replace(/^\//, "") }));
|
|
202
|
-
}
|
|
203
|
-
return [{ prefix: "", name: "" }];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
228
|
// src/spec.ts
|
|
207
229
|
function projectManifest(multi, iface) {
|
|
208
230
|
const data = multi.interfaces[iface];
|
|
@@ -293,18 +315,95 @@ async function compileInterface(iface, name, source) {
|
|
|
293
315
|
}
|
|
294
316
|
|
|
295
317
|
// src/config.ts
|
|
296
|
-
var GODMODE_HOME =
|
|
297
|
-
var GODMODE_EXTENSIONS_DIR = process.env.GODMODE_EXTENSIONS_DIR ? resolve2(process.env.GODMODE_EXTENSIONS_DIR) : resolve2(GODMODE_HOME, "extensions");
|
|
298
|
-
var LEGACY_APIS_DIR = resolve2(GODMODE_HOME, "apis");
|
|
318
|
+
var GODMODE_HOME = resolve2(homedir(), ".godmode");
|
|
299
319
|
var INTERFACE_KEYS = ["api", "graphql", "mcp"];
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
306
373
|
}
|
|
307
|
-
|
|
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;
|
|
308
407
|
}
|
|
309
408
|
async function exists(path) {
|
|
310
409
|
try {
|
|
@@ -340,7 +439,7 @@ async function loadSourceFromDir(dir) {
|
|
|
340
439
|
if (await exists(filePath)) {
|
|
341
440
|
const text = await readFile2(filePath, "utf-8");
|
|
342
441
|
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
343
|
-
return validateSource(raw, filePath);
|
|
442
|
+
return absolutizeSource(validateSource(raw, filePath), dir);
|
|
344
443
|
}
|
|
345
444
|
}
|
|
346
445
|
return null;
|
|
@@ -351,7 +450,40 @@ async function loadSourceFromFile(filePath) {
|
|
|
351
450
|
if (![".yaml", ".yml", ".json"].includes(ext)) return null;
|
|
352
451
|
const text = await readFile2(filePath, "utf-8");
|
|
353
452
|
const raw = ext === ".json" ? JSON.parse(text) : parseYaml2(text);
|
|
354
|
-
|
|
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;
|
|
355
487
|
}
|
|
356
488
|
async function resolveSource(input) {
|
|
357
489
|
const asPath = resolve2(process.cwd(), input);
|
|
@@ -378,11 +510,12 @@ async function resolveSource(input) {
|
|
|
378
510
|
`No manifest found: ${input} (expected manifest file, folder containing manifest.yaml, or @godmode-cli/${input})`
|
|
379
511
|
);
|
|
380
512
|
}
|
|
381
|
-
async function addApi(input) {
|
|
382
|
-
await
|
|
513
|
+
async function addApi(input, scope = "project") {
|
|
514
|
+
const extensionsDir = await ensureScopeDir(scope);
|
|
383
515
|
const resolved = await resolveSource(input).catch(() => null);
|
|
384
516
|
if (resolved) {
|
|
385
517
|
const { name: name2, source } = resolved;
|
|
518
|
+
assertSlugFree(source.slug || name2, scope);
|
|
386
519
|
const ifaceKeys = Object.keys(source.interfaces).filter(
|
|
387
520
|
(k) => INTERFACE_KEYS.includes(k)
|
|
388
521
|
);
|
|
@@ -391,6 +524,7 @@ async function addApi(input) {
|
|
|
391
524
|
name: source.name,
|
|
392
525
|
slug: source.slug || name2,
|
|
393
526
|
description: source.description || "",
|
|
527
|
+
source: "local",
|
|
394
528
|
auth: source.auth,
|
|
395
529
|
headers: source.headers,
|
|
396
530
|
interfaces: {}
|
|
@@ -399,29 +533,61 @@ async function addApi(input) {
|
|
|
399
533
|
const data = await compileInterface(iface, name2, source);
|
|
400
534
|
multi.interfaces[iface] = data;
|
|
401
535
|
}
|
|
402
|
-
await writeFile(resolve2(
|
|
536
|
+
await writeFile(resolve2(extensionsDir, `${name2}.json`), JSON.stringify(multi, null, 2));
|
|
403
537
|
const ifaceSummary = ifaceKeys.map((k) => {
|
|
404
538
|
const d = multi.interfaces[k];
|
|
405
539
|
return d ? `${k}=${d.routes.length}` : k;
|
|
406
540
|
}).join(", ");
|
|
407
|
-
process.stderr.write(`Registered "${name2}" - interfaces: ${ifaceSummary}
|
|
541
|
+
process.stderr.write(`Registered "${name2}" [${scope}] - interfaces: ${ifaceSummary}
|
|
408
542
|
`);
|
|
409
543
|
return;
|
|
410
544
|
}
|
|
411
545
|
}
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
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}]...
|
|
415
554
|
`);
|
|
416
555
|
try {
|
|
417
|
-
|
|
556
|
+
execFileSync("npm", ["install", installTarget, "--prefix", scopeRoot], { stdio: "pipe" });
|
|
418
557
|
} catch (e) {
|
|
419
558
|
const err = e;
|
|
420
559
|
throw new Error(`Failed to install ${name}: ${err.stderr?.toString().trim() || err.message}`);
|
|
421
560
|
}
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
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"))) {
|
|
425
591
|
process.stderr.write(`Installed "${name}" (MCP server extension)
|
|
426
592
|
`);
|
|
427
593
|
} else {
|
|
@@ -429,45 +595,106 @@ async function addApi(input) {
|
|
|
429
595
|
`);
|
|
430
596
|
}
|
|
431
597
|
}
|
|
432
|
-
async function updateApi(name) {
|
|
433
|
-
|
|
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);
|
|
434
607
|
}
|
|
435
|
-
async function removeApi(name) {
|
|
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;
|
|
436
623
|
try {
|
|
437
|
-
await
|
|
438
|
-
|
|
624
|
+
manifestText = await readFile2(manifestPath, "utf-8");
|
|
625
|
+
} catch {
|
|
626
|
+
process.stderr.write(`Extension "${name}" not found
|
|
439
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);
|
|
440
639
|
} catch {
|
|
441
640
|
process.stderr.write(`Extension "${name}" not found
|
|
442
641
|
`);
|
|
443
642
|
process.exit(1);
|
|
444
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;
|
|
445
654
|
}
|
|
446
655
|
async function listApis() {
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
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>");
|
|
452
679
|
return;
|
|
453
680
|
}
|
|
454
|
-
for (const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
const routeTotal = Object.values(m.interfaces).reduce((n, d) => n + (d?.routes.length ?? 0), 0);
|
|
459
|
-
console.log(` ${m.slug} [${ifaces}] ${routeTotal} routes${desc}`);
|
|
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}`);
|
|
460
685
|
}
|
|
461
686
|
}
|
|
462
687
|
async function loadMultiManifest(name) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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}
|
|
468
696
|
`);
|
|
469
|
-
|
|
470
|
-
}
|
|
697
|
+
process.exit(1);
|
|
471
698
|
}
|
|
472
699
|
async function loadManifest(name, iface) {
|
|
473
700
|
const multi = await loadMultiManifest(name);
|
|
@@ -489,15 +716,30 @@ async function loadManifest(name, iface) {
|
|
|
489
716
|
}
|
|
490
717
|
return projectManifest(multi, target);
|
|
491
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
|
+
}
|
|
492
733
|
|
|
493
734
|
export {
|
|
494
735
|
validateGraphQLFlags,
|
|
736
|
+
BUILTINS,
|
|
495
737
|
GODMODE_HOME,
|
|
496
|
-
GODMODE_EXTENSIONS_DIR,
|
|
497
738
|
addApi,
|
|
498
739
|
updateApi,
|
|
499
740
|
removeApi,
|
|
500
741
|
listApis,
|
|
501
742
|
loadMultiManifest,
|
|
502
|
-
loadManifest
|
|
743
|
+
loadManifest,
|
|
744
|
+
findInstalledManifestSync
|
|
503
745
|
};
|