@cubis/foundry 0.3.28 → 0.3.30

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/bin/cubis.js CHANGED
@@ -136,7 +136,10 @@ const STITCH_MCP_URL = "https://stitch.googleapis.com/mcp";
136
136
  const STITCH_API_KEY_PLACEHOLDER = "ur stitch key";
137
137
  const POSTMAN_WORKSPACE_MANUAL_CHOICE = "__postman_workspace_manual__";
138
138
  const CBX_CONFIG_FILENAME = "cbx_config.json";
139
- const POSTMAN_SETTINGS_FILENAME = "postman_setting.json";
139
+ const LEGACY_POSTMAN_CONFIG_FILENAME = ["postman", "setting.json"].join("_");
140
+ const DEFAULT_CREDENTIAL_PROFILE_NAME = "default";
141
+ const RESERVED_CREDENTIAL_PROFILE_NAMES = new Set(["all"]);
142
+ const CREDENTIAL_SERVICES = new Set(["postman", "stitch"]);
140
143
  const POSTMAN_API_KEY_MISSING_WARNING =
141
144
  `Postman API key is not configured. Set ${POSTMAN_API_KEY_ENV_VAR} or update ${CBX_CONFIG_FILENAME}.`;
142
145
  const STITCH_API_KEY_MISSING_WARNING =
@@ -313,6 +316,77 @@ function normalizePostmanWorkspaceId(value) {
313
316
  return normalized;
314
317
  }
315
318
 
319
+ function normalizeCredentialProfileName(value) {
320
+ if (value === undefined || value === null) return null;
321
+ const normalized = String(value).trim();
322
+ return normalized || null;
323
+ }
324
+
325
+ function credentialProfileNameKey(value) {
326
+ const normalized = normalizeCredentialProfileName(value);
327
+ return normalized ? normalized.toLowerCase() : null;
328
+ }
329
+
330
+ function normalizeCredentialService(value, { allowAll = false } = {}) {
331
+ if (!value) return allowAll ? "all" : "postman";
332
+ const normalized = String(value).trim().toLowerCase();
333
+ if (allowAll && normalized === "all") return "all";
334
+ if (!CREDENTIAL_SERVICES.has(normalized)) {
335
+ throw new Error(
336
+ `Unknown credential service '${value}'. Use ${allowAll ? "postman|stitch|all" : "postman|stitch"}.`
337
+ );
338
+ }
339
+ return normalized;
340
+ }
341
+
342
+ function defaultEnvVarForCredentialService(service) {
343
+ return service === "stitch" ? STITCH_API_KEY_ENV_VAR : POSTMAN_API_KEY_ENV_VAR;
344
+ }
345
+
346
+ function defaultMcpUrlForCredentialService(service) {
347
+ return service === "stitch" ? STITCH_MCP_URL : POSTMAN_MCP_URL;
348
+ }
349
+
350
+ function isCredentialServiceEnvVar(value) {
351
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value.trim());
352
+ }
353
+
354
+ function normalizeCredentialProfileRecord(service, rawProfile, fallbackName = DEFAULT_CREDENTIAL_PROFILE_NAME) {
355
+ const defaultEnvVar = defaultEnvVarForCredentialService(service);
356
+ const source = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
357
+ const name = normalizeCredentialProfileName(source.name) || fallbackName;
358
+ const apiKey = normalizePostmanApiKey(source.apiKey);
359
+ const envVarCandidate = normalizePostmanApiKey(source.apiKeyEnvVar) || defaultEnvVar;
360
+ const apiKeyEnvVar = isCredentialServiceEnvVar(envVarCandidate) ? envVarCandidate : defaultEnvVar;
361
+ const profile = {
362
+ name,
363
+ apiKey,
364
+ apiKeyEnvVar
365
+ };
366
+ if (service === "postman") {
367
+ profile.workspaceId = normalizePostmanWorkspaceId(source.workspaceId ?? source.defaultWorkspaceId);
368
+ }
369
+ return profile;
370
+ }
371
+
372
+ function dedupeCredentialProfiles(profiles) {
373
+ const seen = new Set();
374
+ const deduped = [];
375
+ for (const profile of profiles) {
376
+ const key = credentialProfileNameKey(profile?.name);
377
+ if (!key || seen.has(key)) continue;
378
+ seen.add(key);
379
+ deduped.push(profile);
380
+ }
381
+ return deduped;
382
+ }
383
+
384
+ function storedCredentialSource(profile) {
385
+ if (normalizePostmanApiKey(profile?.apiKey)) return "inline";
386
+ if (normalizePostmanApiKey(profile?.apiKeyEnvVar)) return "env";
387
+ return "unset";
388
+ }
389
+
316
390
  function defaultState() {
317
391
  return {
318
392
  schemaVersion: 1,
@@ -1368,6 +1442,79 @@ async function readBundleManifest(bundleId) {
1368
1442
  return parsed;
1369
1443
  }
1370
1444
 
1445
+ function extractSkillIdFromIndexPath(indexPathValue) {
1446
+ const raw = String(indexPathValue || "").trim();
1447
+ if (!raw) return null;
1448
+ const normalized = raw.replace(/\\/g, "/");
1449
+ const marker = "/skills/";
1450
+ const markerIndex = normalized.indexOf(marker);
1451
+ if (markerIndex === -1) return null;
1452
+ const remainder = normalized.slice(markerIndex + marker.length);
1453
+ const [skillId] = remainder.split("/");
1454
+ const normalizedSkillId = String(skillId || "").trim();
1455
+ return normalizedSkillId || null;
1456
+ }
1457
+
1458
+ async function resolveTopLevelSkillIdsFromIndex() {
1459
+ const skillsRoot = path.join(agentAssetsRoot(), "skills");
1460
+ const indexPath = path.join(skillsRoot, "skills_index.json");
1461
+ if (!(await pathExists(indexPath))) return [];
1462
+
1463
+ let parsed;
1464
+ try {
1465
+ parsed = JSON.parse(await readFile(indexPath, "utf8"));
1466
+ } catch {
1467
+ parsed = [];
1468
+ }
1469
+ const entries = Array.isArray(parsed) ? parsed : [];
1470
+
1471
+ const candidates = [];
1472
+ for (const entry of entries) {
1473
+ if (!entry || typeof entry !== "object") continue;
1474
+ const byName = normalizeCredentialProfileName(entry.name);
1475
+ if (byName) candidates.push(byName);
1476
+ const byPath = extractSkillIdFromIndexPath(entry.path);
1477
+ if (byPath) candidates.push(byPath);
1478
+ }
1479
+
1480
+ const topLevelIds = [];
1481
+ const seen = new Set();
1482
+ for (const rawCandidate of candidates) {
1483
+ const skillId = String(rawCandidate || "").trim();
1484
+ if (!skillId || skillId.startsWith(".")) continue;
1485
+ if (skillId === ".system" || skillId.startsWith(".system/")) continue;
1486
+ const key = skillId.toLowerCase();
1487
+ if (seen.has(key)) continue;
1488
+
1489
+ const skillDir = path.join(skillsRoot, skillId);
1490
+ const skillFile = path.join(skillDir, "SKILL.md");
1491
+ if (!(await pathExists(skillDir)) || !(await pathExists(skillFile))) continue;
1492
+
1493
+ seen.add(key);
1494
+ topLevelIds.push(skillId);
1495
+ }
1496
+
1497
+ topLevelIds.sort((a, b) => a.localeCompare(b));
1498
+ return topLevelIds;
1499
+ }
1500
+
1501
+ async function resolveInstallSkillIds({ platformSpec, extraSkillIds = [] }) {
1502
+ const indexedSkillIds = await resolveTopLevelSkillIdsFromIndex();
1503
+ const fallbackManifestSkillIds = Array.isArray(platformSpec.skills) ? platformSpec.skills : [];
1504
+
1505
+ let selected = indexedSkillIds.length > 0 ? indexedSkillIds : fallbackManifestSkillIds;
1506
+ if (Array.isArray(platformSpec.skillAllowList) && platformSpec.skillAllowList.length > 0) {
1507
+ const allow = new Set(platformSpec.skillAllowList.map((item) => String(item).toLowerCase()));
1508
+ selected = selected.filter((skillId) => allow.has(String(skillId).toLowerCase()));
1509
+ }
1510
+ if (Array.isArray(platformSpec.skillBlockList) && platformSpec.skillBlockList.length > 0) {
1511
+ const blocked = new Set(platformSpec.skillBlockList.map((item) => String(item).toLowerCase()));
1512
+ selected = selected.filter((skillId) => !blocked.has(String(skillId).toLowerCase()));
1513
+ }
1514
+
1515
+ return unique([...selected, ...extraSkillIds.filter(Boolean)]);
1516
+ }
1517
+
1371
1518
  async function chooseBundle(bundleOption) {
1372
1519
  const bundleIds = await listBundleIds();
1373
1520
  if (bundleIds.length === 0) {
@@ -2275,12 +2422,12 @@ async function writeGeneratedArtifact({ destination, content, dryRun = false })
2275
2422
  return { action: exists ? "replaced" : "installed", path: destination };
2276
2423
  }
2277
2424
 
2278
- function resolveLegacyPostmanSettingsPath({ scope, cwd = process.cwd() }) {
2425
+ function resolveLegacyPostmanConfigPath({ scope, cwd = process.cwd() }) {
2279
2426
  if (scope === "global") {
2280
- return path.join(os.homedir(), ".cbx", POSTMAN_SETTINGS_FILENAME);
2427
+ return path.join(os.homedir(), ".cbx", LEGACY_POSTMAN_CONFIG_FILENAME);
2281
2428
  }
2282
2429
  const workspaceRoot = findWorkspaceRoot(cwd);
2283
- return path.join(workspaceRoot, POSTMAN_SETTINGS_FILENAME);
2430
+ return path.join(workspaceRoot, LEGACY_POSTMAN_CONFIG_FILENAME);
2284
2431
  }
2285
2432
 
2286
2433
  function resolveCbxConfigPath({ scope, cwd = process.cwd() }) {
@@ -2291,6 +2438,18 @@ function resolveCbxConfigPath({ scope, cwd = process.cwd() }) {
2291
2438
  return path.join(workspaceRoot, CBX_CONFIG_FILENAME);
2292
2439
  }
2293
2440
 
2441
+ async function assertNoLegacyOnlyPostmanConfig({ scope, cwd = process.cwd() }) {
2442
+ const configPath = resolveCbxConfigPath({ scope, cwd });
2443
+ if (await pathExists(configPath)) return;
2444
+
2445
+ const legacyPath = resolveLegacyPostmanConfigPath({ scope, cwd });
2446
+ if (!(await pathExists(legacyPath))) return;
2447
+
2448
+ throw new Error(
2449
+ `Legacy Postman config detected at ${legacyPath}. Create ${configPath} first (for example: cbx workflows config --scope ${scope} --clear-workspace-id).`
2450
+ );
2451
+ }
2452
+
2294
2453
  function resolveMcpRootPath({ scope, cwd = process.cwd() }) {
2295
2454
  if (scope === "global") {
2296
2455
  return path.join(os.homedir(), ".cbx", "mcp");
@@ -2414,20 +2573,158 @@ function normalizePostmanApiKey(value) {
2414
2573
  return normalized || null;
2415
2574
  }
2416
2575
 
2576
+ function parseStoredCredentialServiceConfig({
2577
+ service,
2578
+ rawService
2579
+ }) {
2580
+ if (!rawService || typeof rawService !== "object" || Array.isArray(rawService)) {
2581
+ return null;
2582
+ }
2583
+
2584
+ const defaultEnvVar = defaultEnvVarForCredentialService(service);
2585
+ const defaultMcpUrl = defaultMcpUrlForCredentialService(service);
2586
+ const mcpUrl = String(rawService.mcpUrl || defaultMcpUrl).trim() || defaultMcpUrl;
2587
+ const profiles = [];
2588
+ const rawProfiles = Array.isArray(rawService.profiles) ? rawService.profiles : [];
2589
+
2590
+ for (const rawProfile of rawProfiles) {
2591
+ const name = normalizeCredentialProfileName(rawProfile?.name);
2592
+ if (!name) continue;
2593
+ profiles.push(normalizeCredentialProfileRecord(service, rawProfile, name));
2594
+ }
2595
+
2596
+ if (profiles.length === 0) {
2597
+ profiles.push(
2598
+ normalizeCredentialProfileRecord(
2599
+ service,
2600
+ {
2601
+ name: DEFAULT_CREDENTIAL_PROFILE_NAME,
2602
+ apiKey: rawService.apiKey,
2603
+ apiKeyEnvVar: rawService.apiKeyEnvVar,
2604
+ workspaceId: service === "postman" ? rawService.defaultWorkspaceId : null
2605
+ },
2606
+ DEFAULT_CREDENTIAL_PROFILE_NAME
2607
+ )
2608
+ );
2609
+ }
2610
+
2611
+ const dedupedProfiles = dedupeCredentialProfiles(profiles);
2612
+ const activeProfileNameCandidate =
2613
+ normalizeCredentialProfileName(rawService.activeProfileName) || dedupedProfiles[0].name;
2614
+ const activeProfile =
2615
+ dedupedProfiles.find(
2616
+ (profile) => credentialProfileNameKey(profile.name) === credentialProfileNameKey(activeProfileNameCandidate)
2617
+ ) || dedupedProfiles[0];
2618
+ const activeProfileName = activeProfile.name;
2619
+
2620
+ if (service === "postman" && activeProfile.workspaceId === null) {
2621
+ const legacyWorkspaceId = normalizePostmanWorkspaceId(rawService.defaultWorkspaceId);
2622
+ if (legacyWorkspaceId) {
2623
+ activeProfile.workspaceId = legacyWorkspaceId;
2624
+ }
2625
+ }
2626
+
2627
+ return {
2628
+ mcpUrl,
2629
+ profiles: dedupedProfiles,
2630
+ activeProfileName,
2631
+ activeProfile,
2632
+ apiKey: normalizePostmanApiKey(activeProfile.apiKey),
2633
+ apiKeyEnvVar: String(activeProfile.apiKeyEnvVar || defaultEnvVar).trim() || defaultEnvVar,
2634
+ defaultWorkspaceId: service === "postman" ? normalizePostmanWorkspaceId(activeProfile.workspaceId) : null
2635
+ };
2636
+ }
2637
+
2417
2638
  function parseStoredPostmanConfig(raw) {
2418
2639
  if (!raw || typeof raw !== "object") return null;
2419
2640
  const source = raw.postman && typeof raw.postman === "object" ? raw.postman : raw;
2641
+ return parseStoredCredentialServiceConfig({ service: "postman", rawService: source });
2642
+ }
2420
2643
 
2421
- const apiKey = normalizePostmanApiKey(source.apiKey);
2422
- const apiKeyEnvVar = String(source.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR).trim() || POSTMAN_API_KEY_ENV_VAR;
2423
- const mcpUrl = String(source.mcpUrl || POSTMAN_MCP_URL).trim() || POSTMAN_MCP_URL;
2424
- const defaultWorkspaceId = normalizePostmanWorkspaceId(source.defaultWorkspaceId);
2644
+ function parseStoredStitchConfig(raw) {
2645
+ if (!raw || typeof raw !== "object") return null;
2646
+ const source = raw.stitch && typeof raw.stitch === "object" ? raw.stitch : raw;
2647
+ if (!source || typeof source !== "object" || Array.isArray(source)) return null;
2648
+ const hasStitchShape =
2649
+ source.profiles !== undefined ||
2650
+ source.apiKey !== undefined ||
2651
+ source.apiKeyEnvVar !== undefined ||
2652
+ source.mcpUrl !== undefined ||
2653
+ source.activeProfileName !== undefined;
2654
+ if (!hasStitchShape) return null;
2655
+ return parseStoredCredentialServiceConfig({ service: "stitch", rawService: source, allowMissing: true });
2656
+ }
2657
+
2658
+ function upsertNormalizedPostmanConfig(target, postmanState) {
2659
+ const next = target && typeof target === "object" && !Array.isArray(target) ? target : {};
2660
+ const existingPostman =
2661
+ next.postman && typeof next.postman === "object" && !Array.isArray(next.postman) ? next.postman : {};
2662
+ const serviceConfig = postmanState || parseStoredPostmanConfig({ postman: existingPostman });
2663
+ if (!serviceConfig) return next;
2664
+
2665
+ next.postman = {
2666
+ ...existingPostman,
2667
+ profiles: serviceConfig.profiles,
2668
+ activeProfileName: serviceConfig.activeProfileName,
2669
+ apiKey: serviceConfig.apiKey,
2670
+ apiKeyEnvVar: serviceConfig.apiKeyEnvVar,
2671
+ apiKeySource: storedCredentialSource(serviceConfig.activeProfile),
2672
+ defaultWorkspaceId: serviceConfig.defaultWorkspaceId,
2673
+ mcpUrl: serviceConfig.mcpUrl
2674
+ };
2675
+ return next;
2676
+ }
2677
+
2678
+ function upsertNormalizedStitchConfig(target, stitchState) {
2679
+ const next = target && typeof target === "object" && !Array.isArray(target) ? target : {};
2680
+ const existingStitch =
2681
+ next.stitch && typeof next.stitch === "object" && !Array.isArray(next.stitch) ? next.stitch : {};
2682
+ const serviceConfig =
2683
+ stitchState ||
2684
+ parseStoredStitchConfig({ stitch: existingStitch }) ||
2685
+ parseStoredCredentialServiceConfig({ service: "stitch", rawService: existingStitch, allowMissing: true });
2686
+ if (!serviceConfig) return next;
2687
+
2688
+ next.stitch = {
2689
+ ...existingStitch,
2690
+ server: existingStitch.server || STITCH_MCP_SERVER_ID,
2691
+ profiles: serviceConfig.profiles,
2692
+ activeProfileName: serviceConfig.activeProfileName,
2693
+ apiKey: serviceConfig.apiKey,
2694
+ apiKeyEnvVar: serviceConfig.apiKeyEnvVar,
2695
+ apiKeySource: storedCredentialSource(serviceConfig.activeProfile),
2696
+ mcpUrl: serviceConfig.mcpUrl
2697
+ };
2698
+ return next;
2699
+ }
2700
+
2701
+ function resolveCredentialEffectiveStatus({
2702
+ service,
2703
+ serviceConfig,
2704
+ env = process.env
2705
+ }) {
2706
+ if (!serviceConfig) return null;
2707
+ const defaultEnvVar = defaultEnvVarForCredentialService(service);
2708
+ const activeProfile = serviceConfig.activeProfile || serviceConfig.profiles?.[0] || null;
2709
+ const apiKey = normalizePostmanApiKey(activeProfile?.apiKey);
2710
+ const apiKeyEnvVar = String(activeProfile?.apiKeyEnvVar || defaultEnvVar).trim() || defaultEnvVar;
2711
+ const envApiKey = normalizePostmanApiKey(env?.[apiKeyEnvVar]);
2712
+ const storedSource = storedCredentialSource(activeProfile);
2713
+ const effectiveSource =
2714
+ service === "stitch"
2715
+ ? getStitchApiKeySource({ apiKey, envApiKey })
2716
+ : getPostmanApiKeySource({ apiKey, envApiKey });
2425
2717
 
2426
2718
  return {
2427
- apiKey,
2428
- apiKeyEnvVar,
2429
- mcpUrl,
2430
- defaultWorkspaceId
2719
+ service,
2720
+ activeProfileName: serviceConfig.activeProfileName,
2721
+ activeProfile,
2722
+ profileCount: Array.isArray(serviceConfig.profiles) ? serviceConfig.profiles.length : 0,
2723
+ storedSource,
2724
+ effectiveSource,
2725
+ effectiveEnvVar: apiKeyEnvVar,
2726
+ envVarPresent: Boolean(envApiKey),
2727
+ workspaceId: service === "postman" ? normalizePostmanWorkspaceId(activeProfile?.workspaceId) : null
2431
2728
  };
2432
2729
  }
2433
2730
 
@@ -2482,22 +2779,6 @@ async function fetchPostmanWorkspaces({
2482
2779
  }
2483
2780
  }
2484
2781
 
2485
- function parseStoredStitchConfig(raw) {
2486
- if (!raw || typeof raw !== "object") return null;
2487
- const source = raw.stitch && typeof raw.stitch === "object" ? raw.stitch : null;
2488
- if (!source) return null;
2489
-
2490
- const apiKey = normalizePostmanApiKey(source.apiKey);
2491
- const apiKeyEnvVar = String(source.apiKeyEnvVar || STITCH_API_KEY_ENV_VAR).trim() || STITCH_API_KEY_ENV_VAR;
2492
- const mcpUrl = String(source.mcpUrl || STITCH_MCP_URL).trim() || STITCH_MCP_URL;
2493
-
2494
- return {
2495
- apiKey,
2496
- apiKeyEnvVar,
2497
- mcpUrl
2498
- };
2499
- }
2500
-
2501
2782
  function parseJsonLenient(raw) {
2502
2783
  try {
2503
2784
  return {
@@ -2947,7 +3228,12 @@ async function resolvePostmanInstallSelection({
2947
3228
  }
2948
3229
 
2949
3230
  const cbxConfigPath = resolveCbxConfigPath({ scope: mcpScope, cwd });
2950
- const legacySettingsPath = resolveLegacyPostmanSettingsPath({ scope: mcpScope, cwd });
3231
+ const defaultProfile = normalizeCredentialProfileRecord("postman", {
3232
+ name: DEFAULT_CREDENTIAL_PROFILE_NAME,
3233
+ apiKey: apiKey || null,
3234
+ apiKeyEnvVar: POSTMAN_API_KEY_ENV_VAR,
3235
+ workspaceId: defaultWorkspaceId ?? null
3236
+ });
2951
3237
  const cbxConfig = {
2952
3238
  schemaVersion: 1,
2953
3239
  generatedBy: "cbx workflows install --postman",
@@ -2958,19 +3244,28 @@ async function resolvePostmanInstallSelection({
2958
3244
  platform
2959
3245
  },
2960
3246
  postman: {
2961
- apiKey: apiKey || null,
2962
- apiKeyEnvVar: POSTMAN_API_KEY_ENV_VAR,
2963
- apiKeySource,
2964
- defaultWorkspaceId: defaultWorkspaceId ?? null,
3247
+ profiles: [defaultProfile],
3248
+ activeProfileName: defaultProfile.name,
3249
+ apiKey: defaultProfile.apiKey,
3250
+ apiKeyEnvVar: defaultProfile.apiKeyEnvVar,
3251
+ apiKeySource: storedCredentialSource(defaultProfile),
3252
+ defaultWorkspaceId: defaultProfile.workspaceId,
2965
3253
  mcpUrl: POSTMAN_MCP_URL
2966
3254
  }
2967
3255
  };
2968
3256
  if (stitchEnabled) {
3257
+ const defaultStitchProfile = normalizeCredentialProfileRecord("stitch", {
3258
+ name: DEFAULT_CREDENTIAL_PROFILE_NAME,
3259
+ apiKey: stitchApiKey || null,
3260
+ apiKeyEnvVar: STITCH_API_KEY_ENV_VAR
3261
+ });
2969
3262
  cbxConfig.stitch = {
2970
3263
  server: STITCH_MCP_SERVER_ID,
2971
- apiKey: stitchApiKey || null,
2972
- apiKeyEnvVar: STITCH_API_KEY_ENV_VAR,
2973
- apiKeySource: stitchApiKeySource,
3264
+ profiles: [defaultStitchProfile],
3265
+ activeProfileName: defaultStitchProfile.name,
3266
+ apiKey: defaultStitchProfile.apiKey,
3267
+ apiKeyEnvVar: defaultStitchProfile.apiKeyEnvVar,
3268
+ apiKeySource: storedCredentialSource(defaultStitchProfile),
2974
3269
  mcpUrl: STITCH_MCP_URL
2975
3270
  };
2976
3271
  }
@@ -2987,8 +3282,7 @@ async function resolvePostmanInstallSelection({
2987
3282
  mcpScope,
2988
3283
  warnings,
2989
3284
  cbxConfig,
2990
- cbxConfigPath,
2991
- legacySettingsPath
3285
+ cbxConfigPath
2992
3286
  };
2993
3287
  }
2994
3288
 
@@ -3006,6 +3300,7 @@ async function configurePostmanInstallArtifacts({
3006
3300
  let warnings = postmanSelection.warnings.filter(
3007
3301
  (warning) => warning !== POSTMAN_API_KEY_MISSING_WARNING && warning !== STITCH_API_KEY_MISSING_WARNING
3008
3302
  );
3303
+ await assertNoLegacyOnlyPostmanConfig({ scope: postmanSelection.mcpScope, cwd });
3009
3304
  const cbxConfigContent = `${JSON.stringify(postmanSelection.cbxConfig, null, 2)}\n`;
3010
3305
  const cbxConfigResult = await writeTextFile({
3011
3306
  targetPath: postmanSelection.cbxConfigPath,
@@ -3014,28 +3309,27 @@ async function configurePostmanInstallArtifacts({
3014
3309
  dryRun
3015
3310
  });
3016
3311
 
3017
- let effectiveApiKey = normalizePostmanApiKey(postmanSelection.cbxConfig?.postman?.apiKey);
3018
- let effectiveApiKeyEnvVar = String(
3019
- postmanSelection.cbxConfig?.postman?.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR
3020
- ).trim();
3021
- let effectiveDefaultWorkspaceId = postmanSelection.defaultWorkspaceId ?? null;
3022
- let effectiveMcpUrl = postmanSelection.cbxConfig?.postman?.mcpUrl || POSTMAN_MCP_URL;
3312
+ const installPostmanConfig = parseStoredPostmanConfig(postmanSelection.cbxConfig);
3313
+ let effectiveApiKey = normalizePostmanApiKey(installPostmanConfig?.apiKey);
3314
+ let effectiveApiKeyEnvVar = String(installPostmanConfig?.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR).trim();
3315
+ let effectiveDefaultWorkspaceId = installPostmanConfig?.defaultWorkspaceId ?? postmanSelection.defaultWorkspaceId ?? null;
3316
+ let effectiveMcpUrl = installPostmanConfig?.mcpUrl || POSTMAN_MCP_URL;
3023
3317
  const shouldInstallStitch = Boolean(postmanSelection.stitchEnabled);
3318
+ const installStitchConfig = shouldInstallStitch ? parseStoredStitchConfig(postmanSelection.cbxConfig) : null;
3024
3319
  let effectiveStitchApiKey = shouldInstallStitch
3025
- ? normalizePostmanApiKey(postmanSelection.cbxConfig?.stitch?.apiKey)
3320
+ ? normalizePostmanApiKey(installStitchConfig?.apiKey)
3026
3321
  : null;
3322
+ let effectiveStitchApiKeyEnvVar = shouldInstallStitch
3323
+ ? String(installStitchConfig?.apiKeyEnvVar || STITCH_API_KEY_ENV_VAR).trim() || STITCH_API_KEY_ENV_VAR
3324
+ : STITCH_API_KEY_ENV_VAR;
3027
3325
  let effectiveStitchMcpUrl = shouldInstallStitch
3028
- ? postmanSelection.cbxConfig?.stitch?.mcpUrl || STITCH_MCP_URL
3326
+ ? installStitchConfig?.mcpUrl || STITCH_MCP_URL
3029
3327
  : STITCH_MCP_URL;
3030
3328
 
3031
3329
  if (cbxConfigResult.action === "skipped" || cbxConfigResult.action === "would-skip") {
3032
3330
  const existingCbxConfig = await readJsonFileIfExists(postmanSelection.cbxConfigPath);
3033
- const existingLegacySettings = await readJsonFileIfExists(postmanSelection.legacySettingsPath);
3034
- const storedPostmanConfig =
3035
- parseStoredPostmanConfig(existingCbxConfig.value) || parseStoredPostmanConfig(existingLegacySettings.value);
3036
- const storedStitchConfig =
3037
- shouldInstallStitch &&
3038
- (parseStoredStitchConfig(existingCbxConfig.value) || parseStoredStitchConfig(existingLegacySettings.value));
3331
+ const storedPostmanConfig = parseStoredPostmanConfig(existingCbxConfig.value);
3332
+ const storedStitchConfig = shouldInstallStitch ? parseStoredStitchConfig(existingCbxConfig.value) : null;
3039
3333
 
3040
3334
  if (storedPostmanConfig) {
3041
3335
  effectiveApiKey = storedPostmanConfig.apiKey;
@@ -3044,12 +3338,14 @@ async function configurePostmanInstallArtifacts({
3044
3338
  effectiveMcpUrl = storedPostmanConfig.mcpUrl || POSTMAN_MCP_URL;
3045
3339
  } else {
3046
3340
  warnings.push(
3047
- `Existing ${CBX_CONFIG_FILENAME} (or legacy ${POSTMAN_SETTINGS_FILENAME}) could not be parsed. Using install-time Postman values for MCP config.`
3341
+ `Existing ${CBX_CONFIG_FILENAME} could not be parsed. Using install-time Postman values for MCP config.`
3048
3342
  );
3049
3343
  }
3050
3344
 
3051
3345
  if (storedStitchConfig) {
3052
3346
  effectiveStitchApiKey = storedStitchConfig.apiKey;
3347
+ effectiveStitchApiKeyEnvVar =
3348
+ String(storedStitchConfig.apiKeyEnvVar || STITCH_API_KEY_ENV_VAR).trim() || STITCH_API_KEY_ENV_VAR;
3053
3349
  effectiveStitchMcpUrl = storedStitchConfig.mcpUrl || STITCH_MCP_URL;
3054
3350
  }
3055
3351
 
@@ -3069,7 +3365,7 @@ async function configurePostmanInstallArtifacts({
3069
3365
  }
3070
3366
  }
3071
3367
 
3072
- const envApiKey = normalizePostmanApiKey(process.env[POSTMAN_API_KEY_ENV_VAR]);
3368
+ const envApiKey = normalizePostmanApiKey(process.env[effectiveApiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR]);
3073
3369
  const effectiveApiKeySource = getPostmanApiKeySource({
3074
3370
  apiKey: effectiveApiKey,
3075
3371
  envApiKey
@@ -3077,7 +3373,7 @@ async function configurePostmanInstallArtifacts({
3077
3373
  if (effectiveApiKeySource === "unset") {
3078
3374
  warnings.push(POSTMAN_API_KEY_MISSING_WARNING);
3079
3375
  }
3080
- const envStitchApiKey = normalizePostmanApiKey(process.env[STITCH_API_KEY_ENV_VAR]);
3376
+ const envStitchApiKey = normalizePostmanApiKey(process.env[effectiveStitchApiKeyEnvVar]);
3081
3377
  const effectiveStitchApiKeySource = shouldInstallStitch
3082
3378
  ? getStitchApiKeySource({
3083
3379
  apiKey: effectiveStitchApiKey,
@@ -3340,6 +3636,62 @@ async function validateCopilotAgentsSchema(agentsDir) {
3340
3636
  return findings;
3341
3637
  }
3342
3638
 
3639
+ async function findNestedSkillDirs(rootDir, depth = 0, nested = []) {
3640
+ if (!(await pathExists(rootDir))) return nested;
3641
+ const entries = await readdir(rootDir, { withFileTypes: true });
3642
+ for (const entry of entries) {
3643
+ if (!entry.isDirectory()) continue;
3644
+ const childDir = path.join(rootDir, entry.name);
3645
+ const skillFile = path.join(childDir, "SKILL.md");
3646
+ if (depth >= 1 && (await pathExists(skillFile))) {
3647
+ nested.push(childDir);
3648
+ }
3649
+ await findNestedSkillDirs(childDir, depth + 1, nested);
3650
+ }
3651
+ return nested;
3652
+ }
3653
+
3654
+ async function cleanupNestedDuplicateSkills({
3655
+ skillsRootDir,
3656
+ installedSkillDirs,
3657
+ dryRun = false
3658
+ }) {
3659
+ if (!skillsRootDir || !Array.isArray(installedSkillDirs) || installedSkillDirs.length === 0) return [];
3660
+
3661
+ const topLevelSkillIds = new Set();
3662
+ for (const skillDir of installedSkillDirs) {
3663
+ const skillId = path.basename(skillDir);
3664
+ if (!skillId || skillId.startsWith(".")) continue;
3665
+ const topLevelSkillFile = path.join(skillsRootDir, skillId, "SKILL.md");
3666
+ if (await pathExists(topLevelSkillFile)) {
3667
+ topLevelSkillIds.add(skillId.toLowerCase());
3668
+ }
3669
+ }
3670
+
3671
+ if (topLevelSkillIds.size === 0) return [];
3672
+
3673
+ const cleanup = [];
3674
+ for (const topLevelSkillDir of installedSkillDirs) {
3675
+ if (!(await pathExists(topLevelSkillDir))) continue;
3676
+ const nestedSkillDirs = await findNestedSkillDirs(topLevelSkillDir);
3677
+ for (const nestedDir of nestedSkillDirs) {
3678
+ const nestedSkillId = path.basename(nestedDir);
3679
+ if (!topLevelSkillIds.has(nestedSkillId.toLowerCase())) continue;
3680
+ if (!dryRun) {
3681
+ await rm(nestedDir, { recursive: true, force: true });
3682
+ }
3683
+ cleanup.push({
3684
+ nestedSkillId,
3685
+ path: nestedDir,
3686
+ ownerSkillId: path.basename(topLevelSkillDir),
3687
+ action: dryRun ? "would-remove" : "removed"
3688
+ });
3689
+ }
3690
+ }
3691
+
3692
+ return cleanup;
3693
+ }
3694
+
3343
3695
  async function installBundleArtifacts({
3344
3696
  bundleId,
3345
3697
  manifest,
@@ -3408,8 +3760,10 @@ async function installBundleArtifacts({
3408
3760
  else installed.push(destination);
3409
3761
  }
3410
3762
 
3411
- const manifestSkillIds = Array.isArray(platformSpec.skills) ? platformSpec.skills : [];
3412
- const skillIds = unique([...manifestSkillIds, ...extraSkillIds.filter(Boolean)]);
3763
+ const skillIds = await resolveInstallSkillIds({
3764
+ platformSpec,
3765
+ extraSkillIds
3766
+ });
3413
3767
  for (const skillId of skillIds) {
3414
3768
  const source = path.join(agentAssetsRoot(), "skills", skillId);
3415
3769
  const destination = path.join(profilePaths.skillsDir, skillId);
@@ -3458,6 +3812,12 @@ async function installBundleArtifacts({
3458
3812
  installed.push(...terminalIntegration.installedPaths);
3459
3813
  }
3460
3814
 
3815
+ const duplicateSkillCleanup = await cleanupNestedDuplicateSkills({
3816
+ skillsRootDir: profilePaths.skillsDir,
3817
+ installedSkillDirs: artifacts.skills,
3818
+ dryRun
3819
+ });
3820
+
3461
3821
  const sanitizedSkills = await sanitizeInstalledSkillsForPlatform({
3462
3822
  platform,
3463
3823
  skillDirs: artifacts.skills,
@@ -3476,6 +3836,7 @@ async function installBundleArtifacts({
3476
3836
  artifacts,
3477
3837
  terminalIntegration,
3478
3838
  generatedWrapperSkills,
3839
+ duplicateSkillCleanup,
3479
3840
  sanitizedSkills,
3480
3841
  sanitizedAgents
3481
3842
  };
@@ -3608,7 +3969,8 @@ async function removeBundleArtifacts({
3608
3969
  if (await safeRemove(destination, dryRun)) removed.push(destination);
3609
3970
  }
3610
3971
 
3611
- for (const skillId of platformSpec.skills || []) {
3972
+ const skillIds = await resolveInstallSkillIds({ platformSpec, extraSkillIds: [] });
3973
+ for (const skillId of skillIds) {
3612
3974
  const destination = path.join(profilePaths.skillsDir, skillId);
3613
3975
  if (await safeRemove(destination, dryRun)) removed.push(destination);
3614
3976
  }
@@ -3676,6 +4038,7 @@ function printInstallSummary({
3676
4038
  installed,
3677
4039
  skipped,
3678
4040
  generatedWrapperSkills = [],
4041
+ duplicateSkillCleanup = [],
3679
4042
  sanitizedSkills = [],
3680
4043
  sanitizedAgents = [],
3681
4044
  terminalIntegration = null,
@@ -3725,6 +4088,18 @@ function printInstallSummary({
3725
4088
  }
3726
4089
  }
3727
4090
 
4091
+ if (duplicateSkillCleanup.length > 0) {
4092
+ console.log(`\nNested duplicate skill cleanup (${duplicateSkillCleanup.length}):`);
4093
+ for (const item of duplicateSkillCleanup.slice(0, 10)) {
4094
+ console.log(
4095
+ `- ${item.path}: ${item.action} duplicate skill '${item.nestedSkillId}' nested under '${item.ownerSkillId}'`
4096
+ );
4097
+ }
4098
+ if (duplicateSkillCleanup.length > 10) {
4099
+ console.log(`- ...and ${duplicateSkillCleanup.length - 10} more duplicate skill folder(s).`);
4100
+ }
4101
+ }
4102
+
3728
4103
  if (!dryRun && sanitizedSkills.length > 0) {
3729
4104
  console.log(`\nCopilot skill schema normalization (${sanitizedSkills.length}):`);
3730
4105
  for (const item of sanitizedSkills.slice(0, 8)) {
@@ -4135,6 +4510,56 @@ function printSkillsDeprecation() {
4135
4510
  console.log("[deprecation] 'cbx skills ...' is now an alias. Use 'cbx workflows ...'.");
4136
4511
  }
4137
4512
 
4513
+ function registerConfigKeysSubcommands(configCommand, { aliasMode = false } = {}) {
4514
+ const wrap = (handler) =>
4515
+ aliasMode
4516
+ ? async (options) => {
4517
+ printSkillsDeprecation();
4518
+ await handler(options);
4519
+ }
4520
+ : handler;
4521
+
4522
+ const keysCommand = configCommand
4523
+ .command("keys")
4524
+ .description("Manage named key profiles in cbx_config.json for Postman/Stitch");
4525
+
4526
+ keysCommand
4527
+ .command("list")
4528
+ .description("List key profiles")
4529
+ .option("--service <service>", "postman|stitch|all", "all")
4530
+ .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
4531
+ .action(wrap(runWorkflowConfigKeysList));
4532
+
4533
+ keysCommand
4534
+ .command("add")
4535
+ .description("Add key profile")
4536
+ .option("--service <service>", "postman|stitch", "postman")
4537
+ .requiredOption("--name <profile>", "profile name")
4538
+ .requiredOption("--env-var <envVar>", "environment variable alias")
4539
+ .option("--workspace-id <id|null>", "optional: Postman profile workspace ID")
4540
+ .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
4541
+ .option("--dry-run", "preview changes without writing files")
4542
+ .action(wrap(runWorkflowConfigKeysAdd));
4543
+
4544
+ keysCommand
4545
+ .command("use")
4546
+ .description("Set active key profile")
4547
+ .option("--service <service>", "postman|stitch", "postman")
4548
+ .requiredOption("--name <profile>", "profile name")
4549
+ .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
4550
+ .option("--dry-run", "preview changes without writing files")
4551
+ .action(wrap(runWorkflowConfigKeysUse));
4552
+
4553
+ keysCommand
4554
+ .command("remove")
4555
+ .description("Remove key profile")
4556
+ .option("--service <service>", "postman|stitch", "postman")
4557
+ .requiredOption("--name <profile>", "profile name")
4558
+ .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
4559
+ .option("--dry-run", "preview changes without writing files")
4560
+ .action(wrap(runWorkflowConfigKeysRemove));
4561
+ }
4562
+
4138
4563
  async function resolveAntigravityTerminalVerifierSelection({ platform, options }) {
4139
4564
  const verifierRaw = options.terminalVerifier;
4140
4565
  const hasTerminalIntegrationFlag = Boolean(options.terminalIntegration);
@@ -4379,6 +4804,7 @@ async function runWorkflowInstall(options) {
4379
4804
  installed: installResult.installed,
4380
4805
  skipped: installResult.skipped,
4381
4806
  generatedWrapperSkills: installResult.generatedWrapperSkills,
4807
+ duplicateSkillCleanup: installResult.duplicateSkillCleanup,
4382
4808
  sanitizedSkills: installResult.sanitizedSkills,
4383
4809
  sanitizedAgents: installResult.sanitizedAgents,
4384
4810
  terminalIntegration: installResult.terminalIntegration,
@@ -4573,25 +4999,365 @@ async function runWorkflowDoctor(platformArg, options) {
4573
4999
  }
4574
5000
  }
4575
5001
 
5002
+ function cloneJsonObject(value) {
5003
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
5004
+ return JSON.parse(JSON.stringify(value));
5005
+ }
5006
+
5007
+ function resolveActionOptions(options) {
5008
+ if (!options) return {};
5009
+ if (typeof options.optsWithGlobals === "function") {
5010
+ const resolved = options.optsWithGlobals();
5011
+ return resolved && typeof resolved === "object" ? resolved : {};
5012
+ }
5013
+ if (typeof options.opts === "function") {
5014
+ const resolved = options.opts();
5015
+ return resolved && typeof resolved === "object" ? resolved : {};
5016
+ }
5017
+ return options;
5018
+ }
5019
+
5020
+ function readCliOptionFromArgv(optionName) {
5021
+ const argv = process.argv.slice(2);
5022
+ for (let i = 0; i < argv.length; i += 1) {
5023
+ const token = argv[i];
5024
+ if (token === optionName) {
5025
+ return argv[i + 1] ?? null;
5026
+ }
5027
+ if (token.startsWith(`${optionName}=`)) {
5028
+ return token.slice(optionName.length + 1);
5029
+ }
5030
+ }
5031
+ return null;
5032
+ }
5033
+
5034
+ function hasCliFlag(optionName) {
5035
+ const argv = process.argv.slice(2);
5036
+ return argv.includes(optionName);
5037
+ }
5038
+
5039
+ function prepareConfigDocument(existingValue, { scope, generatedBy }) {
5040
+ const next = cloneJsonObject(existingValue);
5041
+ if (!next.schemaVersion || typeof next.schemaVersion !== "number") next.schemaVersion = 1;
5042
+ next.generatedBy = generatedBy;
5043
+ next.generatedAt = new Date().toISOString();
5044
+ if (!next.mcp || typeof next.mcp !== "object" || Array.isArray(next.mcp)) next.mcp = {};
5045
+ next.mcp.scope = scope;
5046
+ if (!next.mcp.server) next.mcp.server = POSTMAN_SKILL_ID;
5047
+ return next;
5048
+ }
5049
+
5050
+ function ensureCredentialServiceState(configValue, service) {
5051
+ if (service === "postman") {
5052
+ return parseStoredPostmanConfig(configValue) || parseStoredCredentialServiceConfig({ service, rawService: {} });
5053
+ }
5054
+ return (
5055
+ parseStoredStitchConfig(configValue) || parseStoredCredentialServiceConfig({ service: "stitch", rawService: {} })
5056
+ );
5057
+ }
5058
+
5059
+ function upsertCredentialServiceConfig(configValue, service, serviceState) {
5060
+ if (service === "postman") {
5061
+ return upsertNormalizedPostmanConfig(configValue, serviceState);
5062
+ }
5063
+ return upsertNormalizedStitchConfig(configValue, serviceState);
5064
+ }
5065
+
5066
+ function buildConfigShowPayload(rawConfig) {
5067
+ const payload = cloneJsonObject(rawConfig);
5068
+ const postmanState = ensureCredentialServiceState(payload, "postman");
5069
+ upsertNormalizedPostmanConfig(payload, postmanState);
5070
+
5071
+ const stitchState = parseStoredStitchConfig(payload);
5072
+ if (stitchState) {
5073
+ upsertNormalizedStitchConfig(payload, stitchState);
5074
+ }
5075
+
5076
+ payload.status = {
5077
+ postman: resolveCredentialEffectiveStatus({
5078
+ service: "postman",
5079
+ serviceConfig: postmanState
5080
+ })
5081
+ };
5082
+ if (stitchState) {
5083
+ payload.status.stitch = resolveCredentialEffectiveStatus({
5084
+ service: "stitch",
5085
+ serviceConfig: stitchState
5086
+ });
5087
+ }
5088
+
5089
+ return payload;
5090
+ }
5091
+
5092
+ function normalizeProfileNameOrThrow(name) {
5093
+ const normalizedName = normalizeCredentialProfileName(name);
5094
+ if (!normalizedName) {
5095
+ throw new Error("Missing required profile name. Use --name <profile>.");
5096
+ }
5097
+ if (RESERVED_CREDENTIAL_PROFILE_NAMES.has(credentialProfileNameKey(normalizedName))) {
5098
+ throw new Error(`Profile name '${normalizedName}' is reserved.`);
5099
+ }
5100
+ return normalizedName;
5101
+ }
5102
+
5103
+ function findProfileByName(profiles, profileName) {
5104
+ const key = credentialProfileNameKey(profileName);
5105
+ return profiles.find((profile) => credentialProfileNameKey(profile.name) === key) || null;
5106
+ }
5107
+
5108
+ async function loadConfigForScope({ scope, cwd = process.cwd() }) {
5109
+ const configPath = resolveCbxConfigPath({ scope, cwd });
5110
+ await assertNoLegacyOnlyPostmanConfig({ scope, cwd });
5111
+ const existing = await readJsonFileIfExists(configPath);
5112
+ const existingValue =
5113
+ existing.value && typeof existing.value === "object" && !Array.isArray(existing.value) ? existing.value : null;
5114
+ if (existing.exists && !existingValue) {
5115
+ throw new Error(`Existing config at ${configPath} is not valid JSON object.`);
5116
+ }
5117
+ return { configPath, existing, existingValue };
5118
+ }
5119
+
5120
+ async function writeConfigFile({ configPath, nextConfig, existingExists, dryRun }) {
5121
+ const content = `${JSON.stringify(nextConfig, null, 2)}\n`;
5122
+ if (!dryRun) {
5123
+ await mkdir(path.dirname(configPath), { recursive: true });
5124
+ await writeFile(configPath, content, "utf8");
5125
+ }
5126
+ return dryRun ? (existingExists ? "would-update" : "would-create") : existingExists ? "updated" : "created";
5127
+ }
5128
+
5129
+ async function runWorkflowConfigKeysList(options) {
5130
+ try {
5131
+ const opts = resolveActionOptions(options);
5132
+ const cwd = process.cwd();
5133
+ const scopeArg = readCliOptionFromArgv("--scope");
5134
+ const scope = normalizeMcpScope(scopeArg ?? opts.scope, "global");
5135
+ const service = normalizeCredentialService(opts.service, { allowAll: true });
5136
+ const { configPath, existing, existingValue } = await loadConfigForScope({ scope, cwd });
5137
+
5138
+ console.log(`Config file: ${configPath}`);
5139
+ if (!existing.exists) {
5140
+ console.log("Status: missing");
5141
+ return;
5142
+ }
5143
+
5144
+ const services = service === "all" ? ["postman", "stitch"] : [service];
5145
+ for (const serviceId of services) {
5146
+ const serviceState = ensureCredentialServiceState(existingValue, serviceId);
5147
+ if (serviceId === "stitch" && !parseStoredStitchConfig(existingValue)) {
5148
+ console.log(`\n${serviceId}: not configured`);
5149
+ continue;
5150
+ }
5151
+ const effective = resolveCredentialEffectiveStatus({
5152
+ service: serviceId,
5153
+ serviceConfig: serviceState
5154
+ });
5155
+ console.log(`\n${serviceId}: active=${serviceState.activeProfileName} profiles=${serviceState.profiles.length}`);
5156
+ console.log(`- Stored source: ${effective.storedSource}`);
5157
+ console.log(`- Effective source: ${effective.effectiveSource}`);
5158
+ console.log(`- Effective env var: ${effective.effectiveEnvVar}`);
5159
+ for (const profile of serviceState.profiles) {
5160
+ const marker = credentialProfileNameKey(profile.name) === credentialProfileNameKey(serviceState.activeProfileName) ? "*" : " ";
5161
+ const workspaceSuffix =
5162
+ serviceId === "postman"
5163
+ ? ` workspace=${normalizePostmanWorkspaceId(profile.workspaceId) ?? "null"}`
5164
+ : "";
5165
+ console.log(` ${marker} ${profile.name} env=${profile.apiKeyEnvVar}${workspaceSuffix}`);
5166
+ }
5167
+ }
5168
+ } catch (error) {
5169
+ if (error?.name === "ExitPromptError") {
5170
+ console.error("\nCancelled.");
5171
+ process.exit(130);
5172
+ }
5173
+ console.error(`\nError: ${error.message}`);
5174
+ process.exit(1);
5175
+ }
5176
+ }
5177
+
5178
+ async function runWorkflowConfigKeysAdd(options) {
5179
+ try {
5180
+ const opts = resolveActionOptions(options);
5181
+ const cwd = process.cwd();
5182
+ const scopeArg = readCliOptionFromArgv("--scope");
5183
+ const scope = normalizeMcpScope(scopeArg ?? opts.scope, "global");
5184
+ const dryRun = hasCliFlag("--dry-run") || Boolean(opts.dryRun);
5185
+ const service = normalizeCredentialService(opts.service);
5186
+ const profileName = normalizeProfileNameOrThrow(opts.name);
5187
+ const envVar = normalizePostmanApiKey(opts.envVar);
5188
+ if (!envVar || !isCredentialServiceEnvVar(envVar)) {
5189
+ throw new Error("Missing or invalid --env-var. Example: --env-var POSTMAN_API_KEY");
5190
+ }
5191
+
5192
+ const { configPath, existing, existingValue } = await loadConfigForScope({ scope, cwd });
5193
+ const next = prepareConfigDocument(existingValue, {
5194
+ scope,
5195
+ generatedBy: "cbx workflows config keys add"
5196
+ });
5197
+
5198
+ const serviceState = ensureCredentialServiceState(next, service);
5199
+ if (findProfileByName(serviceState.profiles, profileName)) {
5200
+ throw new Error(`Profile '${profileName}' already exists for ${service}.`);
5201
+ }
5202
+
5203
+ const newProfile = normalizeCredentialProfileRecord(service, {
5204
+ name: profileName,
5205
+ apiKey: null,
5206
+ apiKeyEnvVar: envVar,
5207
+ workspaceId: service === "postman" ? normalizePostmanWorkspaceId(opts.workspaceId) : undefined
5208
+ });
5209
+ const updatedServiceState = {
5210
+ ...serviceState,
5211
+ profiles: dedupeCredentialProfiles([...serviceState.profiles, newProfile])
5212
+ };
5213
+ upsertCredentialServiceConfig(next, service, updatedServiceState);
5214
+
5215
+ const action = await writeConfigFile({
5216
+ configPath,
5217
+ nextConfig: next,
5218
+ existingExists: existing.exists,
5219
+ dryRun
5220
+ });
5221
+ console.log(`Config file: ${configPath}`);
5222
+ console.log(`Action: ${action}`);
5223
+ console.log(`Added profile '${profileName}' for ${service} (env var ${envVar}).`);
5224
+ console.log(`Active profile remains '${updatedServiceState.activeProfileName}'.`);
5225
+ } catch (error) {
5226
+ if (error?.name === "ExitPromptError") {
5227
+ console.error("\nCancelled.");
5228
+ process.exit(130);
5229
+ }
5230
+ console.error(`\nError: ${error.message}`);
5231
+ process.exit(1);
5232
+ }
5233
+ }
5234
+
5235
+ async function runWorkflowConfigKeysUse(options) {
5236
+ try {
5237
+ const opts = resolveActionOptions(options);
5238
+ const cwd = process.cwd();
5239
+ const scopeArg = readCliOptionFromArgv("--scope");
5240
+ const scope = normalizeMcpScope(scopeArg ?? opts.scope, "global");
5241
+ const dryRun = hasCliFlag("--dry-run") || Boolean(opts.dryRun);
5242
+ const service = normalizeCredentialService(opts.service);
5243
+ const profileName = normalizeProfileNameOrThrow(opts.name);
5244
+
5245
+ const { configPath, existing, existingValue } = await loadConfigForScope({ scope, cwd });
5246
+ if (!existing.exists) {
5247
+ throw new Error(`Config file is missing at ${configPath}.`);
5248
+ }
5249
+
5250
+ const next = prepareConfigDocument(existingValue, {
5251
+ scope,
5252
+ generatedBy: "cbx workflows config keys use"
5253
+ });
5254
+ const serviceState = ensureCredentialServiceState(next, service);
5255
+ const selectedProfile = findProfileByName(serviceState.profiles, profileName);
5256
+ if (!selectedProfile) {
5257
+ throw new Error(`Profile '${profileName}' does not exist for ${service}.`);
5258
+ }
5259
+
5260
+ const updatedServiceState = {
5261
+ ...serviceState,
5262
+ activeProfileName: selectedProfile.name
5263
+ };
5264
+ upsertCredentialServiceConfig(next, service, updatedServiceState);
5265
+
5266
+ const action = await writeConfigFile({
5267
+ configPath,
5268
+ nextConfig: next,
5269
+ existingExists: existing.exists,
5270
+ dryRun
5271
+ });
5272
+ console.log(`Config file: ${configPath}`);
5273
+ console.log(`Action: ${action}`);
5274
+ console.log(`Active ${service} profile: ${selectedProfile.name}`);
5275
+ } catch (error) {
5276
+ if (error?.name === "ExitPromptError") {
5277
+ console.error("\nCancelled.");
5278
+ process.exit(130);
5279
+ }
5280
+ console.error(`\nError: ${error.message}`);
5281
+ process.exit(1);
5282
+ }
5283
+ }
5284
+
5285
+ async function runWorkflowConfigKeysRemove(options) {
5286
+ try {
5287
+ const opts = resolveActionOptions(options);
5288
+ const cwd = process.cwd();
5289
+ const scopeArg = readCliOptionFromArgv("--scope");
5290
+ const scope = normalizeMcpScope(scopeArg ?? opts.scope, "global");
5291
+ const dryRun = hasCliFlag("--dry-run") || Boolean(opts.dryRun);
5292
+ const service = normalizeCredentialService(opts.service);
5293
+ const profileName = normalizeProfileNameOrThrow(opts.name);
5294
+
5295
+ const { configPath, existing, existingValue } = await loadConfigForScope({ scope, cwd });
5296
+ if (!existing.exists) {
5297
+ throw new Error(`Config file is missing at ${configPath}.`);
5298
+ }
5299
+
5300
+ const next = prepareConfigDocument(existingValue, {
5301
+ scope,
5302
+ generatedBy: "cbx workflows config keys remove"
5303
+ });
5304
+ const serviceState = ensureCredentialServiceState(next, service);
5305
+ const selectedProfile = findProfileByName(serviceState.profiles, profileName);
5306
+ if (!selectedProfile) {
5307
+ throw new Error(`Profile '${profileName}' does not exist for ${service}.`);
5308
+ }
5309
+ if (
5310
+ credentialProfileNameKey(serviceState.activeProfileName) === credentialProfileNameKey(selectedProfile.name)
5311
+ ) {
5312
+ throw new Error(`Cannot remove active profile '${selectedProfile.name}'. Switch active profile first.`);
5313
+ }
5314
+
5315
+ const updatedProfiles = serviceState.profiles.filter(
5316
+ (profile) => credentialProfileNameKey(profile.name) !== credentialProfileNameKey(selectedProfile.name)
5317
+ );
5318
+ const updatedServiceState = {
5319
+ ...serviceState,
5320
+ profiles: updatedProfiles
5321
+ };
5322
+ upsertCredentialServiceConfig(next, service, updatedServiceState);
5323
+
5324
+ const action = await writeConfigFile({
5325
+ configPath,
5326
+ nextConfig: next,
5327
+ existingExists: existing.exists,
5328
+ dryRun
5329
+ });
5330
+ console.log(`Config file: ${configPath}`);
5331
+ console.log(`Action: ${action}`);
5332
+ console.log(`Removed profile '${selectedProfile.name}' from ${service}.`);
5333
+ } catch (error) {
5334
+ if (error?.name === "ExitPromptError") {
5335
+ console.error("\nCancelled.");
5336
+ process.exit(130);
5337
+ }
5338
+ console.error(`\nError: ${error.message}`);
5339
+ process.exit(1);
5340
+ }
5341
+ }
5342
+
4576
5343
  async function runWorkflowConfig(options) {
4577
5344
  try {
5345
+ const opts = resolveActionOptions(options);
4578
5346
  const cwd = process.cwd();
4579
- const scope = normalizeMcpScope(options.scope, "global");
4580
- const dryRun = Boolean(options.dryRun);
4581
- const hasWorkspaceIdOption = options.workspaceId !== undefined;
4582
- const wantsClearWorkspaceId = Boolean(options.clearWorkspaceId);
4583
- const wantsInteractiveEdit = Boolean(options.edit);
5347
+ const scopeArg = readCliOptionFromArgv("--scope");
5348
+ const scope = normalizeMcpScope(scopeArg ?? opts.scope, "global");
5349
+ const dryRun = Boolean(opts.dryRun);
5350
+ const hasWorkspaceIdOption = opts.workspaceId !== undefined;
5351
+ const wantsClearWorkspaceId = Boolean(opts.clearWorkspaceId);
5352
+ const wantsInteractiveEdit = Boolean(opts.edit);
4584
5353
 
4585
5354
  if (hasWorkspaceIdOption && wantsClearWorkspaceId) {
4586
5355
  throw new Error("Use either --workspace-id or --clear-workspace-id, not both.");
4587
5356
  }
4588
5357
 
4589
5358
  const wantsMutation = hasWorkspaceIdOption || wantsClearWorkspaceId || wantsInteractiveEdit;
4590
- const showOnly = Boolean(options.show) || !wantsMutation;
4591
- const configPath = resolveCbxConfigPath({ scope, cwd });
4592
- const existing = await readJsonFileIfExists(configPath);
4593
- const existingValue =
4594
- existing.value && typeof existing.value === "object" && !Array.isArray(existing.value) ? existing.value : null;
5359
+ const showOnly = Boolean(opts.show) || !wantsMutation;
5360
+ const { configPath, existing, existingValue } = await loadConfigForScope({ scope, cwd });
4595
5361
 
4596
5362
  if (showOnly) {
4597
5363
  console.log(`Config file: ${configPath}`);
@@ -4599,33 +5365,20 @@ async function runWorkflowConfig(options) {
4599
5365
  console.log("Status: missing");
4600
5366
  return;
4601
5367
  }
4602
- if (!existingValue) {
4603
- throw new Error(`Existing config at ${configPath} is not valid JSON object.`);
4604
- }
4605
5368
  console.log(`Status: ${existing.exists ? "exists" : "missing"}`);
4606
- console.log(JSON.stringify(existingValue, null, 2));
5369
+ const payload = buildConfigShowPayload(existingValue);
5370
+ console.log(JSON.stringify(payload, null, 2));
4607
5371
  return;
4608
5372
  }
4609
5373
 
4610
- if (existing.exists && !existingValue) {
4611
- throw new Error(`Existing config at ${configPath} is not valid JSON object.`);
4612
- }
4613
-
4614
- const next = existingValue ? JSON.parse(JSON.stringify(existingValue)) : {};
4615
- if (!next.schemaVersion || typeof next.schemaVersion !== "number") next.schemaVersion = 1;
4616
- next.generatedBy = "cbx workflows config";
4617
- next.generatedAt = new Date().toISOString();
4618
-
4619
- if (!next.mcp || typeof next.mcp !== "object" || Array.isArray(next.mcp)) next.mcp = {};
4620
- next.mcp.scope = scope;
4621
- if (!next.mcp.server) next.mcp.server = POSTMAN_SKILL_ID;
4622
-
4623
- if (!next.postman || typeof next.postman !== "object" || Array.isArray(next.postman)) next.postman = {};
4624
- next.postman.apiKey = normalizePostmanApiKey(next.postman.apiKey);
4625
- next.postman.apiKeyEnvVar = String(next.postman.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR).trim() || POSTMAN_API_KEY_ENV_VAR;
4626
- next.postman.mcpUrl = String(next.postman.mcpUrl || POSTMAN_MCP_URL).trim() || POSTMAN_MCP_URL;
5374
+ const next = prepareConfigDocument(existingValue, {
5375
+ scope,
5376
+ generatedBy: "cbx workflows config"
5377
+ });
4627
5378
 
4628
- let workspaceId = normalizePostmanWorkspaceId(next.postman.defaultWorkspaceId);
5379
+ const postmanState = ensureCredentialServiceState(next, "postman");
5380
+ const activeProfile = { ...postmanState.activeProfile };
5381
+ let workspaceId = normalizePostmanWorkspaceId(activeProfile.workspaceId);
4629
5382
 
4630
5383
  if (wantsInteractiveEdit) {
4631
5384
  const promptedWorkspaceId = await input({
@@ -4634,33 +5387,47 @@ async function runWorkflowConfig(options) {
4634
5387
  });
4635
5388
  workspaceId = normalizePostmanWorkspaceId(promptedWorkspaceId);
4636
5389
  }
4637
-
4638
5390
  if (hasWorkspaceIdOption) {
4639
- workspaceId = normalizePostmanWorkspaceId(options.workspaceId);
5391
+ workspaceId = normalizePostmanWorkspaceId(opts.workspaceId);
4640
5392
  }
4641
-
4642
5393
  if (wantsClearWorkspaceId) {
4643
5394
  workspaceId = null;
4644
5395
  }
4645
5396
 
4646
- next.postman.defaultWorkspaceId = workspaceId;
4647
- const envApiKey = normalizePostmanApiKey(process.env[POSTMAN_API_KEY_ENV_VAR]);
4648
- next.postman.apiKeySource = getPostmanApiKeySource({
4649
- apiKey: next.postman.apiKey,
4650
- envApiKey
5397
+ activeProfile.workspaceId = workspaceId;
5398
+ const updatedProfiles = postmanState.profiles.map((profile) =>
5399
+ credentialProfileNameKey(profile.name) === credentialProfileNameKey(postmanState.activeProfileName)
5400
+ ? activeProfile
5401
+ : profile
5402
+ );
5403
+ const updatedPostmanState = parseStoredCredentialServiceConfig({
5404
+ service: "postman",
5405
+ rawService: {
5406
+ ...(next.postman && typeof next.postman === "object" ? next.postman : {}),
5407
+ profiles: updatedProfiles,
5408
+ activeProfileName: postmanState.activeProfileName,
5409
+ mcpUrl: postmanState.mcpUrl
5410
+ }
4651
5411
  });
5412
+ upsertNormalizedPostmanConfig(next, updatedPostmanState);
4652
5413
 
4653
- const content = `${JSON.stringify(next, null, 2)}\n`;
4654
- if (!dryRun) {
4655
- await mkdir(path.dirname(configPath), { recursive: true });
4656
- await writeFile(configPath, content, "utf8");
5414
+ if (parseStoredStitchConfig(next)) {
5415
+ upsertNormalizedStitchConfig(next, parseStoredStitchConfig(next));
4657
5416
  }
4658
5417
 
5418
+ const action = await writeConfigFile({
5419
+ configPath,
5420
+ nextConfig: next,
5421
+ existingExists: existing.exists,
5422
+ dryRun
5423
+ });
5424
+
4659
5425
  console.log(`Config file: ${configPath}`);
4660
- console.log(`Action: ${dryRun ? (existing.exists ? "would-update" : "would-create") : existing.exists ? "updated" : "created"}`);
5426
+ console.log(`Action: ${action}`);
4661
5427
  console.log(`postman.defaultWorkspaceId: ${workspaceId === null ? "null" : workspaceId}`);
4662
- if (Boolean(options.showAfter)) {
4663
- console.log(JSON.stringify(next, null, 2));
5428
+ if (Boolean(opts.showAfter)) {
5429
+ const payload = buildConfigShowPayload(next);
5430
+ console.log(JSON.stringify(payload, null, 2));
4664
5431
  }
4665
5432
  } catch (error) {
4666
5433
  if (error?.name === "ExitPromptError") {
@@ -4903,7 +5670,7 @@ withWorkflowBaseOptions(
4903
5670
  .option("--json", "output JSON")
4904
5671
  ).action(runWorkflowDoctor);
4905
5672
 
4906
- workflowsCommand
5673
+ const workflowsConfigCommand = workflowsCommand
4907
5674
  .command("config")
4908
5675
  .description("View or edit cbx_config.json from terminal")
4909
5676
  .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
@@ -4914,6 +5681,7 @@ workflowsCommand
4914
5681
  .option("--show-after", "print JSON after update")
4915
5682
  .option("--dry-run", "preview changes without writing files")
4916
5683
  .action(runWorkflowConfig);
5684
+ registerConfigKeysSubcommands(workflowsConfigCommand);
4917
5685
 
4918
5686
  workflowsCommand.action(() => {
4919
5687
  workflowsCommand.help();
@@ -4974,7 +5742,7 @@ withWorkflowBaseOptions(
4974
5742
  await runWorkflowDoctor(platform, options);
4975
5743
  });
4976
5744
 
4977
- skillsCommand
5745
+ const skillsConfigCommand = skillsCommand
4978
5746
  .command("config")
4979
5747
  .description("Alias for workflows config")
4980
5748
  .option("--scope <scope>", "config scope: project|workspace|global|user", "global")
@@ -4988,6 +5756,7 @@ skillsCommand
4988
5756
  printSkillsDeprecation();
4989
5757
  await runWorkflowConfig(options);
4990
5758
  });
5759
+ registerConfigKeysSubcommands(skillsConfigCommand, { aliasMode: true });
4991
5760
 
4992
5761
  skillsCommand.action(() => {
4993
5762
  printSkillsDeprecation();