@minniexcode/codex-switch 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ What it does:
18
18
  - Run diagnostics and detect local drift
19
19
  - List backups and roll back to a previous managed state
20
20
 
21
- Current version: `0.0.7`
21
+ Current version: `0.0.8`
22
22
 
23
23
  ## Install
24
24
 
@@ -1,6 +1,40 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.addProvider = addProvider;
37
+ const crypto = __importStar(require("node:crypto"));
4
38
  const config_1 = require("../domain/config");
5
39
  const providers_1 = require("../domain/providers");
6
40
  const errors_1 = require("../domain/errors");
@@ -8,6 +42,7 @@ const config_repo_1 = require("../storage/config-repo");
8
42
  const fs_utils_1 = require("../storage/fs-utils");
9
43
  const providers_repo_1 = require("../storage/providers-repo");
10
44
  const auth_repo_1 = require("../storage/auth-repo");
45
+ const copilot_installer_1 = require("../runtime/copilot-installer");
11
46
  const run_mutation_1 = require("./run-mutation");
12
47
  /**
13
48
  * Adds a new provider record to the managed providers registry.
@@ -18,6 +53,34 @@ function addProvider(args) {
18
53
  if (providers.providers[args.providerName]) {
19
54
  throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Provider "${args.providerName}" already exists.`);
20
55
  }
56
+ const bridgeHost = args.bridgeHost ?? "127.0.0.1";
57
+ const bridgePort = args.bridgePort ?? 4141;
58
+ const runtime = args.copilot
59
+ ? {
60
+ kind: "copilot-sdk-bridge",
61
+ upstream: "github-copilot",
62
+ bridgeHost,
63
+ bridgePort,
64
+ bridgePath: "/v1",
65
+ premiumRequests: true,
66
+ authSource: "official-sdk",
67
+ sdkInstallMode: "lazy",
68
+ }
69
+ : undefined;
70
+ if (args.copilot) {
71
+ const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
72
+ if (!installStatus.installed) {
73
+ if (!args.installCopilotSdk) {
74
+ throw (0, errors_1.cliError)(args.interactive ? "COPILOT_SDK_MISSING" : "COPILOT_SDK_INSTALL_REQUIRES_TTY", args.interactive
75
+ ? "The optional Copilot SDK runtime is not installed. Re-run with --install-copilot-sdk or confirm installation interactively."
76
+ : "The optional Copilot SDK runtime is not installed. Pass --install-copilot-sdk when running non-interactively.", {
77
+ installDir: installStatus.installDir,
78
+ packageName: installStatus.packageName,
79
+ });
80
+ }
81
+ (0, copilot_installer_1.installCopilotSdk)();
82
+ }
83
+ }
21
84
  const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
22
85
  const existingProfile = document.profiles.find((profile) => profile.name === args.profile);
23
86
  const existingModelProvider = document.modelProviders.find((entry) => entry.name === args.profile);
@@ -38,7 +101,7 @@ function addProvider(args) {
38
101
  const upsertModelProviders = !existingModelProvider && args.createProfile
39
102
  ? {
40
103
  [args.profile]: {
41
- baseUrl: args.baseUrl ?? undefined,
104
+ baseUrl: args.copilot ? (0, providers_1.buildCopilotBridgeBaseUrl)(runtime) : args.baseUrl ?? undefined,
42
105
  envKey: (0, config_1.buildManagedProfileEnvKey)(args.profile),
43
106
  },
44
107
  }
@@ -47,16 +110,19 @@ function addProvider(args) {
47
110
  (0, config_repo_1.requireManagedProfileRuntime)(document, providers, args.profile);
48
111
  }
49
112
  const envKey = existingModelProvider?.envKey ?? (0, config_1.buildManagedProfileEnvKey)(args.profile);
113
+ const apiKey = args.copilot ? args.bridgeApiKey ?? crypto.randomBytes(24).toString("hex") : args.apiKey;
114
+ const baseUrl = args.copilot ? (0, providers_1.buildCopilotBridgeBaseUrl)(runtime) : args.baseUrl ?? undefined;
50
115
  const next = {
51
116
  providers: {
52
117
  ...providers.providers,
53
118
  [args.providerName]: (0, providers_1.cleanProviderRecord)({
54
119
  profile: args.profile,
55
- apiKey: args.apiKey,
120
+ apiKey,
56
121
  envKey,
57
- baseUrl: args.baseUrl ?? undefined,
122
+ baseUrl,
58
123
  note: args.note ?? undefined,
59
124
  tags: args.tags,
125
+ runtime,
60
126
  }),
61
127
  },
62
128
  };
@@ -87,6 +153,7 @@ function addProvider(args) {
87
153
  provider: args.providerName,
88
154
  profile: args.profile,
89
155
  envKey,
156
+ runtimeKind: runtime?.kind ?? null,
90
157
  createdProfileSections: configPlan.createdProfileSections,
91
158
  createdModelProviderSections: configPlan.createdModelProviderSections,
92
159
  deletedProfileSections: configPlan.deletedProfileSections,
@@ -41,10 +41,13 @@ const providers_1 = require("../domain/providers");
41
41
  const config_repo_1 = require("../storage/config-repo");
42
42
  const providers_repo_1 = require("../storage/providers-repo");
43
43
  const auth_repo_1 = require("../storage/auth-repo");
44
+ const copilot_installer_1 = require("../runtime/copilot-installer");
45
+ const copilot_bridge_1 = require("../runtime/copilot-bridge");
46
+ const copilot_adapter_1 = require("../runtime/copilot-adapter");
44
47
  /**
45
48
  * Reports the current on-disk runtime state and how it maps back to managed providers.
46
49
  */
47
- function getStatus(codexDir, configPath, providersPath, authPath) {
50
+ async function getStatus(codexDir, configPath, providersPath, authPath) {
48
51
  const configExists = fs.existsSync(configPath);
49
52
  const providersExists = fs.existsSync(providersPath);
50
53
  let currentProfile = null;
@@ -64,6 +67,17 @@ function getStatus(codexDir, configPath, providersPath, authPath) {
64
67
  }
65
68
  const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
66
69
  const activeProviderCandidates = currentProfile && providers ? (0, providers_1.findProvidersByProfile)(providers, currentProfile) : [];
70
+ const activeProvider = activeProviderCandidates.length === 1 && providers ? providers.providers[activeProviderCandidates[0]] : null;
71
+ const copilotInstall = (0, copilot_installer_1.probeCopilotSdkInstall)();
72
+ const copilotBridge = activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider) ? await (0, copilot_bridge_1.probeCopilotBridgeRuntime)(activeProvider) : null;
73
+ const copilotAuth = activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider)
74
+ ? await (0, copilot_adapter_1.readCopilotAuthState)().catch((error) => ({
75
+ ready: false,
76
+ source: "official-sdk",
77
+ mode: "session",
78
+ error: error instanceof Error ? error.message : String(error),
79
+ }))
80
+ : null;
67
81
  if (liveState.canBackfillActiveProvider) {
68
82
  // Surface unmanaged live state without mutating anything during a read-only status call.
69
83
  warnings.push("Current config profile is not mapped in providers.json. Backfill would be required before treating live state as managed.");
@@ -80,6 +94,15 @@ function getStatus(codexDir, configPath, providersPath, authPath) {
80
94
  provider: liveState.mappedProvider,
81
95
  activeProviderResolvable: activeProviderCandidates.length === 1,
82
96
  activeProviderCandidates,
97
+ runtimeProvider: activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider) ? activeProvider.runtime?.kind ?? null : null,
98
+ copilotSdk: {
99
+ installed: copilotInstall.installed,
100
+ installDir: copilotInstall.installDir,
101
+ packageName: copilotInstall.packageName,
102
+ packageVersion: copilotInstall.packageVersion ?? null,
103
+ },
104
+ copilotAuth,
105
+ copilotBridge,
83
106
  liveState,
84
107
  auth: authState,
85
108
  configProfiles: configViews,
@@ -43,10 +43,13 @@ const errors_1 = require("../domain/errors");
43
43
  const codex_probe_1 = require("../runtime/codex-probe");
44
44
  const auth_repo_1 = require("../storage/auth-repo");
45
45
  const providers_1 = require("../domain/providers");
46
+ const copilot_installer_1 = require("../runtime/copilot-installer");
47
+ const copilot_bridge_1 = require("../runtime/copilot-bridge");
48
+ const copilot_adapter_1 = require("../runtime/copilot-adapter");
46
49
  /**
47
50
  * Performs consistency checks across config.toml, providers.json, and the local Codex CLI.
48
51
  */
49
- function runDoctor(args) {
52
+ async function runDoctor(args) {
50
53
  const issues = [];
51
54
  let currentProfile = null;
52
55
  let providers = null;
@@ -134,6 +137,38 @@ function runDoctor(args) {
134
137
  provider: matches[0],
135
138
  });
136
139
  }
140
+ if ((0, providers_1.isCopilotBridgeProvider)(activeProvider)) {
141
+ const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
142
+ if (!installStatus.installed) {
143
+ issues.push({
144
+ code: "COPILOT_SDK_MISSING",
145
+ message: "The optional Copilot SDK runtime is not installed.",
146
+ installDir: installStatus.installDir,
147
+ packageName: installStatus.packageName,
148
+ });
149
+ }
150
+ try {
151
+ await (0, copilot_adapter_1.readCopilotAuthState)();
152
+ }
153
+ catch (error) {
154
+ const normalized = (0, errors_1.normalizeError)(error);
155
+ issues.push({
156
+ code: normalized.code,
157
+ message: normalized.message,
158
+ ...(normalized.details ?? {}),
159
+ });
160
+ }
161
+ const bridge = await (0, copilot_bridge_1.probeCopilotBridgeRuntime)(activeProvider);
162
+ if (!bridge.ok) {
163
+ issues.push({
164
+ code: bridge.cause === "Copilot bridge state base URL does not match the provider runtime configuration."
165
+ ? "PROVIDER_BASE_URL_MISMATCH"
166
+ : "BRIDGE_HEALTHCHECK_FAILED",
167
+ message: bridge.cause,
168
+ ...(bridge.details ?? {}),
169
+ });
170
+ }
171
+ }
137
172
  }
138
173
  }
139
174
  // Drift inspection still runs when files are missing so status output can explain partial state.
@@ -49,7 +49,7 @@ const MIN_CODEX_VERSION = "0.0.1";
49
49
  /**
50
50
  * Migrates unmanaged Codex config profiles into a managed providers.json registry.
51
51
  */
52
- function migrateCodex(args) {
52
+ async function migrateCodex(args) {
53
53
  const available = (0, codex_cli_1.checkCodexAvailable)();
54
54
  if (!available.ok) {
55
55
  throw (0, errors_1.cliError)("CODEX_NOT_INSTALLED", "codex CLI is not available.", {
@@ -148,7 +148,7 @@ function migrateCodex(args) {
148
148
  },
149
149
  });
150
150
  // Re-run doctor on the final state so migrate returns immediate post-migration diagnostics.
151
- const doctor = (0, run_doctor_1.runDoctor)({
151
+ const doctor = await (0, run_doctor_1.runDoctor)({
152
152
  codexDir: args.codexDir,
153
153
  configPath: args.configPath,
154
154
  providersPath: args.providersPath,
@@ -2,14 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.switchProvider = switchProvider;
4
4
  const errors_1 = require("../domain/errors");
5
+ const providers_1 = require("../domain/providers");
5
6
  const config_repo_1 = require("../storage/config-repo");
6
7
  const providers_repo_1 = require("../storage/providers-repo");
7
8
  const auth_repo_1 = require("../storage/auth-repo");
9
+ const copilot_bridge_1 = require("../runtime/copilot-bridge");
10
+ const copilot_installer_1 = require("../runtime/copilot-installer");
11
+ const copilot_adapter_1 = require("../runtime/copilot-adapter");
8
12
  const run_mutation_1 = require("./run-mutation");
9
13
  /**
10
14
  * Switches the active Codex profile and rewrites auth.json for the target provider.
11
15
  */
12
- function switchProvider(args) {
16
+ async function switchProvider(args) {
13
17
  const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
14
18
  const provider = providers.providers[args.providerName];
15
19
  if (!provider) {
@@ -27,6 +31,48 @@ function switchProvider(args) {
27
31
  runtimeEnvKey: envKey,
28
32
  });
29
33
  }
34
+ if ((0, providers_1.isCopilotBridgeProvider)(provider)) {
35
+ const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
36
+ if (!installStatus.installed) {
37
+ throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", {
38
+ installDir: installStatus.installDir,
39
+ packageName: installStatus.packageName,
40
+ });
41
+ }
42
+ await (0, copilot_adapter_1.readCopilotAuthState)();
43
+ const bridge = await (0, copilot_bridge_1.ensureCopilotBridge)(args.providerName, provider);
44
+ try {
45
+ return (0, run_mutation_1.runMutation)({
46
+ codexDir: args.codexDir,
47
+ backupsDir: args.backupsDir,
48
+ latestBackupPath: args.latestBackupPath,
49
+ operation: "switch",
50
+ files: [
51
+ { absolutePath: args.configPath, relativePath: "config.toml" },
52
+ { absolutePath: args.authPath, relativePath: "auth.json" },
53
+ ],
54
+ mutate: () => {
55
+ const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
56
+ setActiveProfile: provider.profile,
57
+ });
58
+ (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
59
+ const existingAuth = (0, auth_repo_1.readAuthFileIfExists)(args.authPath);
60
+ (0, auth_repo_1.writeAuthFile)(args.authPath, provider, existingAuth ?? undefined);
61
+ return {
62
+ provider: args.providerName,
63
+ profile: provider.profile,
64
+ envKey: provider.envKey,
65
+ };
66
+ },
67
+ });
68
+ }
69
+ catch (error) {
70
+ if (!bridge.reused) {
71
+ (0, copilot_bridge_1.stopCopilotBridge)();
72
+ }
73
+ throw error;
74
+ }
75
+ }
30
76
  return (0, run_mutation_1.runMutation)({
31
77
  codexDir: args.codexDir,
32
78
  backupsDir: args.backupsDir,
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ const args_1 = require("./commands/args");
9
9
  const help_1 = require("./commands/help");
10
10
  const errors_1 = require("./domain/errors");
11
11
  const output_1 = require("./cli/output");
12
- const VERSION = "0.0.7";
12
+ const VERSION = "0.0.8";
13
13
  /**
14
14
  * Prints the command help text to stdout.
15
15
  */
@@ -58,6 +58,7 @@ const providers_1 = require("../domain/providers");
58
58
  const add_interactive_1 = require("../interaction/add-interactive");
59
59
  const interactive_1 = require("../interaction/interactive");
60
60
  const prompt_1 = require("../interaction/prompt");
61
+ const copilot_installer_1 = require("../runtime/copilot-installer");
61
62
  const config_repo_1 = require("../storage/config-repo");
62
63
  const codex_paths_1 = require("../storage/codex-paths");
63
64
  const providers_repo_1 = require("../storage/providers-repo");
@@ -155,6 +156,9 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
155
156
  if (!providerName) {
156
157
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
157
158
  }
159
+ if ((0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk")) {
160
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is only supported with add --copilot.");
161
+ }
158
162
  return (0, switch_provider_1.switchProvider)({
159
163
  codexDir: paths.codexDir,
160
164
  backupsDir: paths.backupsDir,
@@ -221,7 +225,22 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
221
225
  let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false);
222
226
  let tags = parsed.commandOptions.get("--tag") ?? [];
223
227
  let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
224
- if (!providerName || !profile || !apiKey) {
228
+ const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
229
+ const bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
230
+ const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
231
+ const bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
232
+ let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
233
+ const bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
234
+ if (copilot && apiKey) {
235
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
236
+ }
237
+ if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
238
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
239
+ }
240
+ if (copilot && !installCopilotSdk && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && !(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
241
+ installCopilotSdk = await runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
242
+ }
243
+ if (!providerName || !profile || (!apiKey && !copilot)) {
225
244
  if (ctx.options.json || !runtime.isInteractive()) {
226
245
  throw (0, add_interactive_1.createNonInteractiveAddError)();
227
246
  }
@@ -251,12 +270,18 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
251
270
  authPath: paths.authPath,
252
271
  providerName,
253
272
  profile,
254
- apiKey,
273
+ apiKey: apiKey ?? "",
255
274
  baseUrl,
256
275
  model,
257
276
  note,
258
277
  tags,
259
278
  createProfile,
279
+ copilot,
280
+ bridgeHost,
281
+ bridgePort,
282
+ bridgeApiKey,
283
+ installCopilotSdk,
284
+ interactive: (0, interactive_1.canPrompt)(runtime, ctx.options.json),
260
285
  });
261
286
  }
262
287
  case "edit": {
@@ -128,6 +128,7 @@ exports.COMMANDS = [
128
128
  usage: ["codexs status [--json] [--codex-dir <path>]"],
129
129
  details: [
130
130
  "Reports file presence, current profile, and whether the live profile is mapped.",
131
+ "When the active provider uses a local runtime bridge, status also reports bridge and SDK state.",
131
132
  "Surfaces config consistency signals without mutating any files.",
132
133
  "Use doctor for deeper diagnostics.",
133
134
  ],
@@ -160,6 +161,7 @@ exports.COMMANDS = [
160
161
  summary: "Add a provider with explicit flags or progressive TTY prompts.",
161
162
  usage: [
162
163
  "codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
164
+ "codexs add <provider> --copilot --profile <name> [--bridge-host <host>] [--bridge-port <port>] [--bridge-api-key <secret>] [--install-copilot-sdk]",
163
165
  "codexs add <provider> --profile <name> --api-key <key> --create-profile --model <name> --base-url <url>",
164
166
  "codexs add [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...]",
165
167
  ],
@@ -170,8 +172,13 @@ exports.COMMANDS = [
170
172
  "Interactive tags use preset multi-select only.",
171
173
  "Automation and non-TTY environments must pass all required values explicitly.",
172
174
  "Creating a missing profile section requires --create-profile together with --model and --base-url.",
175
+ "Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
176
+ ],
177
+ examples: [
178
+ "codexs add packycode --profile packycode --api-key sk-xxx",
179
+ "codexs add copilot-main --copilot --profile copilot-main --install-copilot-sdk",
180
+ "codexs add",
173
181
  ],
174
- examples: ["codexs add packycode --profile packycode --api-key sk-xxx", "codexs add packycode --profile packycode", "codexs add"],
175
182
  },
176
183
  {
177
184
  id: "switch",
@@ -184,6 +191,7 @@ exports.COMMANDS = [
184
191
  "When <provider> is omitted in a TTY, an interactive provider selector is shown.",
185
192
  "When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
186
193
  "Switch updates the active config profile and rewrites auth.json from the provider envKey/apiKey pair.",
194
+ "Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
187
195
  "Backs up config.toml and auth.json, then rolls back on failure.",
188
196
  ],
189
197
  examples: ["codexs switch freemodel", "codexs switch packycode --json"],
@@ -252,7 +260,11 @@ exports.COMMANDS = [
252
260
  group: "recovery",
253
261
  summary: "Run configuration and environment diagnostics.",
254
262
  usage: ["codexs doctor [--json] [--codex-dir <path>]"],
255
- details: ["Checks the expected config files, provider/profile consistency, and Codex CLI availability.", "Returns structured issues so users and AI agents can act on them."],
263
+ details: [
264
+ "Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
265
+ "Copilot bridge providers add runtime dependency, auth, and bridge health diagnostics.",
266
+ "Returns structured issues so users and AI agents can act on them.",
267
+ ],
256
268
  examples: ["codexs doctor", "codexs doctor --json"],
257
269
  },
258
270
  {
@@ -6,6 +6,9 @@ exports.sortProviders = sortProviders;
6
6
  exports.findProviderByProfile = findProviderByProfile;
7
7
  exports.findProvidersByProfile = findProvidersByProfile;
8
8
  exports.maskSecret = maskSecret;
9
+ exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
10
+ exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
11
+ exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
9
12
  /**
10
13
  * Validates and normalizes unknown JSON into the providers.json domain model.
11
14
  */
@@ -42,6 +45,13 @@ function validateProvidersShape(input) {
42
45
  (!Array.isArray(provider.tags) || provider.tags.some((tag) => typeof tag !== "string"))) {
43
46
  throw new Error(`Provider "${name}" has invalid tags.`);
44
47
  }
48
+ if (provider.runtime !== undefined) {
49
+ validateProviderRuntime(name, provider.runtime);
50
+ const expectedBaseUrl = buildCopilotBridgeBaseUrl(provider.runtime);
51
+ if (typeof provider.baseUrl !== "string" || provider.baseUrl.trim() !== expectedBaseUrl) {
52
+ throw new Error(`Provider "${name}" baseUrl must match runtime bridge base URL "${expectedBaseUrl}".`);
53
+ }
54
+ }
45
55
  // Normalize provider fields during validation so the persisted format stays clean.
46
56
  providers[name] = cleanProviderRecord({
47
57
  profile: provider.profile,
@@ -50,6 +60,7 @@ function validateProvidersShape(input) {
50
60
  baseUrl: provider.baseUrl,
51
61
  note: provider.note,
52
62
  tags: provider.tags,
63
+ runtime: provider.runtime,
53
64
  });
54
65
  }
55
66
  return { providers };
@@ -72,6 +83,18 @@ function cleanProviderRecord(record) {
72
83
  if (record.tags && record.tags.length > 0) {
73
84
  next.tags = record.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
74
85
  }
86
+ if (record.runtime) {
87
+ next.runtime = {
88
+ kind: record.runtime.kind,
89
+ upstream: record.runtime.upstream,
90
+ bridgeHost: record.runtime.bridgeHost.trim(),
91
+ bridgePort: record.runtime.bridgePort,
92
+ bridgePath: record.runtime.bridgePath,
93
+ premiumRequests: record.runtime.premiumRequests,
94
+ authSource: record.runtime.authSource,
95
+ sdkInstallMode: record.runtime.sdkInstallMode,
96
+ };
97
+ }
75
98
  return next;
76
99
  }
77
100
  /**
@@ -114,3 +137,54 @@ function maskSecret(value) {
114
137
  }
115
138
  return `${value.slice(0, 3)}***${value.slice(-2)}`;
116
139
  }
140
+ /**
141
+ * Returns whether one provider record relies on an auxiliary runtime component.
142
+ */
143
+ function isRuntimeBackedProvider(provider) {
144
+ return Boolean(provider.runtime);
145
+ }
146
+ /**
147
+ * Returns whether one provider uses the GitHub Copilot SDK bridge runtime.
148
+ */
149
+ function isCopilotBridgeProvider(provider) {
150
+ return provider.runtime?.kind === "copilot-sdk-bridge";
151
+ }
152
+ /**
153
+ * Builds the canonical local bridge URL for one Copilot runtime provider.
154
+ */
155
+ function buildCopilotBridgeBaseUrl(runtime) {
156
+ return `http://${runtime.bridgeHost}:${runtime.bridgePort}${runtime.bridgePath}`;
157
+ }
158
+ /**
159
+ * Validates one runtime-backed provider block.
160
+ */
161
+ function validateProviderRuntime(name, runtime) {
162
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
163
+ throw new Error(`Provider "${name}" has an invalid runtime block.`);
164
+ }
165
+ const record = runtime;
166
+ if (record.kind !== "copilot-sdk-bridge") {
167
+ throw new Error(`Provider "${name}" has an unsupported runtime kind.`);
168
+ }
169
+ if (record.upstream !== "github-copilot") {
170
+ throw new Error(`Provider "${name}" has an invalid runtime upstream.`);
171
+ }
172
+ if (typeof record.bridgeHost !== "string" || record.bridgeHost.trim() === "") {
173
+ throw new Error(`Provider "${name}" has an invalid runtime bridgeHost.`);
174
+ }
175
+ if (typeof record.bridgePort !== "number" || !Number.isInteger(record.bridgePort) || record.bridgePort <= 0) {
176
+ throw new Error(`Provider "${name}" has an invalid runtime bridgePort.`);
177
+ }
178
+ if (record.bridgePath !== "/v1") {
179
+ throw new Error(`Provider "${name}" has an invalid runtime bridgePath.`);
180
+ }
181
+ if (record.premiumRequests !== true) {
182
+ throw new Error(`Provider "${name}" must enable runtime premiumRequests.`);
183
+ }
184
+ if (record.authSource !== "official-sdk") {
185
+ throw new Error(`Provider "${name}" has an invalid runtime authSource.`);
186
+ }
187
+ if (record.sdkInstallMode !== "lazy") {
188
+ throw new Error(`Provider "${name}" has an invalid runtime sdkInstallMode.`);
189
+ }
190
+ }