@switchboard-mcp/core 0.1.0

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 (74) hide show
  1. package/README.md +9 -0
  2. package/dist/approvals/approval-requests.d.ts +151 -0
  3. package/dist/approvals/approval-requests.d.ts.map +1 -0
  4. package/dist/approvals/approval-requests.js +393 -0
  5. package/dist/approvals/approval-requests.js.map +1 -0
  6. package/dist/audit/audit-log.d.ts +45 -0
  7. package/dist/audit/audit-log.d.ts.map +1 -0
  8. package/dist/audit/audit-log.js +88 -0
  9. package/dist/audit/audit-log.js.map +1 -0
  10. package/dist/config/gitignore.d.ts +9 -0
  11. package/dist/config/gitignore.d.ts.map +1 -0
  12. package/dist/config/gitignore.js +90 -0
  13. package/dist/config/gitignore.js.map +1 -0
  14. package/dist/config/load-config.d.ts +28 -0
  15. package/dist/config/load-config.d.ts.map +1 -0
  16. package/dist/config/load-config.js +138 -0
  17. package/dist/config/load-config.js.map +1 -0
  18. package/dist/config/paths.d.ts +13 -0
  19. package/dist/config/paths.d.ts.map +1 -0
  20. package/dist/config/paths.js +38 -0
  21. package/dist/config/paths.js.map +1 -0
  22. package/dist/daemon/daemon-state.d.ts +44 -0
  23. package/dist/daemon/daemon-state.d.ts.map +1 -0
  24. package/dist/daemon/daemon-state.js +110 -0
  25. package/dist/daemon/daemon-state.js.map +1 -0
  26. package/dist/import/import-plan.d.ts +92 -0
  27. package/dist/import/import-plan.d.ts.map +1 -0
  28. package/dist/import/import-plan.js +677 -0
  29. package/dist/import/import-plan.js.map +1 -0
  30. package/dist/index.d.ts +18 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/install/client-config.d.ts +73 -0
  35. package/dist/install/client-config.d.ts.map +1 -0
  36. package/dist/install/client-config.js +514 -0
  37. package/dist/install/client-config.js.map +1 -0
  38. package/dist/mandates/mandates.d.ts +244 -0
  39. package/dist/mandates/mandates.d.ts.map +1 -0
  40. package/dist/mandates/mandates.js +690 -0
  41. package/dist/mandates/mandates.js.map +1 -0
  42. package/dist/namespaces/namespaces.d.ts +16 -0
  43. package/dist/namespaces/namespaces.d.ts.map +1 -0
  44. package/dist/namespaces/namespaces.js +49 -0
  45. package/dist/namespaces/namespaces.js.map +1 -0
  46. package/dist/onboarding/init-config.d.ts +19 -0
  47. package/dist/onboarding/init-config.d.ts.map +1 -0
  48. package/dist/onboarding/init-config.js +85 -0
  49. package/dist/onboarding/init-config.js.map +1 -0
  50. package/dist/providers/provider-add.d.ts +25 -0
  51. package/dist/providers/provider-add.d.ts.map +1 -0
  52. package/dist/providers/provider-add.js +179 -0
  53. package/dist/providers/provider-add.js.map +1 -0
  54. package/dist/providers/provider-templates.d.ts +96 -0
  55. package/dist/providers/provider-templates.d.ts.map +1 -0
  56. package/dist/providers/provider-templates.js +556 -0
  57. package/dist/providers/provider-templates.js.map +1 -0
  58. package/dist/scan/scan.d.ts +67 -0
  59. package/dist/scan/scan.d.ts.map +1 -0
  60. package/dist/scan/scan.js +389 -0
  61. package/dist/scan/scan.js.map +1 -0
  62. package/dist/schemas/config.d.ts +197 -0
  63. package/dist/schemas/config.d.ts.map +1 -0
  64. package/dist/schemas/config.js +133 -0
  65. package/dist/schemas/config.js.map +1 -0
  66. package/dist/secrets/secret-refs.d.ts +10 -0
  67. package/dist/secrets/secret-refs.d.ts.map +1 -0
  68. package/dist/secrets/secret-refs.js +32 -0
  69. package/dist/secrets/secret-refs.js.map +1 -0
  70. package/dist/secrets/secrets.d.ts +63 -0
  71. package/dist/secrets/secrets.d.ts.map +1 -0
  72. package/dist/secrets/secrets.js +301 -0
  73. package/dist/secrets/secrets.js.map +1 -0
  74. package/package.json +42 -0
@@ -0,0 +1,677 @@
1
+ import { constants, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import { deepMerge, loadSwitchboardConfig } from "../config/load-config.js";
6
+ import { resolveRepoConfigPaths } from "../config/paths.js";
7
+ import { resolveProjectClientConfigPath } from "../install/client-config.js";
8
+ import { normalizeNamespace } from "../namespaces/namespaces.js";
9
+ import { switchboardConfigSchema } from "../schemas/config.js";
10
+ export const importPlanSchemaVersion = "switchboard.import-plan.v1";
11
+ const providerEnvPrefixes = {
12
+ github: [/^GITHUB_/i, /^GH_TOKEN$/i, /^GH_/i],
13
+ vercel: [/^VERCEL_/i],
14
+ stripe: [/^STRIPE_/i],
15
+ supabase: [/^SUPABASE_/i],
16
+ posthog: [/^POSTHOG_/i, /^NEXT_PUBLIC_POSTHOG_/i]
17
+ };
18
+ export async function createSwitchboardImportPlan(options = {}) {
19
+ const cwd = resolve(options.cwd ?? process.cwd());
20
+ const repoName = basename(cwd);
21
+ const loaded = loadSwitchboardConfig({ cwd });
22
+ const profileNames = new Set(Object.keys(loaded.config.profiles));
23
+ const switchboardProfiles = Object.entries(loaded.config.profiles).map(([name, profile]) => ({
24
+ name,
25
+ provider: profile.provider ?? null,
26
+ namespace: profile.namespace ?? null
27
+ }));
28
+ const clients = detectClientConfigs(cwd, repoName);
29
+ const envFiles = detectEnvFiles(cwd);
30
+ const detectedServers = clients.flatMap((client) => client.servers.filter((server) => server.name !== "switchboard"));
31
+ const createProfileActions = uniqueServersByProfile(detectedServers)
32
+ .filter((server) => !profileNames.has(server.suggestedProfileName))
33
+ .map((server) => createProfileAction(server));
34
+ const secretActions = uniqueSecretRefs(detectedServers).map((secret) => ({
35
+ kind: "store-secret",
36
+ title: `Store ${secret.envName} as ${secret.ref}`,
37
+ status: "planned",
38
+ provider: providerFromEnvName(secret.envName),
39
+ command: {
40
+ command: "switchboard",
41
+ args: ["secrets", "set", secret.ref, "--value-stdin"]
42
+ },
43
+ reason: "The existing MCP config references this secret-looking env name; Switchboard will keep values out of repo and client config."
44
+ }));
45
+ const clientsMissingSwitchboard = clients.filter((client) => client.status !== "detected" ||
46
+ !client.servers.some((server) => server.routesThroughSwitchboard));
47
+ const installActions = clientsMissingSwitchboard
48
+ .map((client) => ({
49
+ kind: "install-client",
50
+ title: `Route ${client.client} through Switchboard MCP`,
51
+ status: "planned",
52
+ client: client.client,
53
+ command: {
54
+ command: "switchboard",
55
+ args: ["install", client.client, "--write"]
56
+ },
57
+ reason: "Importing profiles is separate from client installation so existing client config stays reversible."
58
+ }));
59
+ const reviewActions = switchboardProfiles.map((profile) => ({
60
+ kind: "review-existing-profile",
61
+ title: `Review existing Switchboard profile ${profile.name}`,
62
+ status: "already-configured",
63
+ profileName: profile.name,
64
+ provider: providerFromProfile(profile.provider),
65
+ reason: "Existing Switchboard profiles are preserved; import dry-run only plans around them."
66
+ }));
67
+ const secretCommands = secretActions.map((action) => action.command);
68
+ const installClients = clientsMissingSwitchboard.map((client) => ({
69
+ command: "switchboard",
70
+ args: ["install", client.client, "--write"]
71
+ }));
72
+ const actions = [
73
+ ...createProfileActions,
74
+ ...secretActions,
75
+ ...installActions,
76
+ ...reviewActions
77
+ ];
78
+ const warnings = buildWarnings({ clients, envFiles, detectedServers });
79
+ const safetyNotes = [
80
+ "Dry run only: this command does not write .switchboard.yaml or client config.",
81
+ "Secret values are never read from env files or client config; only env variable names are reported.",
82
+ "Imported MCP servers should be treated as setup candidates until doctor and preset checks pass.",
83
+ "Run client install separately so existing Codex/Claude config remains backup-protected and reversible."
84
+ ];
85
+ const nextActions = [
86
+ "switchboard import --dry-run",
87
+ ...secretCommands.map((command) => renderCommand(command)),
88
+ ...installClients.map((command) => renderCommand(command))
89
+ ];
90
+ return {
91
+ ok: true,
92
+ schemaVersion: importPlanSchemaVersion,
93
+ mode: "dry-run",
94
+ repo: { cwd, name: repoName },
95
+ detected: {
96
+ clients,
97
+ switchboardProfiles,
98
+ envFiles
99
+ },
100
+ actions,
101
+ commands: {
102
+ dryRun: { command: "switchboard", args: ["import", "--dry-run"] },
103
+ writePreview: { command: "switchboard", args: ["import", "--write"] },
104
+ installClients,
105
+ secretCommands
106
+ },
107
+ warnings,
108
+ safetyNotes,
109
+ nextActions: [...new Set(nextActions)]
110
+ };
111
+ }
112
+ export async function writeSwitchboardImportPlan(options = {}) {
113
+ const plan = await createSwitchboardImportPlan(options);
114
+ const cwd = plan.repo.cwd;
115
+ const targetPath = resolveRepoConfigPaths({ cwd }).repoConfigPath ?? join(cwd, ".switchboard.yaml");
116
+ const importableServers = uniqueServersByProfile(plan.detected.clients.flatMap((client) => client.servers.filter((server) => !server.routesThroughSwitchboard &&
117
+ server.command !== null &&
118
+ !plan.detected.switchboardProfiles.some((profile) => profile.name === server.suggestedProfileName))));
119
+ if (importableServers.length === 0) {
120
+ return {
121
+ ok: true,
122
+ schemaVersion: importPlanSchemaVersion,
123
+ action: "noop",
124
+ targetPath,
125
+ backupPath: null,
126
+ plan,
127
+ createdProfiles: [],
128
+ nextContent: null
129
+ };
130
+ }
131
+ assertNoNamespaceCollisions(plan, importableServers);
132
+ const existing = await readOptionalTextFileAsync(targetPath);
133
+ const nextContent = renderMergedImportConfig(existing, importableServers);
134
+ await mkdir(dirname(targetPath), { recursive: true });
135
+ const backupPath = existing
136
+ ? await backupExistingFile(targetPath, options.now)
137
+ : null;
138
+ await writeFile(targetPath, nextContent, "utf8");
139
+ return {
140
+ ok: true,
141
+ schemaVersion: importPlanSchemaVersion,
142
+ action: existing ? "updated" : "created",
143
+ targetPath,
144
+ backupPath,
145
+ plan,
146
+ createdProfiles: importableServers.map((server) => server.suggestedProfileName),
147
+ nextContent
148
+ };
149
+ }
150
+ function assertNoNamespaceCollisions(plan, servers) {
151
+ const existing = new Map();
152
+ for (const profile of plan.detected.switchboardProfiles) {
153
+ existing.set(normalizeNamespace(profile.namespace ?? profile.name), profile.name);
154
+ }
155
+ const planned = new Map();
156
+ for (const server of servers) {
157
+ const namespace = normalizeNamespace(server.suggestedNamespace);
158
+ const existingProfile = existing.get(namespace);
159
+ if (existingProfile && existingProfile !== server.suggestedProfileName) {
160
+ throw new Error(`namespace "${namespace}" would collide with existing profile "${existingProfile}"`);
161
+ }
162
+ const plannedProfile = planned.get(namespace);
163
+ if (plannedProfile && plannedProfile !== server.suggestedProfileName) {
164
+ throw new Error(`namespace "${namespace}" would collide between planned profiles "${plannedProfile}" and "${server.suggestedProfileName}"`);
165
+ }
166
+ planned.set(namespace, server.suggestedProfileName);
167
+ }
168
+ }
169
+ function renderMergedImportConfig(existingContent, servers) {
170
+ const rendered = renderImportConfig(servers);
171
+ if (!existingContent?.trim()) {
172
+ return `${stringifyYaml(rendered, { lineWidth: 0 })}`;
173
+ }
174
+ const existing = parseConfigYaml(existingContent, ".switchboard.yaml");
175
+ const merged = deepMerge(existing, rendered);
176
+ const parsed = switchboardConfigSchema.safeParse(merged);
177
+ if (!parsed.success) {
178
+ throw new Error(parsed.error.issues.map((issue) => issue.message).join("\n"));
179
+ }
180
+ return `${stringifyYaml(ensureDefaultWorkspaceProfiles(parsed.data), {
181
+ lineWidth: 0
182
+ })}`;
183
+ }
184
+ function renderImportConfig(servers) {
185
+ const profiles = Object.fromEntries(servers.map((server) => [
186
+ server.suggestedProfileName,
187
+ {
188
+ provider: server.provider === "unknown" ? "generic" : server.provider,
189
+ namespace: server.suggestedNamespace,
190
+ readOnly: false,
191
+ mode: "guarded",
192
+ enforcement: "switchboard",
193
+ upstream: {
194
+ type: "stdio",
195
+ command: server.command ?? "",
196
+ ...(server.args.length > 0 ? { args: server.args } : {}),
197
+ ...renderImportedEnv(server)
198
+ }
199
+ }
200
+ ]));
201
+ return {
202
+ version: 1,
203
+ defaults: {},
204
+ profiles,
205
+ workspaces: {
206
+ default: {
207
+ paths: ["."],
208
+ profiles: servers.map((server) => server.suggestedProfileName)
209
+ }
210
+ },
211
+ policies: {}
212
+ };
213
+ }
214
+ function renderImportedEnv(server) {
215
+ const env = Object.fromEntries(server.suggestedSecretRefs.map((secret) => [
216
+ secret.envName,
217
+ { secretRef: secret.ref }
218
+ ]));
219
+ return Object.keys(env).length > 0 ? { env } : {};
220
+ }
221
+ function ensureDefaultWorkspaceProfiles(config) {
222
+ const defaultWorkspace = config.workspaces.default;
223
+ if (!defaultWorkspace) {
224
+ return config;
225
+ }
226
+ return {
227
+ ...config,
228
+ workspaces: {
229
+ ...config.workspaces,
230
+ default: {
231
+ ...defaultWorkspace,
232
+ paths: unique(defaultWorkspace.paths),
233
+ profiles: unique(defaultWorkspace.profiles)
234
+ }
235
+ }
236
+ };
237
+ }
238
+ function parseConfigYaml(content, label) {
239
+ const parsed = content.trim().length === 0 ? {} : parseYaml(content);
240
+ if (parsed === null || parsed === undefined) {
241
+ return {};
242
+ }
243
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
244
+ throw new Error(`${label} must be a YAML mapping`);
245
+ }
246
+ return parsed;
247
+ }
248
+ function uniqueServersByProfile(servers) {
249
+ const byProfile = new Map();
250
+ for (const server of servers) {
251
+ byProfile.set(server.suggestedProfileName, server);
252
+ }
253
+ return [...byProfile.values()];
254
+ }
255
+ function detectClientConfigs(cwd, repoName) {
256
+ return [
257
+ detectCodexConfig(resolveProjectClientConfigPath("codex", cwd), repoName),
258
+ detectClaudeConfig(resolveProjectClientConfigPath("claude", cwd), repoName)
259
+ ];
260
+ }
261
+ function detectCodexConfig(targetPath, repoName) {
262
+ const content = readOptionalTextFile(targetPath);
263
+ if (content === null) {
264
+ return missingClient("codex", targetPath);
265
+ }
266
+ try {
267
+ return {
268
+ client: "codex",
269
+ targetPath,
270
+ status: "detected",
271
+ message: "Codex project MCP config was found.",
272
+ servers: codexServerSections(content).map((section) => detectedServerFromEntry({
273
+ repoName,
274
+ name: section.name,
275
+ command: parseTomlString(section.assignments.command),
276
+ args: parseTomlStringArray(section.assignments.args),
277
+ envKeys: [
278
+ ...parseTomlInlineObjectKeys(section.assignments.env),
279
+ ...section.envKeys
280
+ ]
281
+ }))
282
+ };
283
+ }
284
+ catch (error) {
285
+ return invalidClient("codex", targetPath, error);
286
+ }
287
+ }
288
+ function detectClaudeConfig(targetPath, repoName) {
289
+ const content = readOptionalTextFile(targetPath);
290
+ if (content === null) {
291
+ return missingClient("claude", targetPath);
292
+ }
293
+ try {
294
+ const parsed = JSON.parse(content);
295
+ const mcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
296
+ return {
297
+ client: "claude",
298
+ targetPath,
299
+ status: "detected",
300
+ message: "Claude project MCP config was found.",
301
+ servers: Object.entries(mcpServers).map(([name, value]) => detectedServerFromEntry({
302
+ repoName,
303
+ name,
304
+ command: isRecord(value) && typeof value.command === "string"
305
+ ? value.command
306
+ : null,
307
+ args: isRecord(value) && Array.isArray(value.args)
308
+ ? value.args.filter((arg) => typeof arg === "string")
309
+ : [],
310
+ envKeys: isRecord(value) && isRecord(value.env)
311
+ ? Object.keys(value.env).sort()
312
+ : []
313
+ }))
314
+ };
315
+ }
316
+ catch (error) {
317
+ return invalidClient("claude", targetPath, error);
318
+ }
319
+ }
320
+ function missingClient(client, targetPath) {
321
+ return {
322
+ client,
323
+ targetPath,
324
+ status: "missing",
325
+ message: `${client} project MCP config was not found.`,
326
+ servers: []
327
+ };
328
+ }
329
+ function invalidClient(client, targetPath, error) {
330
+ return {
331
+ client,
332
+ targetPath,
333
+ status: "invalid",
334
+ message: messageFromError(error),
335
+ servers: []
336
+ };
337
+ }
338
+ function detectedServerFromEntry(options) {
339
+ const routesThroughSwitchboard = options.name === "switchboard";
340
+ const provider = routesThroughSwitchboard
341
+ ? "unknown"
342
+ : inferProvider({
343
+ name: options.name,
344
+ command: options.command,
345
+ args: options.args,
346
+ envKeys: options.envKeys
347
+ });
348
+ const environment = inferEnvironment(options);
349
+ const profileBase = provider === "unknown" ? options.name : `${provider}_${options.repoName}`;
350
+ const suggestedProfileName = safeIdentifier(environment === "unknown" ? profileBase : `${profileBase}_${environment}`);
351
+ return {
352
+ name: options.name,
353
+ routesThroughSwitchboard,
354
+ command: options.command,
355
+ args: options.args,
356
+ envKeys: [...new Set(options.envKeys)].sort(),
357
+ provider,
358
+ suggestedProfileName,
359
+ suggestedNamespace: suggestedProfileName,
360
+ suggestedSecretRefs: options.envKeys
361
+ .filter(isSecretLikeName)
362
+ .map((envName) => ({
363
+ envName,
364
+ ref: `${provider === "unknown" ? "mcp" : provider}/${safeIdentifier(options.repoName)}/${environment === "unknown" ? "dev" : environment}/${secretRefLeaf(envName)}`
365
+ }))
366
+ };
367
+ }
368
+ function createProfileAction(server) {
369
+ return {
370
+ kind: "create-profile",
371
+ title: `Create Switchboard profile ${server.suggestedProfileName}`,
372
+ status: "planned",
373
+ serverName: server.name,
374
+ profileName: server.suggestedProfileName,
375
+ provider: server.provider,
376
+ reason: "An existing MCP server was found in project client config and can be routed through Switchboard."
377
+ };
378
+ }
379
+ function uniqueSecretRefs(servers) {
380
+ const byRef = new Map();
381
+ for (const server of servers) {
382
+ for (const secret of server.suggestedSecretRefs) {
383
+ byRef.set(secret.ref, secret);
384
+ }
385
+ }
386
+ return [...byRef.values()].sort((a, b) => a.ref.localeCompare(b.ref));
387
+ }
388
+ function detectEnvFiles(cwd) {
389
+ let entries;
390
+ try {
391
+ entries = readdirSync(cwd);
392
+ }
393
+ catch {
394
+ return [];
395
+ }
396
+ return entries
397
+ .filter((entry) => /^\.env(?:\.|$)/.test(entry))
398
+ .map((entry) => join(cwd, entry))
399
+ .filter((path) => safeIsFile(path))
400
+ .map((path) => {
401
+ const envKeys = envVarNames(readFileSync(path, "utf8"));
402
+ return {
403
+ path,
404
+ envKeys,
405
+ providerHints: [...new Set(envKeys.map(providerFromEnvName))]
406
+ };
407
+ });
408
+ }
409
+ function buildWarnings(options) {
410
+ const warnings = [];
411
+ for (const client of options.clients) {
412
+ if (client.status === "invalid") {
413
+ warnings.push(`${client.client} config could not be parsed: ${client.message}`);
414
+ }
415
+ if (client.servers.some((server) => server.name === "switchboard")) {
416
+ warnings.push(`${client.client} already has a switchboard MCP server; import will preserve it.`);
417
+ }
418
+ }
419
+ if (options.detectedServers.length === 0) {
420
+ warnings.push("No existing project MCP servers were found to import.");
421
+ }
422
+ if (options.envFiles.some((file) => file.envKeys.some((key) => isSecretLikeName(key)))) {
423
+ warnings.push("Secret-looking env var names were found in env files; import reports names only and does not read values.");
424
+ }
425
+ if (options.detectedServers.some((server) => server.envKeys.some((key) => isSecretLikeName(key)))) {
426
+ warnings.push("Existing MCP configs reference secret-looking env names; store values behind Switchboard local token aliases before routing agents.");
427
+ }
428
+ return [...new Set(warnings)];
429
+ }
430
+ function codexServerSections(content) {
431
+ const sections = [];
432
+ let current = null;
433
+ for (const line of content.split(/\r?\n/)) {
434
+ const serverHeader = line.match(/^\s*\[mcp_servers\.([^\].]+)\]\s*$/);
435
+ const envHeader = line.match(/^\s*\[mcp_servers\.([^\].]+)\.env\]\s*$/);
436
+ if (serverHeader?.[1]) {
437
+ if (current) {
438
+ sections.push(current);
439
+ }
440
+ current = {
441
+ name: unquoteTomlKey(serverHeader[1]),
442
+ assignments: {},
443
+ envKeys: [],
444
+ envSection: false
445
+ };
446
+ continue;
447
+ }
448
+ if (envHeader?.[1]) {
449
+ if (current) {
450
+ sections.push(current);
451
+ }
452
+ current = {
453
+ name: unquoteTomlKey(envHeader[1]),
454
+ assignments: {},
455
+ envKeys: [],
456
+ envSection: true
457
+ };
458
+ continue;
459
+ }
460
+ if (!current) {
461
+ continue;
462
+ }
463
+ const assignment = line.match(/^\s*([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.*?)\s*$/);
464
+ if (!assignment?.[1] || assignment[2] === undefined) {
465
+ continue;
466
+ }
467
+ if (current.envSection) {
468
+ current.envKeys.push(assignment[1]);
469
+ }
470
+ else {
471
+ current.assignments[assignment[1]] = assignment[2];
472
+ }
473
+ }
474
+ if (current) {
475
+ sections.push(current);
476
+ }
477
+ const merged = new Map();
478
+ for (const section of sections) {
479
+ const existing = merged.get(section.name);
480
+ if (existing) {
481
+ Object.assign(existing.assignments, section.assignments);
482
+ existing.envKeys.push(...section.envKeys);
483
+ }
484
+ else {
485
+ merged.set(section.name, {
486
+ name: section.name,
487
+ assignments: { ...section.assignments },
488
+ envKeys: [...section.envKeys]
489
+ });
490
+ }
491
+ }
492
+ return [...merged.values()];
493
+ }
494
+ function parseTomlString(value) {
495
+ if (!value) {
496
+ return null;
497
+ }
498
+ const match = value.match(/^"((?:\\"|[^"])*)"$/);
499
+ return match?.[1]?.replace(/\\"/g, "\"") ?? null;
500
+ }
501
+ function parseTomlStringArray(value) {
502
+ if (!value) {
503
+ return [];
504
+ }
505
+ const trimmed = value.trim();
506
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
507
+ return [];
508
+ }
509
+ const matches = [...trimmed.matchAll(/"((?:\\"|[^"])*)"/g)];
510
+ return matches.map((match) => (match[1] ?? "").replace(/\\"/g, "\""));
511
+ }
512
+ function parseTomlInlineObjectKeys(value) {
513
+ if (!value) {
514
+ return [];
515
+ }
516
+ const trimmed = value.trim();
517
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
518
+ return [];
519
+ }
520
+ return [...trimmed.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)\s*=/g)]
521
+ .map((match) => match[1])
522
+ .filter((key) => key !== undefined)
523
+ .sort();
524
+ }
525
+ function unquoteTomlKey(value) {
526
+ return value.replace(/^"|"$/g, "");
527
+ }
528
+ function envVarNames(content) {
529
+ const names = new Set();
530
+ for (const line of content.split(/\r?\n/)) {
531
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
532
+ if (match?.[1]) {
533
+ names.add(match[1]);
534
+ }
535
+ }
536
+ return [...names].sort();
537
+ }
538
+ function inferProvider(options) {
539
+ const haystack = [options.name, options.command ?? "", ...options.args, ...options.envKeys]
540
+ .join(" ")
541
+ .toLowerCase();
542
+ for (const provider of Object.keys(providerEnvPrefixes)) {
543
+ if (haystack.includes(provider)) {
544
+ return provider;
545
+ }
546
+ }
547
+ if (options.envKeys.some((key) => providerFromEnvName(key) !== "unknown")) {
548
+ return providerFromEnvName(options.envKeys.find((key) => providerFromEnvName(key) !== "unknown") ?? "");
549
+ }
550
+ return "unknown";
551
+ }
552
+ function providerFromEnvName(name) {
553
+ for (const [provider, patterns] of Object.entries(providerEnvPrefixes)) {
554
+ if (patterns.some((pattern) => pattern.test(name))) {
555
+ return provider;
556
+ }
557
+ }
558
+ return "unknown";
559
+ }
560
+ function providerFromProfile(value) {
561
+ if (value === "github" ||
562
+ value === "vercel" ||
563
+ value === "stripe" ||
564
+ value === "supabase" ||
565
+ value === "posthog") {
566
+ return value;
567
+ }
568
+ return "unknown";
569
+ }
570
+ function inferEnvironment(options) {
571
+ const text = [options.name, ...options.args, ...options.envKeys].join(" ");
572
+ if (/(^|[_\-.])(prod|production|live)([_\-.]|$)/i.test(text)) {
573
+ return "prod";
574
+ }
575
+ if (/(^|[_\-.])preview([_\-.]|$)/i.test(text)) {
576
+ return "preview";
577
+ }
578
+ if (/(^|[_\-.])(test|testing)([_\-.]|$)/i.test(text)) {
579
+ return "test";
580
+ }
581
+ if (/(^|[_\-.])(dev|development|local)([_\-.]|$)/i.test(text)) {
582
+ return "dev";
583
+ }
584
+ return "unknown";
585
+ }
586
+ function safeIdentifier(value) {
587
+ const normalized = value
588
+ .trim()
589
+ .toLowerCase()
590
+ .replace(/[^a-z0-9]+/g, "_")
591
+ .replace(/^_+|_+$/g, "");
592
+ return normalized.length > 0 ? normalized : "mcp_server";
593
+ }
594
+ function secretRefLeaf(envName) {
595
+ if (/TOKEN/i.test(envName)) {
596
+ return "token";
597
+ }
598
+ if (/KEY/i.test(envName)) {
599
+ return "key";
600
+ }
601
+ if (/SECRET/i.test(envName)) {
602
+ return "secret";
603
+ }
604
+ return safeIdentifier(envName);
605
+ }
606
+ function isSecretLikeName(name) {
607
+ return /(SECRET|TOKEN|KEY|PASSWORD|PRIVATE)/i.test(name);
608
+ }
609
+ function renderCommand(command) {
610
+ return ["switchboard", ...command.args].join(" ");
611
+ }
612
+ async function readOptionalTextFileAsync(path) {
613
+ try {
614
+ return await readFile(path, "utf8");
615
+ }
616
+ catch (error) {
617
+ if (error instanceof Error &&
618
+ "code" in error &&
619
+ error.code === "ENOENT") {
620
+ return null;
621
+ }
622
+ throw error;
623
+ }
624
+ }
625
+ function readOptionalTextFile(path) {
626
+ try {
627
+ return readFileSync(path, "utf8");
628
+ }
629
+ catch (error) {
630
+ if (error instanceof Error &&
631
+ "code" in error &&
632
+ error.code === "ENOENT") {
633
+ return null;
634
+ }
635
+ throw error;
636
+ }
637
+ }
638
+ async function backupExistingFile(path, now) {
639
+ const baseBackupPath = `${path}.switchboard-backup-${backupTimestamp(now)}`;
640
+ for (let attempt = 0; attempt < 100; attempt += 1) {
641
+ const backupPath = attempt === 0 ? baseBackupPath : `${baseBackupPath}-${attempt}`;
642
+ try {
643
+ await copyFile(path, backupPath, constants.COPYFILE_EXCL);
644
+ return backupPath;
645
+ }
646
+ catch (error) {
647
+ if (error instanceof Error &&
648
+ "code" in error &&
649
+ error.code === "EEXIST") {
650
+ continue;
651
+ }
652
+ throw error;
653
+ }
654
+ }
655
+ throw new Error(`could not create a unique backup path for ${path}`);
656
+ }
657
+ function backupTimestamp(now = new Date()) {
658
+ return now.toISOString().replaceAll(/[-:.]/g, "").replace("T", "-");
659
+ }
660
+ function unique(values) {
661
+ return [...new Set(values)];
662
+ }
663
+ function safeIsFile(path) {
664
+ try {
665
+ return statSync(path).isFile();
666
+ }
667
+ catch {
668
+ return false;
669
+ }
670
+ }
671
+ function isRecord(value) {
672
+ return typeof value === "object" && value !== null && !Array.isArray(value);
673
+ }
674
+ function messageFromError(error) {
675
+ return error instanceof Error ? error.message : String(error);
676
+ }
677
+ //# sourceMappingURL=import-plan.js.map