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.
@@ -1,7 +1,117 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  parseMcp
4
- } from "./chunk-EXWYJU2I.js";
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
- const authType = config.auth?.type || "bearer";
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 = process.platform === "linux" && process.env.XDG_CONFIG_HOME ? resolve2(process.env.XDG_CONFIG_HOME, "godmode") : resolve2(homedir(), ".godmode");
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
- async function ensureDirs() {
301
- if (await exists(LEGACY_APIS_DIR) && !await exists(GODMODE_EXTENSIONS_DIR)) {
302
- await mkdir(dirname(GODMODE_EXTENSIONS_DIR), { recursive: true });
303
- await rename(LEGACY_APIS_DIR, GODMODE_EXTENSIONS_DIR);
304
- process.stderr.write(`Migrated ${LEGACY_APIS_DIR} -> ${GODMODE_EXTENSIONS_DIR}
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
- await mkdir(GODMODE_EXTENSIONS_DIR, { recursive: true });
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
- return { source: validateSource(raw, filePath), dir: dirname(filePath) };
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 ensureDirs();
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(GODMODE_EXTENSIONS_DIR, `${name2}.json`), JSON.stringify(multi, null, 2));
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 name = resolved?.name || input;
413
- const installTarget = resolved?.dir && await exists(resolve2(resolved.dir, "package.json")) ? resolved.dir : input.startsWith("@") ? input : `@godmode-cli/${input}`;
414
- process.stderr.write(`Installing ${name}...
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
- execSync(`npm install ${installTarget} --prefix ${GODMODE_HOME}`, { stdio: "pipe" });
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 pkgName = `@godmode-cli/${name}`;
423
- const mcpConfigPath = resolve2(GODMODE_HOME, "node_modules", pkgName, ".mcp.json");
424
- if (await exists(mcpConfigPath)) {
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
- await addApi(name);
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 unlink(resolve2(GODMODE_EXTENSIONS_DIR, `${name}.json`));
438
- process.stderr.write(`Removed "${name}"
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
- await ensureDirs();
448
- const files = await readdir(GODMODE_EXTENSIONS_DIR);
449
- const apis = files.filter((f) => f.endsWith(".json"));
450
- if (!apis.length) {
451
- console.log("No extensions registered. Create a manifest.yaml and run: godmode extension add <name>");
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 file of apis) {
455
- const m = JSON.parse(await readFile2(resolve2(GODMODE_EXTENSIONS_DIR, file), "utf-8"));
456
- const ifaces = Object.keys(m.interfaces).join(", ");
457
- const desc = m.description ? ` ${m.description}` : "";
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
- await ensureDirs();
464
- try {
465
- return JSON.parse(await readFile2(resolve2(GODMODE_EXTENSIONS_DIR, `${name}.json`), "utf-8"));
466
- } catch {
467
- process.stderr.write(`Extension "${name}" not found. Create ${name}.yaml and run: godmode extension add ${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}
468
696
  `);
469
- process.exit(1);
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
  };