omnigateway 0.3.0 → 0.4.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.
Files changed (72) hide show
  1. package/README.md +175 -1
  2. package/bin/omni.js +1155 -40
  3. package/gateway.js +985 -89
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DbKk1ExR.js → Chip-BltJSw8A.js} +5 -5
  6. package/public/assets/Confirm-gd8Y6snu.js +4 -0
  7. package/public/assets/CopyValue-Dds-Ow9a.js +27 -0
  8. package/public/assets/{Field-B7GOsJf8.js → Field-D-14HF0x.js} +11 -11
  9. package/public/assets/Lamp-BDf7-dMa.js +25 -0
  10. package/public/assets/Match-CgNxphvS.js +1 -0
  11. package/public/assets/Meter-stpt3vQu.js +14 -0
  12. package/public/assets/Modal-Dj3LvMS9.js +82 -0
  13. package/public/assets/Rack-B4c0dbIb.js +180 -0
  14. package/public/assets/{Readout--CQnPjoO.js → Readout-Du15WYx0.js} +5 -5
  15. package/public/assets/{States-CQFkw1UZ.js → States-CxLDlCaw.js} +5 -5
  16. package/public/assets/{Table-Ba58Wg4-.js → Table-CVDZ69Zx.js} +1 -1
  17. package/public/assets/Toggle-DMk46RX2.js +39 -0
  18. package/public/assets/TokenBreakdown-BySwFK6Q.js +31 -0
  19. package/public/assets/_app-Dx_xnlud.js +1 -0
  20. package/public/assets/_app.accounts-DvBYnbfS.js +64 -0
  21. package/public/assets/_app.console-CoaRdkB1.js +44 -0
  22. package/public/assets/_app.database-CES5ec5g.js +39 -0
  23. package/public/assets/_app.index-DwOZe1A-.js +62 -0
  24. package/public/assets/_app.keys-B5HrAu2U.js +76 -0
  25. package/public/assets/_app.logs-NbfIexz0.js +68 -0
  26. package/public/assets/_app.models-B1MdFRtQ.js +148 -0
  27. package/public/assets/_app.plugins._pluginId-ByMziW8p.js +20 -0
  28. package/public/assets/_app.settings-BkDkGkKF.js +46 -0
  29. package/public/assets/_app.usage-DNpM8Nub.js +83 -0
  30. package/public/assets/dist-9ps0Qd15.js +1 -0
  31. package/public/assets/index-gMv8Qs7m.js +178 -0
  32. package/public/assets/login-cCXnl34r.js +36 -0
  33. package/public/assets/matchContext-BOenO3J2.js +1 -0
  34. package/public/assets/plus-fXfUTt6_.js +1 -0
  35. package/public/assets/queries-CIOPbRcE.js +142 -0
  36. package/public/assets/shared-B4o-2quR.js +84 -0
  37. package/public/assets/{trash-2-DTWWI3vA.js → trash-2-ZPh3619D.js} +1 -1
  38. package/public/index.html +7 -2
  39. package/public/shared/chunks/jsx-runtime-CznXPbDH.js +1 -0
  40. package/public/shared/chunks/react-GitiQ0dt.js +1 -0
  41. package/public/shared/chunks/react-dom-BTlfKTOo.js +1 -0
  42. package/public/shared/chunks/rolldown-runtime-CbXtAM7H.js +1 -0
  43. package/public/shared/jsx-runtime.js +1 -0
  44. package/public/shared/react-dom-client.js +9 -0
  45. package/public/shared/react-dom.js +1 -0
  46. package/public/shared/react-query.js +1 -0
  47. package/public/shared/react.js +1 -0
  48. package/public/shared/styled-components.js +7 -0
  49. package/public/assets/Confirm-C5DYoI93.js +0 -4
  50. package/public/assets/CopyValue-CXjietiZ.js +0 -27
  51. package/public/assets/Lamp-QRSv-SqM.js +0 -25
  52. package/public/assets/Meter-Dngwbpjo.js +0 -14
  53. package/public/assets/Modal-DfltaSCz.js +0 -82
  54. package/public/assets/Rack-D1zBw9SW.js +0 -151
  55. package/public/assets/Toggle-DWrWc9-9.js +0 -39
  56. package/public/assets/TokenBreakdown-CyHJv0O5.js +0 -31
  57. package/public/assets/_app-CTDgfstQ.js +0 -1
  58. package/public/assets/_app.accounts-DP6T8z7I.js +0 -64
  59. package/public/assets/_app.console-DYr7TCAl.js +0 -44
  60. package/public/assets/_app.database-BzO1S_VR.js +0 -39
  61. package/public/assets/_app.index-CBt6b-lA.js +0 -62
  62. package/public/assets/_app.keys-B8HGsPPS.js +0 -76
  63. package/public/assets/_app.logs-DKsxa2Su.js +0 -68
  64. package/public/assets/_app.models-CZD2UpM-.js +0 -148
  65. package/public/assets/_app.settings-B2C1LQIF.js +0 -46
  66. package/public/assets/_app.usage-CDWUXtZy.js +0 -83
  67. package/public/assets/dist-IkJ0qg-4.js +0 -1
  68. package/public/assets/index-ClU3Xqui.js +0 -186
  69. package/public/assets/login-E3gh2_Cl.js +0 -36
  70. package/public/assets/plus-DkO4vG-T.js +0 -1
  71. package/public/assets/queries-BJHk_wso.js +0 -144
  72. package/public/assets/shared-Bp35cgFf.js +0 -84
package/bin/omni.js CHANGED
@@ -114,7 +114,7 @@ function formatBytes(bytes) {
114
114
  }
115
115
 
116
116
  // apps/cli/src/run.ts
117
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
117
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
118
118
  import { homedir as homedir3 } from "os";
119
119
 
120
120
  // packages/control/src/adminAuth.ts
@@ -19551,6 +19551,39 @@ CREATE TABLE usage_rollup (
19551
19551
  -- cannot drift into disagreeing about which rows count.
19552
19552
  `;
19553
19553
 
19554
+ // packages/store/src/sqlite/migrations/011_plugin_migrations.sql
19555
+ var _011_plugin_migrations_default = `-- The plugin migration ledger, kept deliberately apart from core's \`migrations\`.
19556
+ --
19557
+ -- Core's track is a single ascending integer owned by this repository. A plugin
19558
+ -- ships its own numbered migrations and knows nothing about ours, so the two
19559
+ -- cannot share a counter: if they did, installing a plugin would consume numbers
19560
+ -- that core's next migration then collides with, and the collision would surface
19561
+ -- as "migration already applied" on an upgrade rather than as anything legible.
19562
+ -- Core's next migration is 012 regardless of how many migrations any plugin
19563
+ -- ships, and a plugin's version 1 is its own version 1 forever.
19564
+ --
19565
+ -- The primary key is (plugin_id, version) rather than a surrogate id because
19566
+ -- that pair *is* the identity: applying the same version twice is the thing this
19567
+ -- table exists to prevent, and a uniqueness constraint states it once instead of
19568
+ -- leaving it to whichever code path happens to check first.
19569
+ --
19570
+ -- \`applied_at\` is milliseconds, matching every other timestamp in this schema.
19571
+ -- It is recorded for an operator reading the table during an incident, not read
19572
+ -- by the host: the host asks only whether a row exists.
19573
+ --
19574
+ -- Note what is *not* here: no row for the plugin's tables, and no cascade. A
19575
+ -- plugin's tables outlive its rows here on purpose \u2014 see \`PluginRepo.dropAll\`
19576
+ -- and \`orphanTables\`. Dropping a table because a plugin is currently absent
19577
+ -- destroys data irreversibly, and a restore is precisely the moment a plugin is
19578
+ -- most likely to be absent.
19579
+ CREATE TABLE plugin_migrations (
19580
+ plugin_id TEXT NOT NULL,
19581
+ version INTEGER NOT NULL,
19582
+ applied_at INTEGER NOT NULL,
19583
+ PRIMARY KEY (plugin_id, version)
19584
+ );
19585
+ `;
19586
+
19554
19587
  // packages/store/src/sqlite/rollup.ts
19555
19588
  var HOUR_MS = 3600000;
19556
19589
  function hourOf(at) {
@@ -19785,7 +19818,8 @@ var MIGRATIONS = [
19785
19818
  { id: 7, sql: _007_quota_samples_default },
19786
19819
  { id: 8, sql: _008_body_logging_default },
19787
19820
  { id: 9, sql: _009_key_limits_default },
19788
- { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
19821
+ { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
19822
+ { id: 11, sql: _011_plugin_migrations_default }
19789
19823
  ];
19790
19824
  function openDb(path) {
19791
19825
  const db = new Database(path, { create: true });
@@ -19938,6 +19972,206 @@ function createMaintenanceRepo(db) {
19938
19972
  }
19939
19973
  };
19940
19974
  }
19975
+ // packages/store/src/sqlite/plugins.ts
19976
+ var PLUGIN_ID = /^[a-z][a-z0-9-]{0,31}$/;
19977
+ var TABLE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
19978
+ var PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
19979
+ var CORE_TABLES = [
19980
+ "api_keys",
19981
+ "credential_health",
19982
+ "credentials",
19983
+ "migrations",
19984
+ "plugin_migrations",
19985
+ "quota_samples",
19986
+ "quota_windows",
19987
+ "request_bodies",
19988
+ "request_logs",
19989
+ "settings",
19990
+ "usage_daily",
19991
+ "usage_rollup",
19992
+ "virtual_models"
19993
+ ];
19994
+ var CORE_TABLE_REFERENCE = new RegExp(`\\b(?:${CORE_TABLES.join("|")})\\b`, "i");
19995
+ var PREFIX3 = "plugin_";
19996
+ function stripNoise(sql) {
19997
+ let out = "";
19998
+ let i = 0;
19999
+ while (i < sql.length) {
20000
+ const c = sql[i];
20001
+ const next = sql[i + 1];
20002
+ if (c === '"' || c === "`" || c === "[") {
20003
+ const close = c === "[" ? "]" : c;
20004
+ out += c;
20005
+ i += 1;
20006
+ while (i < sql.length) {
20007
+ if (sql[i] === close) {
20008
+ if (close !== "]" && sql[i + 1] === close) {
20009
+ out += close + close;
20010
+ i += 2;
20011
+ continue;
20012
+ }
20013
+ break;
20014
+ }
20015
+ out += sql[i];
20016
+ i += 1;
20017
+ }
20018
+ out += close;
20019
+ i += 1;
20020
+ } else if (c === "'") {
20021
+ i += 1;
20022
+ while (i < sql.length) {
20023
+ if (sql[i] === "'") {
20024
+ if (sql[i + 1] === "'")
20025
+ i += 2;
20026
+ else
20027
+ break;
20028
+ } else
20029
+ i += 1;
20030
+ }
20031
+ i += 1;
20032
+ out += " ";
20033
+ } else if (c === "-" && next === "-") {
20034
+ while (i < sql.length && sql[i] !== `
20035
+ `)
20036
+ i += 1;
20037
+ out += " ";
20038
+ } else if (c === "/" && next === "*") {
20039
+ i += 2;
20040
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/"))
20041
+ i += 1;
20042
+ i += 2;
20043
+ out += " ";
20044
+ } else {
20045
+ out += c;
20046
+ i += 1;
20047
+ }
20048
+ }
20049
+ return out;
20050
+ }
20051
+ function tableFor(pluginId, name) {
20052
+ return `"${PREFIX3}${pluginId}_${name}"`;
20053
+ }
20054
+ function prefixFor(pluginId) {
20055
+ return `${PREFIX3}${pluginId}_`;
20056
+ }
20057
+ function assertPluginId(pluginId) {
20058
+ if (!PLUGIN_ID.test(pluginId)) {
20059
+ throw new Error(`plugin id ${JSON.stringify(pluginId)} is not a valid identifier`);
20060
+ }
20061
+ }
20062
+ var CONNECTION_STATEMENT = /^\s*(pragma|attach|detach|vacuum)\b/i;
20063
+ function prepare(pluginId, sql) {
20064
+ assertPluginId(pluginId);
20065
+ const expanded = sql.replace(PLACEHOLDER, (_match, name) => {
20066
+ if (!TABLE_NAME.test(name)) {
20067
+ throw new Error(`plugin table placeholder ${JSON.stringify(name)} must match ${TABLE_NAME.source}`);
20068
+ }
20069
+ return tableFor(pluginId, name);
20070
+ });
20071
+ const stripped = stripNoise(expanded);
20072
+ const connection = CONNECTION_STATEMENT.exec(stripped);
20073
+ if (connection !== null) {
20074
+ throw new Error(`plugin sql may not ${connection[1]?.toUpperCase()}: the database handle is the gateway's, ` + "and this would reconfigure it for everything sharing it");
20075
+ }
20076
+ const found = CORE_TABLE_REFERENCE.exec(stripped);
20077
+ if (found !== null) {
20078
+ throw new Error(`plugin sql may not reference the core table ${found[0]}`);
20079
+ }
20080
+ return expanded;
20081
+ }
20082
+ function schemaObjects(db) {
20083
+ const rows = db.query("SELECT name FROM sqlite_master").all();
20084
+ return new Set(rows.map((row) => row.name));
20085
+ }
20086
+ function assertOwnNamespace(db, pluginId, before) {
20087
+ const prefix = `${PREFIX3}${pluginId}_`;
20088
+ for (const name of schemaObjects(db)) {
20089
+ if (before.has(name) || name.startsWith(prefix) || name.startsWith("sqlite_"))
20090
+ continue;
20091
+ throw new Error(`plugin migration created ${name}, which is outside this plugin's namespace \u2014 ` + `name it {{\u2026}} so it becomes ${prefix}\u2026`);
20092
+ }
20093
+ }
20094
+ function toBinding(value, index) {
20095
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || value instanceof Uint8Array) {
20096
+ return value;
20097
+ }
20098
+ throw new Error(`plugin sql parameter ${index} is not a bindable value`);
20099
+ }
20100
+ var toBindings = (params) => (params ?? []).map(toBinding);
20101
+ function createPluginRepo(db) {
20102
+ const pluginTables = () => db.query("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all().map((r) => r.name).filter((name) => name.startsWith(PREFIX3) && name !== "plugin_migrations");
20103
+ const tablesOf = (pluginId) => {
20104
+ const prefix = prefixFor(pluginId);
20105
+ return pluginTables().filter((name) => name.startsWith(prefix));
20106
+ };
20107
+ const appliedVersions = (pluginId) => new Set(db.query("SELECT version FROM plugin_migrations WHERE plugin_id = ?").all(pluginId).map((r) => r.version));
20108
+ return {
20109
+ migrate(pluginId, migrations) {
20110
+ assertPluginId(pluginId);
20111
+ const done = appliedVersions(pluginId);
20112
+ const applied = [];
20113
+ const ordered = [...migrations].sort((a, b) => a.version - b.version);
20114
+ for (const migration of ordered) {
20115
+ if (done.has(migration.version))
20116
+ continue;
20117
+ try {
20118
+ if (!Number.isSafeInteger(migration.version) || migration.version < 1) {
20119
+ throw new Error("migration version must be a positive integer");
20120
+ }
20121
+ const sql = prepare(pluginId, migration.sql);
20122
+ const before = schemaObjects(db);
20123
+ db.transaction(() => {
20124
+ db.run(sql);
20125
+ assertOwnNamespace(db, pluginId, before);
20126
+ db.run("INSERT INTO plugin_migrations (plugin_id, version, applied_at) VALUES (?,?,?)", [pluginId, migration.version, Date.now()]);
20127
+ })();
20128
+ applied.push(migration.version);
20129
+ } catch (error51) {
20130
+ return {
20131
+ applied,
20132
+ failed: {
20133
+ version: migration.version,
20134
+ reason: error51 instanceof Error ? error51.message : String(error51)
20135
+ }
20136
+ };
20137
+ }
20138
+ }
20139
+ return { applied };
20140
+ },
20141
+ run(pluginId, sql, params) {
20142
+ db.run(prepare(pluginId, sql), toBindings(params));
20143
+ },
20144
+ all(pluginId, sql, params) {
20145
+ return db.prepare(prepare(pluginId, sql)).all(...toBindings(params));
20146
+ },
20147
+ get(pluginId, sql, params) {
20148
+ const row = db.prepare(prepare(pluginId, sql)).get(...toBindings(params));
20149
+ return row === null || row === undefined ? null : row;
20150
+ },
20151
+ transaction(pluginId, fn) {
20152
+ assertPluginId(pluginId);
20153
+ return db.transaction(fn)();
20154
+ },
20155
+ listTables(pluginId) {
20156
+ assertPluginId(pluginId);
20157
+ return tablesOf(pluginId);
20158
+ },
20159
+ dropAll(pluginId) {
20160
+ assertPluginId(pluginId);
20161
+ const tables = tablesOf(pluginId);
20162
+ db.transaction(() => {
20163
+ for (const table2 of tables)
20164
+ db.run(`DROP TABLE IF EXISTS "${table2}"`);
20165
+ db.run("DELETE FROM plugin_migrations WHERE plugin_id = ?", [pluginId]);
20166
+ })();
20167
+ return tables.length;
20168
+ },
20169
+ orphanTables(installedIds) {
20170
+ const prefixes = installedIds.filter((id) => PLUGIN_ID.test(id)).map(prefixFor);
20171
+ return pluginTables().filter((name) => !prefixes.some((p) => name.startsWith(p)));
20172
+ }
20173
+ };
20174
+ }
19941
20175
  // packages/rtk/src/catalog.ts
19942
20176
  var RTK_FILTER_IDS = [
19943
20177
  "git-diff",
@@ -20250,7 +20484,8 @@ async function createStore(opts) {
20250
20484
  keys: createKeyRepo(db, logger2),
20251
20485
  usage: createUsageRepo(db),
20252
20486
  bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
20253
- maintenance: createMaintenanceRepo(db)
20487
+ maintenance: createMaintenanceRepo(db),
20488
+ plugins: createPluginRepo(db)
20254
20489
  };
20255
20490
  };
20256
20491
  let handle = open();
@@ -20323,6 +20558,22 @@ async function createStore(opts) {
20323
20558
  snapshotTo: (path) => handle.maintenance.snapshotTo(path),
20324
20559
  inspect: (path) => handle.maintenance.inspect(path)
20325
20560
  },
20561
+ plugins: {
20562
+ migrate: (pluginId, migrations) => handle.plugins.migrate(pluginId, migrations),
20563
+ run: (pluginId, sql, params) => handle.plugins.run(pluginId, sql, params),
20564
+ all(pluginId, sql, params) {
20565
+ return handle.plugins.all(pluginId, sql, params);
20566
+ },
20567
+ get(pluginId, sql, params) {
20568
+ return handle.plugins.get(pluginId, sql, params);
20569
+ },
20570
+ transaction(pluginId, fn) {
20571
+ return handle.plugins.transaction(pluginId, fn);
20572
+ },
20573
+ listTables: (pluginId) => handle.plugins.listTables(pluginId),
20574
+ dropAll: (pluginId) => handle.plugins.dropAll(pluginId),
20575
+ orphanTables: (installedIds) => handle.plugins.orphanTables(installedIds)
20576
+ },
20326
20577
  routing: {
20327
20578
  version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
20328
20579
  subscribe(listener) {
@@ -20727,6 +20978,13 @@ async function swapIn(deps, candidate) {
20727
20978
  reason: error51 instanceof Error ? error51.message : "unknown"
20728
20979
  });
20729
20980
  }
20981
+ try {
20982
+ await deps.reapplyPluginSchema?.();
20983
+ } catch (error51) {
20984
+ (deps.logger ?? noopLogger).warn("plugin schema not reapplied after the swap; restart", {
20985
+ reason: error51 instanceof Error ? error51.message : "unknown"
20986
+ });
20987
+ }
20730
20988
  return {
20731
20989
  ok: true,
20732
20990
  counts: inspection.counts,
@@ -21028,13 +21286,594 @@ import {
21028
21286
  copyFileSync,
21029
21287
  mkdirSync,
21030
21288
  readdirSync,
21289
+ readFileSync,
21031
21290
  realpathSync,
21032
21291
  renameSync,
21033
21292
  rmSync,
21034
21293
  statfsSync,
21035
- statSync
21294
+ statSync,
21295
+ writeFileSync
21036
21296
  } from "fs";
21037
- import { join as join3 } from "path";
21297
+ import { join as join4 } from "path";
21298
+
21299
+ // packages/control/src/plugins.ts
21300
+ import { Buffer as Buffer2 } from "buffer";
21301
+ import { basename as basename2, join as join3, resolve as resolve2, sep as sep2 } from "path";
21302
+
21303
+ // packages/plugin-api/src/version.ts
21304
+ var PLUGIN_API_VERSION = 1;
21305
+ var DASHBOARD_SDK_VERSION = "0.1.0";
21306
+ // packages/plugin-api/src/manifest.ts
21307
+ var CAPABILITIES = [
21308
+ "storage",
21309
+ "files",
21310
+ "net:outbound",
21311
+ "events:request",
21312
+ "events:limit"
21313
+ ];
21314
+ var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
21315
+ var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
21316
+ var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
21317
+ message: "entry must be a relative path with no '..' segment"
21318
+ });
21319
+ var originSchema = exports_external.string().refine((value) => {
21320
+ let url2;
21321
+ try {
21322
+ url2 = new URL(value);
21323
+ } catch {
21324
+ return false;
21325
+ }
21326
+ if (url2.protocol !== "https:" && url2.protocol !== "http:")
21327
+ return false;
21328
+ if (url2.hostname === "")
21329
+ return false;
21330
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "")
21331
+ return false;
21332
+ return value === url2.origin || value === `${url2.origin}/`;
21333
+ }, { message: "origin must be scheme://host with no path, query or fragment" });
21334
+ var navSchema = exports_external.object({
21335
+ label: exports_external.string().min(1).max(32)
21336
+ }).strict();
21337
+ var manifestSchema = exports_external.object({
21338
+ id: idSchema,
21339
+ name: exports_external.string().min(1).max(64),
21340
+ version: exports_external.string().min(1).max(32),
21341
+ api: exports_external.number().int().nonnegative(),
21342
+ sdk: exports_external.string().min(1).max(32).optional(),
21343
+ server: entrySchema.optional(),
21344
+ ui: entrySchema.optional(),
21345
+ nav: navSchema.optional(),
21346
+ capabilities: exports_external.array(exports_external.enum(CAPABILITIES)).default([]),
21347
+ origins: exports_external.array(originSchema).min(1).optional(),
21348
+ defaults: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21349
+ }).strict().refine((m) => m.server !== undefined || m.ui !== undefined, {
21350
+ message: "manifest must declare a server entry, a ui entry, or both"
21351
+ }).refine((m) => m.ui === undefined || m.sdk !== undefined, {
21352
+ message: "a ui entry requires an sdk range"
21353
+ }).refine((m) => !m.capabilities.includes("net:outbound") || m.origins !== undefined, {
21354
+ message: "net:outbound requires origins"
21355
+ }).refine((m) => m.capabilities.includes("net:outbound") || m.origins === undefined, {
21356
+ message: "origins requires the net:outbound capability"
21357
+ });
21358
+ function safeParseManifest(input) {
21359
+ const parsed = manifestSchema.safeParse(input);
21360
+ if (parsed.success)
21361
+ return { ok: true, manifest: parsed.data };
21362
+ const first = parsed.error.issues[0];
21363
+ const path = first?.path.join(".") ?? "";
21364
+ const message = first?.message ?? "invalid manifest";
21365
+ return { ok: false, reason: path === "" ? message : `${path}: ${message}` };
21366
+ }
21367
+ function isApiCompatible(manifest) {
21368
+ return manifest.api === PLUGIN_API_VERSION;
21369
+ }
21370
+
21371
+ // packages/control/src/plugins.ts
21372
+ var PLUGINS_DIRNAME = "plugins";
21373
+ function pluginsDir(root) {
21374
+ return join3(root, PLUGINS_DIRNAME);
21375
+ }
21376
+ var MANIFEST_FILENAME = "omni-plugin.json";
21377
+ var MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
21378
+ var ID_PATTERN2 = /^[a-z][a-z0-9-]{0,31}$/;
21379
+ var NPM_TARBALL_ROOT = "package";
21380
+ function readManifest(fs, home) {
21381
+ const raw = fs.readText(join3(home, MANIFEST_FILENAME));
21382
+ if (raw === null) {
21383
+ return { check: "manifest", reason: `no ${MANIFEST_FILENAME}`, fatal: true };
21384
+ }
21385
+ let document;
21386
+ try {
21387
+ document = JSON.parse(raw);
21388
+ } catch (error51) {
21389
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21390
+ return {
21391
+ check: "manifest",
21392
+ reason: `${MANIFEST_FILENAME} is not valid JSON: ${detail}`,
21393
+ fatal: true
21394
+ };
21395
+ }
21396
+ const parsed = safeParseManifest(document);
21397
+ if (!parsed.ok)
21398
+ return { check: "manifest", reason: parsed.reason, fatal: true };
21399
+ return parsed.manifest;
21400
+ }
21401
+ function checkEntry(fs, home, entry, label2, fatal) {
21402
+ const target = resolve2(home, entry);
21403
+ if (target !== home && !target.startsWith(`${home}${sep2}`)) {
21404
+ return {
21405
+ check: "entry",
21406
+ reason: `${label2} entry resolves outside the plugin directory`,
21407
+ fatal
21408
+ };
21409
+ }
21410
+ if (!fs.isFile(target)) {
21411
+ return { check: "entry", reason: `${label2} entry ${entry} does not exist`, fatal };
21412
+ }
21413
+ return null;
21414
+ }
21415
+ function verifyPlugin(deps, root, id) {
21416
+ const home = join3(pluginsDir(root), id);
21417
+ if (!ID_PATTERN2.test(id) || !deps.fs.isDirectory(home)) {
21418
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21419
+ }
21420
+ return reportFor(deps, home, id);
21421
+ }
21422
+ function reportFor(deps, home, id) {
21423
+ const problems = [];
21424
+ const read = readManifest(deps.fs, home);
21425
+ if (!("id" in read)) {
21426
+ problems.push(read);
21427
+ return { id, path: home, manifest: null, problems, loadable: false };
21428
+ }
21429
+ const manifest = read;
21430
+ if (manifest.id !== id) {
21431
+ problems.push({
21432
+ check: "id",
21433
+ reason: `manifest id ${manifest.id} does not match its directory ${id}`,
21434
+ fatal: true
21435
+ });
21436
+ }
21437
+ if (!isApiCompatible(manifest)) {
21438
+ problems.push({
21439
+ check: "api",
21440
+ reason: `plugin api ${manifest.api} is not supported by this host (api ${PLUGIN_API_VERSION})`,
21441
+ fatal: true
21442
+ });
21443
+ }
21444
+ if (manifest.server !== undefined) {
21445
+ const problem = checkEntry(deps.fs, home, manifest.server, "server", true);
21446
+ if (problem !== null)
21447
+ problems.push(problem);
21448
+ }
21449
+ if (manifest.ui !== undefined) {
21450
+ const problem = checkEntry(deps.fs, home, manifest.ui, "ui", false);
21451
+ if (problem !== null)
21452
+ problems.push(problem);
21453
+ }
21454
+ const sdk = sdkProblem(deps, manifest);
21455
+ if (sdk !== null)
21456
+ problems.push(sdk);
21457
+ return {
21458
+ id,
21459
+ path: home,
21460
+ manifest,
21461
+ problems,
21462
+ loadable: !problems.some((problem) => problem.fatal)
21463
+ };
21464
+ }
21465
+ function sdkProblem(deps, manifest) {
21466
+ if (manifest.ui === undefined || manifest.sdk === undefined)
21467
+ return null;
21468
+ if (deps.sdkVersion === undefined)
21469
+ return null;
21470
+ if (Bun.semver.satisfies(deps.sdkVersion, manifest.sdk))
21471
+ return null;
21472
+ return {
21473
+ check: "sdk",
21474
+ reason: `requires dashboard sdk ${manifest.sdk}, host ships ${deps.sdkVersion}`,
21475
+ fatal: false
21476
+ };
21477
+ }
21478
+ function listPlugins(deps, root) {
21479
+ const dir = pluginsDir(root);
21480
+ const summaries = [];
21481
+ for (const entry of [...deps.fs.readdir(dir)].sort()) {
21482
+ const home = join3(dir, entry);
21483
+ if (!deps.fs.isDirectory(home))
21484
+ continue;
21485
+ if (!ID_PATTERN2.test(entry)) {
21486
+ summaries.push({
21487
+ id: entry,
21488
+ path: home,
21489
+ manifest: null,
21490
+ problems: [
21491
+ {
21492
+ check: "id",
21493
+ reason: `directory name ${entry} is not a valid plugin id`,
21494
+ fatal: true
21495
+ }
21496
+ ],
21497
+ loadable: false,
21498
+ name: null,
21499
+ version: null,
21500
+ api: null,
21501
+ sdk: null,
21502
+ capabilities: [],
21503
+ origins: []
21504
+ });
21505
+ continue;
21506
+ }
21507
+ const report = reportFor(deps, home, entry);
21508
+ const manifest = report.manifest;
21509
+ summaries.push({
21510
+ ...report,
21511
+ name: manifest?.name ?? null,
21512
+ version: manifest?.version ?? null,
21513
+ api: manifest?.api ?? null,
21514
+ sdk: manifest?.sdk ?? null,
21515
+ capabilities: manifest?.capabilities ?? [],
21516
+ origins: manifest?.origins ?? []
21517
+ });
21518
+ }
21519
+ return summaries;
21520
+ }
21521
+ var BLOCK = 512;
21522
+ var decoder2 = new TextDecoder;
21523
+ function fieldText(header, offset, length) {
21524
+ const slice = header.subarray(offset, offset + length);
21525
+ const end = slice.indexOf(0);
21526
+ return decoder2.decode(end === -1 ? slice : slice.subarray(0, end));
21527
+ }
21528
+ function fieldOctal(header, offset, length) {
21529
+ const text = fieldText(header, offset, length).trim();
21530
+ if (text.length === 0)
21531
+ return 0;
21532
+ const value = Number.parseInt(text, 8);
21533
+ return Number.isSafeInteger(value) && value >= 0 ? value : -1;
21534
+ }
21535
+ function isSafeArchivePath(path) {
21536
+ if (path.length === 0 || path.startsWith("/") || path.includes("\x00"))
21537
+ return false;
21538
+ if (path.includes("\\"))
21539
+ return false;
21540
+ return !path.split("/").includes("..");
21541
+ }
21542
+ function readTar(bytes) {
21543
+ const files = new Map;
21544
+ let total = 0;
21545
+ let longName = null;
21546
+ let offset = 0;
21547
+ while (offset + BLOCK <= bytes.length) {
21548
+ const header = bytes.subarray(offset, offset + BLOCK);
21549
+ if (header.every((byte) => byte === 0))
21550
+ break;
21551
+ const size = fieldOctal(header, 124, 12);
21552
+ if (size < 0)
21553
+ throw new GatewayError("BAD_REQUEST", "archive has an unreadable entry size");
21554
+ const type = String.fromCharCode(header[156] ?? 0);
21555
+ const name = fieldText(header, 0, 100);
21556
+ const prefix = fieldText(header, 345, 155);
21557
+ offset += BLOCK;
21558
+ total += size;
21559
+ if (total > MAX_PLUGIN_BYTES) {
21560
+ throw new GatewayError("BAD_REQUEST", `archive unpacks to more than ${MAX_PLUGIN_BYTES} bytes`);
21561
+ }
21562
+ if (offset + size > bytes.length) {
21563
+ throw new GatewayError("BAD_REQUEST", `archive is truncated: entry ${name || "(unnamed)"} declares ${size} bytes and the archive ends before them`);
21564
+ }
21565
+ const data = bytes.subarray(offset, offset + size);
21566
+ offset += Math.ceil(size / BLOCK) * BLOCK;
21567
+ if (type === "1" || type === "2") {
21568
+ throw new GatewayError("BAD_REQUEST", `archive contains a link (${name}); plugin packages must be plain files`);
21569
+ }
21570
+ if (type === "L") {
21571
+ longName = decoder2.decode(data).replace(/\0+$/, "");
21572
+ continue;
21573
+ }
21574
+ if (type === "K" || type === "x" || type === "g")
21575
+ continue;
21576
+ if (type !== "0" && type !== "\x00") {
21577
+ longName = null;
21578
+ continue;
21579
+ }
21580
+ const path = longName ?? (prefix.length > 0 ? `${prefix}/${name}` : name);
21581
+ longName = null;
21582
+ if (!isSafeArchivePath(path)) {
21583
+ throw new GatewayError("BAD_REQUEST", `archive contains an unsafe path: ${path}`);
21584
+ }
21585
+ files.set(path, new Uint8Array(data));
21586
+ }
21587
+ return files;
21588
+ }
21589
+ function gunzipIfNeeded(bytes) {
21590
+ if (bytes[0] !== 31 || bytes[1] !== 139)
21591
+ return bytes;
21592
+ try {
21593
+ return Bun.gunzipSync(new Uint8Array(bytes));
21594
+ } catch (error51) {
21595
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21596
+ throw new GatewayError("BAD_REQUEST", `could not decompress the archive: ${detail}`);
21597
+ }
21598
+ }
21599
+ function stripRoot(files) {
21600
+ const roots = new Set;
21601
+ for (const path of files.keys()) {
21602
+ const slash = path.indexOf("/");
21603
+ if (slash <= 0)
21604
+ return { files, claimedId: null };
21605
+ roots.add(path.slice(0, slash));
21606
+ }
21607
+ const [only] = [...roots];
21608
+ if (roots.size !== 1 || only === undefined)
21609
+ return { files, claimedId: null };
21610
+ const stripped = new Map;
21611
+ for (const [path, bytes] of files)
21612
+ stripped.set(path.slice(only.length + 1), bytes);
21613
+ return { files: stripped, claimedId: only === NPM_TARBALL_ROOT ? null : only };
21614
+ }
21615
+ function readTree(fs, dir, prefix = "") {
21616
+ const files = new Map;
21617
+ let total = 0;
21618
+ for (const entry of fs.readdir(dir)) {
21619
+ const path = join3(dir, entry);
21620
+ const relative = prefix.length === 0 ? entry : `${prefix}/${entry}`;
21621
+ if (fs.isDirectory(path)) {
21622
+ for (const [nested, bytes2] of readTree(fs, path, relative))
21623
+ files.set(nested, bytes2);
21624
+ continue;
21625
+ }
21626
+ const bytes = fs.readBytes(path);
21627
+ if (bytes === null)
21628
+ continue;
21629
+ total += bytes.byteLength;
21630
+ if (total > MAX_PLUGIN_BYTES) {
21631
+ throw new GatewayError("BAD_REQUEST", `${dir} holds more than ${MAX_PLUGIN_BYTES} bytes`);
21632
+ }
21633
+ files.set(relative, bytes);
21634
+ }
21635
+ return files;
21636
+ }
21637
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
21638
+ var NPM_PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
21639
+ var NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
21640
+ var EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
21641
+ var SRI_ALGORITHMS = ["sha512", "sha384", "sha256"];
21642
+ function asRecord(value) {
21643
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21644
+ return null;
21645
+ return value;
21646
+ }
21647
+ function asString(value) {
21648
+ return typeof value === "string" ? value : null;
21649
+ }
21650
+ function parseNpmSpec(spec) {
21651
+ const at = spec.lastIndexOf("@");
21652
+ const name = at <= 0 ? spec : spec.slice(0, at);
21653
+ const version2 = at <= 0 ? null : spec.slice(at + 1);
21654
+ if (name.length > 214 || !NPM_NAME_PATTERN.test(name))
21655
+ return null;
21656
+ return { name, version: version2 };
21657
+ }
21658
+ function registryUrl(deps) {
21659
+ const raw = deps.registry ?? DEFAULT_NPM_REGISTRY;
21660
+ let url2;
21661
+ try {
21662
+ url2 = new URL(raw);
21663
+ } catch {
21664
+ throw new GatewayError("BAD_REQUEST", `registry ${raw} is not a URL`);
21665
+ }
21666
+ if (url2.protocol !== "https:") {
21667
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; ${raw} is not`);
21668
+ }
21669
+ return url2;
21670
+ }
21671
+ function packumentUrl(registry2, name) {
21672
+ return `${registry2.href.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
21673
+ }
21674
+ function resolveDist(document, spec) {
21675
+ const root = asRecord(document);
21676
+ if (root === null) {
21677
+ throw new GatewayError("BAD_REQUEST", `the registry's answer for ${spec.name} is not a package`);
21678
+ }
21679
+ const versions2 = asRecord(root.versions);
21680
+ if (versions2 === null) {
21681
+ throw new GatewayError("BAD_REQUEST", `the registry lists no versions of ${spec.name}`);
21682
+ }
21683
+ let wanted = spec.version;
21684
+ if (wanted === null) {
21685
+ const tags = asRecord(root["dist-tags"]);
21686
+ wanted = tags === null ? null : asString(tags.latest);
21687
+ if (wanted === null) {
21688
+ throw new GatewayError("BAD_REQUEST", `the registry names no dist-tags.latest for ${spec.name}; install ${spec.name}@<exact version>`);
21689
+ }
21690
+ }
21691
+ const entry = asRecord(versions2[wanted]);
21692
+ if (entry === null) {
21693
+ throw new GatewayError("BAD_REQUEST", `the registry has no version ${wanted} of ${spec.name}`);
21694
+ }
21695
+ const dist = asRecord(entry.dist);
21696
+ const tarball = dist === null ? null : asString(dist.tarball);
21697
+ if (dist === null || tarball === null) {
21698
+ throw new GatewayError("BAD_REQUEST", `the registry's entry for ${spec.name}@${wanted} names no tarball`);
21699
+ }
21700
+ return {
21701
+ version: wanted,
21702
+ dist: { tarball, integrity: asString(dist.integrity), shasum: asString(dist.shasum) }
21703
+ };
21704
+ }
21705
+ function checkTarballUrl(tarball, registry2, what) {
21706
+ let url2;
21707
+ try {
21708
+ url2 = new URL(tarball);
21709
+ } catch {
21710
+ throw new GatewayError("BAD_REQUEST", `the registry gave ${what} a tarball that is not a URL`);
21711
+ }
21712
+ if (url2.protocol !== "https:") {
21713
+ throw new GatewayError("BAD_REQUEST", `plugins may only be installed over https; the registry points ${what} at ${tarball}`);
21714
+ }
21715
+ if (url2.host !== registry2.host) {
21716
+ throw new GatewayError("BAD_REQUEST", `registry ${registry2.host} points ${what} at ${url2.host}; ` + "install that URL directly if it is the one you meant");
21717
+ }
21718
+ }
21719
+ function digestsFor(dist, what) {
21720
+ const digests = [];
21721
+ if (dist.integrity !== null) {
21722
+ for (const entry of dist.integrity.trim().split(/\s+/)) {
21723
+ const dash = entry.indexOf("-");
21724
+ const algorithm = dash === -1 ? "" : entry.slice(0, dash);
21725
+ const value = dash === -1 ? "" : entry.slice(dash + 1);
21726
+ if (value.length === 0)
21727
+ continue;
21728
+ const known = SRI_ALGORITHMS.find((candidate) => candidate === algorithm);
21729
+ if (known === undefined)
21730
+ continue;
21731
+ digests.push({
21732
+ algorithm: known,
21733
+ expected: Buffer2.from(value, "base64").toString("hex"),
21734
+ source: `${known} integrity`
21735
+ });
21736
+ }
21737
+ }
21738
+ if (digests.length === 0 && dist.shasum !== null && /^[0-9a-f]{40}$/i.test(dist.shasum)) {
21739
+ digests.push({ algorithm: "sha1", expected: dist.shasum.toLowerCase(), source: "sha1 shasum" });
21740
+ }
21741
+ if (digests.length === 0) {
21742
+ throw new GatewayError("BAD_REQUEST", `the registry advertises no usable integrity or shasum for ${what}; ` + "refusing to install bytes nothing vouches for");
21743
+ }
21744
+ return digests;
21745
+ }
21746
+ function verifyDigests(bytes, digests, what) {
21747
+ for (const digest of digests) {
21748
+ const actual = new Bun.CryptoHasher(digest.algorithm).update(bytes).digest("hex");
21749
+ if (actual === digest.expected)
21750
+ return;
21751
+ }
21752
+ const first = digests[0];
21753
+ throw new GatewayError("BAD_REQUEST", `${what} does not match the ${first?.source ?? "digest"} the registry advertised; ` + "the download was corrupted or the registry served something else");
21754
+ }
21755
+ async function npmPayload(deps, spec) {
21756
+ if (spec.version !== null && !EXACT_VERSION_PATTERN.test(spec.version)) {
21757
+ throw new GatewayError("BAD_REQUEST", `${spec.name}@${spec.version} is not an exact version; this installer resolves no ranges ` + `or tags \u2014 install ${spec.name}@1.2.3, or ${spec.name} for the registry's latest`);
21758
+ }
21759
+ const fetchBytes = deps.fetchBytes;
21760
+ if (fetchBytes === undefined) {
21761
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a registry");
21762
+ }
21763
+ const registry2 = registryUrl(deps);
21764
+ const document = parseJson(await fetchBytes(packumentUrl(registry2, spec.name), NPM_PACKUMENT_ACCEPT), `the registry's answer for ${spec.name}`);
21765
+ const { version: version2, dist } = resolveDist(document, spec);
21766
+ const what = `${spec.name}@${version2}`;
21767
+ checkTarballUrl(dist.tarball, registry2, what);
21768
+ const digests = digestsFor(dist, what);
21769
+ const bytes = await fetchBytes(dist.tarball);
21770
+ verifyDigests(bytes, digests, what);
21771
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21772
+ }
21773
+ function parseJson(bytes, what) {
21774
+ try {
21775
+ return JSON.parse(decoder2.decode(bytes));
21776
+ } catch (error51) {
21777
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21778
+ throw new GatewayError("BAD_REQUEST", `${what} is not valid JSON: ${detail}`);
21779
+ }
21780
+ }
21781
+ async function loadPayload(deps, spec) {
21782
+ if (spec.startsWith("http://")) {
21783
+ throw new GatewayError("BAD_REQUEST", "plugins may only be installed over https");
21784
+ }
21785
+ if (spec.startsWith("https://")) {
21786
+ if (deps.fetchBytes === undefined) {
21787
+ throw new GatewayError("BAD_REQUEST", "this caller cannot install from a URL");
21788
+ }
21789
+ return stripRoot(readTar(gunzipIfNeeded(await deps.fetchBytes(spec))));
21790
+ }
21791
+ const source = resolve2(spec);
21792
+ if (deps.fs.isDirectory(source)) {
21793
+ const files = readTree(deps.fs, source);
21794
+ const claimed = basename2(source);
21795
+ return { files, claimedId: ID_PATTERN2.test(claimed) ? claimed : null };
21796
+ }
21797
+ const bytes = deps.fs.readBytes(source);
21798
+ if (bytes !== null)
21799
+ return stripRoot(readTar(gunzipIfNeeded(bytes)));
21800
+ const npm = parseNpmSpec(spec);
21801
+ if (npm !== null)
21802
+ return npmPayload(deps, npm);
21803
+ throw new GatewayError("BAD_REQUEST", `no directory or archive at ${source}, and "${spec}" is not a package name`);
21804
+ }
21805
+ async function installPlugin(deps, root, spec) {
21806
+ const payload = await loadPayload(deps, spec);
21807
+ const raw = payload.files.get(MANIFEST_FILENAME);
21808
+ if (raw === undefined) {
21809
+ throw new GatewayError("BAD_REQUEST", `${spec} has no ${MANIFEST_FILENAME} at its root`);
21810
+ }
21811
+ let document;
21812
+ try {
21813
+ document = JSON.parse(decoder2.decode(raw));
21814
+ } catch (error51) {
21815
+ const detail = error51 instanceof Error ? error51.message : String(error51);
21816
+ throw new GatewayError("BAD_REQUEST", `${MANIFEST_FILENAME} is not valid JSON: ${detail}`);
21817
+ }
21818
+ const parsed = safeParseManifest(document);
21819
+ if (!parsed.ok)
21820
+ throw new GatewayError("BAD_REQUEST", `invalid ${MANIFEST_FILENAME}: ${parsed.reason}`);
21821
+ const manifest = parsed.manifest;
21822
+ const targetId = payload.claimedId ?? manifest.id;
21823
+ if (manifest.id !== targetId) {
21824
+ throw new GatewayError("BAD_REQUEST", `manifest id ${manifest.id} does not match its directory ${targetId}; ` + "rename one of them so the gateway loads what you installed");
21825
+ }
21826
+ const dir = pluginsDir(root);
21827
+ const target = join3(dir, targetId);
21828
+ const replaced = deps.fs.isDirectory(target);
21829
+ const staging = join3(dir, `.staging-${targetId}`);
21830
+ deps.fs.rm(staging);
21831
+ try {
21832
+ for (const [path, bytes] of payload.files) {
21833
+ const destination = join3(staging, path);
21834
+ deps.fs.mkdir(join3(destination, ".."));
21835
+ deps.fs.writeBytes(destination, bytes);
21836
+ }
21837
+ deps.fs.rm(target);
21838
+ deps.fs.rename(staging, target);
21839
+ } finally {
21840
+ deps.fs.rm(staging);
21841
+ }
21842
+ return {
21843
+ id: targetId,
21844
+ name: manifest.name,
21845
+ version: manifest.version,
21846
+ path: target,
21847
+ replaced,
21848
+ restartRequired: true
21849
+ };
21850
+ }
21851
+ function removePlugin(deps, root, id, options = {}) {
21852
+ if (!ID_PATTERN2.test(id)) {
21853
+ throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
21854
+ }
21855
+ const target = join3(pluginsDir(root), id);
21856
+ const present = deps.fs.isDirectory(target);
21857
+ const purge = options.purge === true;
21858
+ if (purge && deps.store === undefined) {
21859
+ throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
21860
+ }
21861
+ const tables = purge && deps.store !== undefined ? deps.store.plugins.listTables(id) : [];
21862
+ if (!present && tables.length === 0) {
21863
+ throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
21864
+ }
21865
+ if (present)
21866
+ deps.fs.rm(target);
21867
+ if (purge && deps.store !== undefined)
21868
+ deps.store.plugins.dropAll(id);
21869
+ return { id, removed: present, droppedTables: tables };
21870
+ }
21871
+ function orphanPluginTables(deps, root, store) {
21872
+ const installed = listPlugins(deps, root).map((plugin) => plugin.id);
21873
+ return store.plugins.orphanTables(installed);
21874
+ }
21875
+
21876
+ // packages/control/src/nodeFs.ts
21038
21877
  function nodeDatabaseFs() {
21039
21878
  const dirBytes = (dir) => {
21040
21879
  let entries;
@@ -21045,7 +21884,7 @@ function nodeDatabaseFs() {
21045
21884
  }
21046
21885
  let total = 0;
21047
21886
  for (const entry of entries) {
21048
- const path = join3(dir, entry.name);
21887
+ const path = join4(dir, entry.name);
21049
21888
  if (entry.isDirectory()) {
21050
21889
  total += dirBytes(path);
21051
21890
  continue;
@@ -21098,6 +21937,96 @@ function nodeDatabaseFs() {
21098
21937
  dirBytes
21099
21938
  };
21100
21939
  }
21940
+ function nodePluginFs() {
21941
+ return {
21942
+ readdir: (dir) => {
21943
+ try {
21944
+ return readdirSync(dir);
21945
+ } catch {
21946
+ return [];
21947
+ }
21948
+ },
21949
+ readText: (path) => {
21950
+ try {
21951
+ return readFileSync(path, "utf8");
21952
+ } catch {
21953
+ return null;
21954
+ }
21955
+ },
21956
+ readBytes: (path) => {
21957
+ try {
21958
+ return new Uint8Array(readFileSync(path));
21959
+ } catch {
21960
+ return null;
21961
+ }
21962
+ },
21963
+ writeBytes: (path, bytes) => {
21964
+ writeFileSync(path, bytes);
21965
+ },
21966
+ isDirectory: (path) => {
21967
+ try {
21968
+ return statSync(path).isDirectory();
21969
+ } catch {
21970
+ return false;
21971
+ }
21972
+ },
21973
+ isFile: (path) => {
21974
+ try {
21975
+ return statSync(path).isFile();
21976
+ } catch {
21977
+ return false;
21978
+ }
21979
+ },
21980
+ mkdir: (dir) => {
21981
+ mkdirSync(dir, { recursive: true });
21982
+ },
21983
+ rm: (path) => {
21984
+ rmSync(path, { force: true, recursive: true });
21985
+ },
21986
+ rename: (from, to) => renameSync(from, to)
21987
+ };
21988
+ }
21989
+ function nodeFetchBytes(options = {}) {
21990
+ const impl = options.fetchImpl ?? fetch;
21991
+ const maxBytes = options.maxBytes ?? MAX_PLUGIN_BYTES;
21992
+ return async (url2, accept) => {
21993
+ if (!url2.startsWith("https://")) {
21994
+ throw new GatewayError("BAD_REQUEST", `plugins may only be fetched over https; ${url2} is not`);
21995
+ }
21996
+ const response = await impl(url2, {
21997
+ headers: accept === undefined ? {} : { accept },
21998
+ redirect: "follow"
21999
+ });
22000
+ if (!response.ok) {
22001
+ throw new GatewayError("BAD_REQUEST", `${url2} answered ${response.status}`);
22002
+ }
22003
+ const reader = response.body?.getReader();
22004
+ if (reader === undefined)
22005
+ return new Uint8Array(0);
22006
+ const chunks = [];
22007
+ let total = 0;
22008
+ for (;; ) {
22009
+ const { done, value } = await reader.read();
22010
+ if (done)
22011
+ break;
22012
+ if (value === undefined)
22013
+ continue;
22014
+ total += value.byteLength;
22015
+ if (total > maxBytes) {
22016
+ await reader.cancel();
22017
+ throw new GatewayError("BAD_REQUEST", `${url2} sent more than ${maxBytes} bytes`);
22018
+ }
22019
+ chunks.push(value);
22020
+ }
22021
+ const bytes = new Uint8Array(total);
22022
+ let offset = 0;
22023
+ for (const chunk of chunks) {
22024
+ bytes.set(chunk, offset);
22025
+ offset += chunk.byteLength;
22026
+ }
22027
+ return bytes;
22028
+ };
22029
+ }
21101
22030
  // packages/control/src/oauth/pkce.ts
21102
22031
  function randomToken() {
21103
22032
  return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
@@ -22305,9 +23234,9 @@ function requirePositional(parsed, index, name) {
22305
23234
  }
22306
23235
 
22307
23236
  // apps/cli/src/context.ts
22308
- import { existsSync, readFileSync } from "fs";
23237
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
22309
23238
  import { homedir } from "os";
22310
- import { isAbsolute, join as join4, resolve as resolve2 } from "path";
23239
+ import { isAbsolute, join as join5, resolve as resolve3 } from "path";
22311
23240
  class CliError extends Error {
22312
23241
  exitCode;
22313
23242
  constructor(message, exitCode = 1) {
@@ -22316,21 +23245,21 @@ class CliError extends Error {
22316
23245
  this.exitCode = exitCode;
22317
23246
  }
22318
23247
  }
22319
- var DEFAULT_ROOT = join4(homedir(), ".config", "omnigateway");
23248
+ var DEFAULT_ROOT = join5(homedir(), ".config", "omnigateway");
22320
23249
  function looksLikeRoot(dir) {
22321
- return existsSync(join4(dir, ".env")) || existsSync(join4(dir, "omnigateway.db")) || existsSync(join4(dir, "apps", "gateway", "src", "index.ts"));
23250
+ return existsSync(join5(dir, ".env")) || existsSync(join5(dir, "omnigateway.db")) || existsSync(join5(dir, "apps", "gateway", "src", "index.ts"));
22322
23251
  }
22323
23252
  function resolveRoot(flags, env2, cwd) {
22324
23253
  const withEnvFile = (root, source) => {
22325
- const envFile = join4(root, ".env");
23254
+ const envFile = join5(root, ".env");
22326
23255
  return { root, source, envFile: existsSync(envFile) ? envFile : null };
22327
23256
  };
22328
23257
  if (flags.root !== undefined && flags.root.length > 0) {
22329
- return withEnvFile(resolve2(cwd, flags.root), "flag");
23258
+ return withEnvFile(resolve3(cwd, flags.root), "flag");
22330
23259
  }
22331
23260
  const fromEnv = env2.OMNI_ROOT;
22332
23261
  if (typeof fromEnv === "string" && fromEnv.length > 0) {
22333
- return withEnvFile(resolve2(cwd, fromEnv), "env");
23262
+ return withEnvFile(resolve3(cwd, fromEnv), "env");
22334
23263
  }
22335
23264
  if (looksLikeRoot(cwd))
22336
23265
  return withEnvFile(cwd, "cwd");
@@ -22361,7 +23290,7 @@ function createContext(parsed, options = {}) {
22361
23290
  const processEnv = options.env ?? process.env;
22362
23291
  const cwd = options.cwd ?? process.cwd();
22363
23292
  const root = resolveRoot({ root: stringFlag(parsed.values, "root") }, processEnv, cwd);
22364
- const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync(root.envFile, "utf8"));
23293
+ const fileEnv = root.envFile === null ? {} : parseEnvFile(readFileSync2(root.envFile, "utf8"));
22365
23294
  const env2 = { ...processEnv, ...fileEnv };
22366
23295
  const rawDbFlag = stringFlag(parsed.values, "db");
22367
23296
  const dbFlag = rawDbFlag === undefined || rawDbFlag.trim().length === 0 ? undefined : rawDbFlag;
@@ -22377,7 +23306,7 @@ function createContext(parsed, options = {}) {
22377
23306
  configError = error51 instanceof Error ? error51.message : "invalid configuration";
22378
23307
  }
22379
23308
  const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
22380
- const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve2(root.root, configuredPath);
23309
+ const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve3(root.root, configuredPath);
22381
23310
  const warnings = suppressedDbPath === null ? [] : [
22382
23311
  `ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
22383
23312
  ];
@@ -22603,21 +23532,21 @@ async function pollUntilAuthorized(flows, start, deps) {
22603
23532
  }
22604
23533
 
22605
23534
  // apps/cli/src/service.ts
22606
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync } from "fs";
23535
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
22607
23536
  import { homedir as homedir2 } from "os";
22608
- import { dirname as dirname3, join as join5 } from "path";
23537
+ import { dirname as dirname3, join as join6 } from "path";
22609
23538
  var UNIT_NAME2 = "omnigateway.service";
22610
23539
  function unitPath(scope) {
22611
- return scope === "system" ? join5("/etc/systemd/system", UNIT_NAME2) : join5(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
23540
+ return scope === "system" ? join6("/etc/systemd/system", UNIT_NAME2) : join6(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
22612
23541
  }
22613
23542
  function systemctlArgs(scope, args) {
22614
23543
  return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
22615
23544
  }
22616
23545
  function pidFile(stateDir) {
22617
- return join5(stateDir, "gateway.pid");
23546
+ return join6(stateDir, "gateway.pid");
22618
23547
  }
22619
23548
  function logFile(stateDir) {
22620
- return join5(stateDir, "gateway.log");
23549
+ return join6(stateDir, "gateway.log");
22621
23550
  }
22622
23551
  function supervisedLogFile(deps) {
22623
23552
  const configured = deps.logFile?.trim();
@@ -22625,8 +23554,8 @@ function supervisedLogFile(deps) {
22625
23554
  }
22626
23555
  function defaultStateDir(env2) {
22627
23556
  const xdg = env2.XDG_STATE_HOME;
22628
- const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join5(homedir2(), ".local", "state");
22629
- return join5(base, "omnigateway");
23557
+ const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join6(homedir2(), ".local", "state");
23558
+ return join6(base, "omnigateway");
22630
23559
  }
22631
23560
  function unitFile(input) {
22632
23561
  return `[Unit]
@@ -22637,7 +23566,7 @@ Wants=network-online.target
22637
23566
  [Service]
22638
23567
  Type=simple
22639
23568
  WorkingDirectory=${input.root}
22640
- EnvironmentFile=${join5(input.root, ".env")}
23569
+ EnvironmentFile=${join6(input.root, ".env")}
22641
23570
  ExecStart=${input.bun} ${input.entrypoint}
22642
23571
  Restart=on-failure
22643
23572
  RestartSec=2
@@ -22653,7 +23582,7 @@ function readPid(stateDir) {
22653
23582
  const file2 = pidFile(stateDir);
22654
23583
  if (!existsSync2(file2))
22655
23584
  return null;
22656
- const pid = Number(readFileSync2(file2, "utf8").trim());
23585
+ const pid = Number(readFileSync3(file2, "utf8").trim());
22657
23586
  return Number.isInteger(pid) && pid > 0 ? pid : null;
22658
23587
  }
22659
23588
  function livePid(deps) {
@@ -22719,7 +23648,7 @@ async function start(deps, input) {
22719
23648
  env: { ...input.env, OMNI_LOG_FILE: file2 },
22720
23649
  logFile: file2
22721
23650
  });
22722
- writeFileSync(pidFile(deps.stateDir), `${pid}
23651
+ writeFileSync2(pidFile(deps.stateDir), `${pid}
22723
23652
  `);
22724
23653
  return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
22725
23654
  }
@@ -22765,7 +23694,7 @@ async function install(deps, input) {
22765
23694
  throw new Error(`${path} already exists; pass --force to replace it`);
22766
23695
  }
22767
23696
  mkdirSync2(dirname3(path), { recursive: true });
22768
- writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
23697
+ writeFileSync2(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
22769
23698
  const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
22770
23699
  let enabled = false;
22771
23700
  if (input.enable) {
@@ -23716,6 +24645,168 @@ ${candidates}${excluded}`;
23716
24645
  });
23717
24646
  }
23718
24647
  };
24648
+ // packages/plugin-api/src/events.ts
24649
+ var WINDOW_MS2 = {
24650
+ "1m": 60000,
24651
+ "5h": 5 * 60 * 60 * 1000,
24652
+ "1w": 7 * 24 * 60 * 60 * 1000
24653
+ };
24654
+ // apps/cli/src/commands/plugins.ts
24655
+ function pluginDeps() {
24656
+ return { fs: nodePluginFs(), sdkVersion: DASHBOARD_SDK_VERSION };
24657
+ }
24658
+ function installDeps(ctx, args) {
24659
+ const flag = stringFlag(args.values, "registry");
24660
+ const raw = flag ?? ctx.env.OMNI_PLUGIN_REGISTRY;
24661
+ const registry2 = raw === undefined || raw.trim().length === 0 ? undefined : raw.trim();
24662
+ return {
24663
+ ...pluginDeps(),
24664
+ fetchBytes: nodeFetchBytes(),
24665
+ ...registry2 === undefined ? {} : { registry: registry2 }
24666
+ };
24667
+ }
24668
+ function pluginState(ctx, plugin) {
24669
+ if (!plugin.loadable)
24670
+ return state(ctx, false, "will not load");
24671
+ if (plugin.problems.length > 0)
24672
+ return paint(ctx, "yellow", "loads, with warnings");
24673
+ return state(ctx, true, "ok");
24674
+ }
24675
+ var pluginList = {
24676
+ usage: "plugin list",
24677
+ summary: "List installed plugins and whether the gateway would load each one",
24678
+ async run(_args, { ctx, writer }) {
24679
+ const root = ctx.root.root;
24680
+ const plugins = listPlugins(pluginDeps(), root);
24681
+ emit(ctx, writer, { pluginsDir: pluginsDir(root), plugins }, () => {
24682
+ if (plugins.length === 0) {
24683
+ return `no plugins installed; unpack one into ${pluginsDir(root)} with: omni plugin install <path-or-package>`;
24684
+ }
24685
+ const rows = plugins.map((plugin) => [
24686
+ plugin.id,
24687
+ plugin.name ?? "\u2014",
24688
+ plugin.version ?? "\u2014",
24689
+ plugin.api === null ? "\u2014" : String(plugin.api),
24690
+ plugin.sdk ?? "\u2014",
24691
+ plugin.capabilities.length === 0 ? "\u2014" : plugin.capabilities.join(","),
24692
+ pluginState(ctx, plugin)
24693
+ ]);
24694
+ const body2 = table([
24695
+ { header: "ID" },
24696
+ { header: "NAME" },
24697
+ { header: "VERSION" },
24698
+ { header: "API" },
24699
+ { header: "SDK" },
24700
+ { header: "CAPABILITIES" },
24701
+ { header: "STATE" }
24702
+ ], rows);
24703
+ const notes = plugins.flatMap((plugin) => plugin.problems.map((problem) => `${plugin.id}: ${paint(ctx, problem.fatal ? "red" : "yellow", problem.reason)}`));
24704
+ return notes.length === 0 ? body2 : `${body2}
24705
+
24706
+ ${notes.join(`
24707
+ `)}`;
24708
+ });
24709
+ }
24710
+ };
24711
+ var pluginVerify = {
24712
+ usage: "plugin verify <id>",
24713
+ summary: "Run every load-time check on one plugin without loading it",
24714
+ async run(args, { ctx, writer }) {
24715
+ const id = requirePositional(args, 0, "plugin id");
24716
+ const report = verifyPlugin(pluginDeps(), ctx.root.root, id);
24717
+ emit(ctx, writer, report, () => {
24718
+ const manifest = report.manifest;
24719
+ const head = fields([
24720
+ ["id", report.id],
24721
+ ["path", report.path],
24722
+ ["name", manifest?.name ?? "\u2014"],
24723
+ ["version", manifest?.version ?? "\u2014"],
24724
+ ["api", manifest === null ? "\u2014" : String(manifest.api)],
24725
+ ["sdk", manifest?.sdk ?? "\u2014"],
24726
+ ["server", manifest?.server ?? "\u2014"],
24727
+ ["ui", manifest?.ui ?? "\u2014"],
24728
+ [
24729
+ "capabilities",
24730
+ manifest === null || manifest.capabilities.length === 0 ? "\u2014" : manifest.capabilities.join(",")
24731
+ ],
24732
+ ["origins", manifest?.origins?.join(",") ?? "\u2014"]
24733
+ ]);
24734
+ if (report.problems.length === 0) {
24735
+ return `${head}
24736
+
24737
+ ${state(ctx, true, "ok")}: this plugin would load`;
24738
+ }
24739
+ const lines = report.problems.map((problem) => ` ${paint(ctx, problem.fatal ? "red" : "yellow", problem.check)} ${problem.reason}`);
24740
+ const verdict = report.loadable ? paint(ctx, "yellow", "this plugin would load, with the warnings above") : state(ctx, false, "this plugin would be skipped at boot");
24741
+ return `${head}
24742
+
24743
+ ${lines.join(`
24744
+ `)}
24745
+
24746
+ ${verdict}`;
24747
+ });
24748
+ if (!report.loadable)
24749
+ throw new CliError(`plugin "${id}" would not load`);
24750
+ }
24751
+ };
24752
+ var pluginInstall = {
24753
+ usage: "plugin install <path|tarball|https url|package[@version]> [--registry <url>]",
24754
+ summary: "Unpack a plugin into this installation, running nothing from it",
24755
+ options: { registry: { type: "string" } },
24756
+ async run(args, { ctx, writer }) {
24757
+ const spec = requirePositional(args, 0, "plugin directory, tarball, url, or package name");
24758
+ const result = await installPlugin(installDeps(ctx, args), ctx.root.root, spec);
24759
+ emit(ctx, writer, result, () => {
24760
+ note(ctx, writer, paint(ctx, "dim", "no code from the package was executed; verify it before restarting"));
24761
+ const verb = result.replaced ? "replaced" : "installed";
24762
+ return [
24763
+ `${verb} ${result.name} ${result.version} at ${result.path}`,
24764
+ paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart")
24765
+ ].join(`
24766
+ `);
24767
+ });
24768
+ }
24769
+ };
24770
+ var pluginRemove = {
24771
+ usage: "plugin remove <id> [--purge]",
24772
+ summary: "Remove a plugin, optionally dropping its stored data too",
24773
+ options: { purge: { type: "boolean" } },
24774
+ async run(args, { ctx, writer, prompt }) {
24775
+ const id = requirePositional(args, 0, "plugin id");
24776
+ const purge = boolFlag(args.values, "purge");
24777
+ const question = purge ? `remove plugin "${id}" AND permanently drop its stored data?` : `remove plugin "${id}"? its stored data is kept`;
24778
+ if (!await prompt.confirm(question))
24779
+ throw new CliError("cancelled");
24780
+ const deps = purge ? { fs: nodePluginFs(), store: await ctx.store() } : { fs: nodePluginFs() };
24781
+ const result = removePlugin(deps, ctx.root.root, id, { purge });
24782
+ emit(ctx, writer, result, () => {
24783
+ const lines = [];
24784
+ lines.push(result.removed ? `removed ${id}` : `no directory for ${id} to remove`);
24785
+ if (purge) {
24786
+ lines.push(result.droppedTables.length === 0 ? "no stored tables to drop" : `dropped ${result.droppedTables.length} table(s): ${result.droppedTables.join(", ")}`);
24787
+ } else {
24788
+ lines.push(paint(ctx, "dim", "stored tables and migration history kept; --purge drops them"));
24789
+ }
24790
+ lines.push(paint(ctx, "yellow", "restart the gateway for this to take effect: omni restart"));
24791
+ return lines.join(`
24792
+ `);
24793
+ });
24794
+ }
24795
+ };
24796
+ function doctorPluginDeps() {
24797
+ return pluginDeps();
24798
+ }
24799
+ function pluginDoctorLines(ctx, plugins) {
24800
+ return plugins.map((plugin) => {
24801
+ const version2 = plugin.version === null ? "" : ` ${plugin.version}`;
24802
+ const api2 = plugin.api === null ? "api ?" : `api ${plugin.api}`;
24803
+ const sdk = plugin.sdk === null ? "no ui" : `sdk ${plugin.sdk}`;
24804
+ const reasons = plugin.problems.filter((problem) => problem.fatal).map((p) => p.reason);
24805
+ const warnings = plugin.problems.length - reasons.length;
24806
+ const verdict = reasons.length > 0 ? state(ctx, false, `will not load: ${reasons.join("; ")}`) : warnings > 0 ? paint(ctx, "yellow", `loads, ${warnings} warning(s); see omni plugin verify ${plugin.id}`) : state(ctx, true, "ok");
24807
+ return `${plugin.id}${version2} (${api2}, ${sdk}) ${verdict}`;
24808
+ });
24809
+ }
23719
24810
 
23720
24811
  // apps/cli/src/commands/quota.ts
23721
24812
  var WINDOW_ORDER = {
@@ -23820,7 +24911,7 @@ import { existsSync as existsSync5 } from "fs";
23820
24911
 
23821
24912
  // apps/cli/src/runtime.ts
23822
24913
  import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
23823
- import { join as join6 } from "path";
24914
+ import { join as join7 } from "path";
23824
24915
  var runCommand = async (argv) => {
23825
24916
  const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
23826
24917
  const [stdout, stderr, code] = await Promise.all([
@@ -23877,9 +24968,9 @@ function processAlive(pid) {
23877
24968
  }
23878
24969
  function gatewayEntrypoint(root, cliDir = import.meta.dir) {
23879
24970
  const candidates = [
23880
- join6(root, "apps", "gateway", "src", "index.ts"),
23881
- join6(cliDir, "..", "gateway.js"),
23882
- join6(cliDir, "gateway.js")
24971
+ join7(root, "apps", "gateway", "src", "index.ts"),
24972
+ join7(cliDir, "..", "gateway.js"),
24973
+ join7(cliDir, "gateway.js")
23883
24974
  ];
23884
24975
  return candidates.find((path) => existsSync4(path)) ?? null;
23885
24976
  }
@@ -24016,6 +25107,15 @@ async function rollupState(ctx) {
24016
25107
  return null;
24017
25108
  }
24018
25109
  }
25110
+ async function orphanTables(ctx) {
25111
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
25112
+ return null;
25113
+ try {
25114
+ return orphanPluginTables(doctorPluginDeps(), ctx.root.root, await ctx.store());
25115
+ } catch {
25116
+ return null;
25117
+ }
25118
+ }
24019
25119
  var doctor = {
24020
25120
  usage: "doctor",
24021
25121
  summary: "Check what this CLI resolved, and whether it can do anything with it",
@@ -24026,6 +25126,8 @@ var doctor = {
24026
25126
  const unit = unitInstalled(deps);
24027
25127
  const status2 = await status(deps);
24028
25128
  const usageRollup = await rollupState(ctx);
25129
+ const plugins = listPlugins(doctorPluginDeps(), deps.root);
25130
+ const orphans = await orphanTables(ctx);
24029
25131
  const checks3 = {
24030
25132
  root: deps.root,
24031
25133
  rootSource: ctx.root.source,
@@ -24040,6 +25142,9 @@ var doctor = {
24040
25142
  supervisor: status2.supervisor,
24041
25143
  running: status2.running,
24042
25144
  consoleSource: consoleSource(deps).source,
25145
+ pluginsDir: pluginsDir(deps.root),
25146
+ plugins,
25147
+ orphanPluginTables: orphans,
24043
25148
  warnings: ctx.warnings
24044
25149
  };
24045
25150
  emit(ctx, writer, checks3, () => {
@@ -24061,6 +25166,11 @@ var doctor = {
24061
25166
  ["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
24062
25167
  ["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
24063
25168
  ["console log", sourceHint(checks3.consoleSource)],
25169
+ ...plugins.length === 0 ? [["plugins", paint(ctx, "dim", "none")]] : pluginDoctorLines(ctx, plugins).map((line, index) => [index === 0 ? "plugins" : "", line]),
25170
+ [
25171
+ "orphan plugin tables",
25172
+ orphans === null ? paint(ctx, "dim", "not checked") : orphans.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${orphans.length}: ${orphans.join(", ")}`)
25173
+ ],
24064
25174
  ...checks3.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
24065
25175
  ]);
24066
25176
  });
@@ -24141,7 +25251,7 @@ var settingsSet = {
24141
25251
  };
24142
25252
 
24143
25253
  // apps/cli/src/commands/setup.ts
24144
- import { join as join7 } from "path";
25254
+ import { join as join8 } from "path";
24145
25255
  var OPTIONS = {
24146
25256
  dir: { type: "string" },
24147
25257
  key: { type: "string" },
@@ -24173,7 +25283,7 @@ ${f.contents}`).join(`
24173
25283
  }
24174
25284
  }
24175
25285
  function at(dir, file2) {
24176
- return { path: join7(dir, file2.path), contents: file2.contents };
25286
+ return { path: join8(dir, file2.path), contents: file2.contents };
24177
25287
  }
24178
25288
  async function promptMapping(models, prompt) {
24179
25289
  const choices = models.map(({ model }) => model.id).join(", ");
@@ -24207,11 +25317,11 @@ var setupClaude = {
24207
25317
  options: OPTIONS,
24208
25318
  async run(args, { ctx, writer, prompt, setupFs }) {
24209
25319
  const models = await described(ctx);
24210
- const dir = stringFlag(args.values, "dir") ?? join7(setupFs.homeDir, ".claude");
25320
+ const dir = stringFlag(args.values, "dir") ?? join8(setupFs.homeDir, ".claude");
24211
25321
  const key = stringFlag(args.values, "key");
24212
25322
  const dryRun2 = boolFlag(args.values, "dry-run");
24213
25323
  const mapping = await promptMapping(models, prompt);
24214
- const path = join7(dir, "settings.json");
25324
+ const path = join8(dir, "settings.json");
24215
25325
  const file2 = claudeSettings(models, {
24216
25326
  baseUrl: baseUrl(ctx),
24217
25327
  discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
@@ -24466,6 +25576,10 @@ var COMMANDS = {
24466
25576
  "keys create": keysCreate,
24467
25577
  "keys limits": keysLimits,
24468
25578
  "keys revoke": keysRevoke,
25579
+ "plugin list": pluginList,
25580
+ "plugin verify": pluginVerify,
25581
+ "plugin install": pluginInstall,
25582
+ "plugin remove": pluginRemove,
24469
25583
  "settings get": settingsGet,
24470
25584
  "settings set": settingsSet,
24471
25585
  "setup claude": setupClaude,
@@ -24521,6 +25635,7 @@ var GROUPS = [
24521
25635
  { title: "Accounts", prefixes: ["connect", "credentials "] },
24522
25636
  { title: "Models", prefixes: ["models "] },
24523
25637
  { title: "Keys", prefixes: ["keys "] },
25638
+ { title: "Plugins", prefixes: ["plugin "] },
24524
25639
  { title: "Configuration", prefixes: ["settings ", "admin ", "db "] },
24525
25640
  { title: "Reports", prefixes: ["usage", "quota"] }
24526
25641
  ];
@@ -24577,10 +25692,10 @@ async function readHidden() {
24577
25692
  stdin.setRawMode?.(true);
24578
25693
  stdin.resume();
24579
25694
  try {
24580
- const decoder2 = new TextDecoder;
25695
+ const decoder3 = new TextDecoder;
24581
25696
  let line = "";
24582
25697
  for await (const chunk of stdin) {
24583
- const text = decoder2.decode(chunk, { stream: true });
25698
+ const text = decoder3.decode(chunk, { stream: true });
24584
25699
  for (const char of text) {
24585
25700
  if (char === "\r" || char === `
24586
25701
  `)
@@ -24601,10 +25716,10 @@ async function readHidden() {
24601
25716
  }
24602
25717
  }
24603
25718
  async function readLine() {
24604
- const decoder2 = new TextDecoder;
25719
+ const decoder3 = new TextDecoder;
24605
25720
  let line = "";
24606
25721
  for await (const chunk of process.stdin) {
24607
- const text = decoder2.decode(chunk, { stream: true });
25722
+ const text = decoder3.decode(chunk, { stream: true });
24608
25723
  const newline = text.indexOf(`
24609
25724
  `);
24610
25725
  if (newline >= 0)
@@ -24700,10 +25815,10 @@ async function run(argv, writer, options = {}) {
24700
25815
  setupFs: options.setupFs ?? {
24701
25816
  homeDir: homedir3(),
24702
25817
  cwd: process.cwd(),
24703
- read: (path) => existsSync6(path) ? readFileSync3(path, "utf8") : null,
25818
+ read: (path) => existsSync6(path) ? readFileSync4(path, "utf8") : null,
24704
25819
  write: (path, contents) => atomicWriteFile(path, contents, {
24705
25820
  mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
24706
- write: (temporary, data) => writeFileSync2(temporary, data),
25821
+ write: (temporary, data) => writeFileSync3(temporary, data),
24707
25822
  rename: renameSync2,
24708
25823
  remove: (temporary) => rmSync3(temporary, { force: true })
24709
25824
  })