@kolatts/pncli 1.26.0 → 3.0.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.
package/dist/cli.js CHANGED
@@ -4,15 +4,18 @@ import {
4
4
  loadConfig,
5
5
  loadJsonFile,
6
6
  maskConfig,
7
+ normalizeBaseUrl,
7
8
  setConfigValue,
8
9
  setRepoConfigValue,
9
10
  writeGlobalConfig,
10
11
  writeRepoConfig
11
- } from "./chunk-JOAWUILY.js";
12
+ } from "./chunk-LH7WBP7W.js";
13
+ import {
14
+ createHttpClient
15
+ } from "./chunk-ZDJYOI3W.js";
12
16
  import {
13
17
  ExitCode,
14
18
  PncliError,
15
- createHttpClient,
16
19
  fail,
17
20
  log,
18
21
  setGlobalOptions,
@@ -20,7 +23,7 @@ import {
20
23
  success,
21
24
  warn,
22
25
  writeRawOutput
23
- } from "./chunk-AGELHQKO.js";
26
+ } from "./chunk-HZF6WQPU.js";
24
27
  import {
25
28
  AdoCoreClient
26
29
  } from "./chunk-VDL32PSG.js";
@@ -6311,16 +6314,6 @@ function registerConfigCommands(program2) {
6311
6314
  } else {
6312
6315
  results.artifactory = { ok: null, message: "not configured" };
6313
6316
  }
6314
- if (cfg.udeploy.baseUrl && (cfg.udeploy.pat || cfg.udeploy.username && cfg.udeploy.password)) {
6315
- try {
6316
- await http.udeploy("/cli/application");
6317
- results.udeploy = { ok: true, message: "connected" };
6318
- } catch (err) {
6319
- results.udeploy = { ok: false, message: err instanceof Error ? err.message : String(err) };
6320
- }
6321
- } else {
6322
- results.udeploy = { ok: null, message: "not configured" };
6323
- }
6324
6317
  if (cfg.checkmarx.baseUrl && cfg.checkmarx.tenantName && (cfg.checkmarx.apiKey || cfg.checkmarx.clientId && cfg.checkmarx.clientSecret)) {
6325
6318
  try {
6326
6319
  await http.checkmarx("projects", { params: { limit: 1 } });
@@ -6372,6 +6365,25 @@ function registerConfigCommands(program2) {
6372
6365
  } else {
6373
6366
  results.openshift = { ok: null, message: "not configured" };
6374
6367
  }
6368
+ for (const [envName, envCfg] of Object.entries(cfg.openshift.environments)) {
6369
+ for (const [instName, instCfg] of Object.entries(envCfg.instances ?? {})) {
6370
+ const key = `openshift:${envName}/${instName}`;
6371
+ if (instCfg.baseUrl && instCfg.token) {
6372
+ const clusterHttp = createHttpClient(
6373
+ { ...cfg, openshift: { ...cfg.openshift, baseUrl: instCfg.baseUrl, token: instCfg.token } },
6374
+ false
6375
+ );
6376
+ try {
6377
+ await clusterHttp.openshift("/api/v1");
6378
+ results[key] = { ok: true, message: "connected" };
6379
+ } catch (err) {
6380
+ results[key] = { ok: false, message: err instanceof Error ? err.message : String(err) };
6381
+ }
6382
+ } else {
6383
+ results[key] = { ok: null, message: "not configured" };
6384
+ }
6385
+ }
6386
+ }
6375
6387
  if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {
6376
6388
  try {
6377
6389
  await http.dynatrace("/api/v2/entities", {
@@ -6381,8 +6393,10 @@ function registerConfigCommands(program2) {
6381
6393
  } catch (err) {
6382
6394
  results.dynatrace = { ok: false, message: err instanceof Error ? err.message : String(err) };
6383
6395
  }
6384
- } else {
6396
+ } else if (Object.keys(cfg.dynatrace.environments).length === 0) {
6385
6397
  results.dynatrace = { ok: null, message: "not configured" };
6398
+ } else {
6399
+ results.dynatrace = { ok: null, message: "flat config not set \u2014 see named environments below" };
6386
6400
  }
6387
6401
  if (cfg.dynatrace.platformUrl && cfg.dynatrace.platformToken) {
6388
6402
  try {
@@ -6403,6 +6417,46 @@ function registerConfigCommands(program2) {
6403
6417
  } else {
6404
6418
  results.dynatrace_platform = { ok: null, message: "not configured" };
6405
6419
  }
6420
+ for (const [envName, envConfig] of Object.entries(cfg.dynatrace.environments)) {
6421
+ const envHttp = createHttpClient({
6422
+ ...cfg,
6423
+ dynatrace: {
6424
+ ...cfg.dynatrace,
6425
+ baseUrl: envConfig.baseUrl,
6426
+ apiToken: envConfig.apiToken,
6427
+ platformUrl: envConfig.platformUrl,
6428
+ platformToken: envConfig.platformToken
6429
+ }
6430
+ });
6431
+ const key = `dynatrace.${envName}`;
6432
+ if (envConfig.baseUrl && envConfig.apiToken) {
6433
+ try {
6434
+ await envHttp.dynatrace("/api/v2/entities", {
6435
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
6436
+ });
6437
+ results[key] = { ok: true, message: "connected" };
6438
+ } catch (err) {
6439
+ results[key] = { ok: false, message: err instanceof Error ? err.message : String(err) };
6440
+ }
6441
+ } else {
6442
+ results[key] = { ok: null, message: "baseUrl or apiToken not configured" };
6443
+ }
6444
+ const platformKey = `dynatrace.${envName}_platform`;
6445
+ if (envConfig.platformUrl && envConfig.platformToken) {
6446
+ try {
6447
+ await envHttp.dynatracePlatform("/platform/storage/query/v1/query:execute", {
6448
+ method: "POST",
6449
+ body: { query: "fetch spans | limit 1", requestTimeoutMilliseconds: 5e3 },
6450
+ timeoutMs: 1e4
6451
+ });
6452
+ results[platformKey] = { ok: true, message: "connected" };
6453
+ } catch (err) {
6454
+ results[platformKey] = { ok: false, message: err instanceof Error ? err.message : String(err) };
6455
+ }
6456
+ } else if (envConfig.platformUrl || envConfig.platformToken) {
6457
+ results[platformKey] = { ok: false, message: "platformUrl and platformToken must both be configured" };
6458
+ }
6459
+ }
6406
6460
  if (cfg.logscale.baseUrl && cfg.logscale.token) {
6407
6461
  try {
6408
6462
  await http.logscale("/api/v1/repositories");
@@ -6413,6 +6467,26 @@ function registerConfigCommands(program2) {
6413
6467
  } else {
6414
6468
  results.logscale = { ok: null, message: "not configured" };
6415
6469
  }
6470
+ if (cfg.splitio.baseUrl && cfg.splitio.adminApiKey) {
6471
+ try {
6472
+ await http.splitio("/internal/api/v2/workspaces");
6473
+ results.splitio = { ok: true, message: "connected" };
6474
+ } catch (err) {
6475
+ results.splitio = { ok: false, message: err instanceof Error ? err.message : String(err) };
6476
+ }
6477
+ } else {
6478
+ results.splitio = { ok: null, message: "not configured" };
6479
+ }
6480
+ if (cfg.figma.baseUrl && cfg.figma.token) {
6481
+ try {
6482
+ await http.figma("/v1/me");
6483
+ results.figma = { ok: true, message: "connected" };
6484
+ } catch (err) {
6485
+ results.figma = { ok: false, message: err instanceof Error ? err.message : String(err) };
6486
+ }
6487
+ } else {
6488
+ results.figma = { ok: null, message: "not configured" };
6489
+ }
6416
6490
  success(results, "config", "test", start);
6417
6491
  } catch (err) {
6418
6492
  fail(err, "config", "test", start);
@@ -6537,18 +6611,6 @@ function registerConfigCommands(program2) {
6537
6611
  results.jenkins = categorize2(err);
6538
6612
  }
6539
6613
  }
6540
- if (!cfg.udeploy.pat && !(cfg.udeploy.username && cfg.udeploy.password)) {
6541
- results.udeploy = { status: "blank", message: "not configured" };
6542
- } else if (!cfg.udeploy.baseUrl) {
6543
- results.udeploy = { status: "error", message: "baseUrl not configured" };
6544
- } else {
6545
- try {
6546
- await http.udeploy("/cli/application", { timeoutMs: 1e4 });
6547
- results.udeploy = { status: "valid", message: "ok" };
6548
- } catch (err) {
6549
- results.udeploy = categorize2(err);
6550
- }
6551
- }
6552
6614
  const artResult = await checkArtifactoryConnectivity(cfg.artifactory);
6553
6615
  if (!artResult.configured) {
6554
6616
  results.artifactory = { status: "blank", message: artResult.error ?? "not configured" };
@@ -6620,8 +6682,33 @@ function registerConfigCommands(program2) {
6620
6682
  results.openshift = categorize2(err);
6621
6683
  }
6622
6684
  }
6685
+ for (const [envName, envCfg] of Object.entries(cfg.openshift.environments)) {
6686
+ for (const [instName, instCfg] of Object.entries(envCfg.instances ?? {})) {
6687
+ const key = `openshift:${envName}/${instName}`;
6688
+ if (!instCfg.token) {
6689
+ results[key] = { status: "blank", message: "token not configured" };
6690
+ } else if (!instCfg.baseUrl) {
6691
+ results[key] = { status: "error", message: "baseUrl not configured" };
6692
+ } else {
6693
+ const clusterHttp = createHttpClient(
6694
+ { ...cfg, openshift: { ...cfg.openshift, baseUrl: instCfg.baseUrl, token: instCfg.token } },
6695
+ false
6696
+ );
6697
+ try {
6698
+ await clusterHttp.openshift("/api/v1", { timeoutMs: 1e4 });
6699
+ results[key] = { status: "valid", message: "ok" };
6700
+ } catch (err) {
6701
+ results[key] = categorize2(err);
6702
+ }
6703
+ }
6704
+ }
6705
+ }
6623
6706
  if (!cfg.dynatrace.apiToken) {
6624
- results.dynatrace = { status: "blank", message: "not configured" };
6707
+ if (Object.keys(cfg.dynatrace.environments).length === 0) {
6708
+ results.dynatrace = { status: "blank", message: "not configured" };
6709
+ } else {
6710
+ results.dynatrace = { status: "blank", message: "flat config not set \u2014 see named environments below" };
6711
+ }
6625
6712
  } else if (!cfg.dynatrace.baseUrl) {
6626
6713
  results.dynatrace = { status: "error", message: "baseUrl not configured" };
6627
6714
  } else {
@@ -6657,6 +6744,52 @@ function registerConfigCommands(program2) {
6657
6744
  results.dynatrace_platform = categorize2(err);
6658
6745
  }
6659
6746
  }
6747
+ for (const [envName, envConfig] of Object.entries(cfg.dynatrace.environments)) {
6748
+ const envHttp = createHttpClient({
6749
+ ...cfg,
6750
+ dynatrace: {
6751
+ ...cfg.dynatrace,
6752
+ baseUrl: envConfig.baseUrl,
6753
+ apiToken: envConfig.apiToken,
6754
+ platformUrl: envConfig.platformUrl,
6755
+ platformToken: envConfig.platformToken
6756
+ }
6757
+ }, false);
6758
+ const key = `dynatrace.${envName}`;
6759
+ if (!envConfig.apiToken) {
6760
+ results[key] = { status: "blank", message: "apiToken not configured" };
6761
+ } else if (!envConfig.baseUrl) {
6762
+ results[key] = { status: "error", message: "baseUrl not configured" };
6763
+ } else {
6764
+ try {
6765
+ await envHttp.dynatrace("/api/v2/entities", {
6766
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 },
6767
+ timeoutMs: 1e4
6768
+ });
6769
+ results[key] = { status: "valid", message: "ok" };
6770
+ } catch (err) {
6771
+ results[key] = categorize2(err);
6772
+ }
6773
+ }
6774
+ const platformKey = `dynatrace.${envName}_platform`;
6775
+ if (envConfig.platformUrl && envConfig.platformToken) {
6776
+ try {
6777
+ await envHttp.dynatracePlatform("/platform/storage/query/v1/query:execute", {
6778
+ method: "POST",
6779
+ body: { query: "fetch spans | limit 1", requestTimeoutMilliseconds: 5e3 },
6780
+ timeoutMs: 1e4
6781
+ });
6782
+ results[platformKey] = { status: "valid", message: "ok" };
6783
+ } catch (err) {
6784
+ results[platformKey] = categorize2(err);
6785
+ }
6786
+ } else if (envConfig.platformUrl || envConfig.platformToken) {
6787
+ results[platformKey] = {
6788
+ status: "error",
6789
+ message: "platformUrl and platformToken must both be configured"
6790
+ };
6791
+ }
6792
+ }
6660
6793
  if (!cfg.logscale.token) {
6661
6794
  results.logscale = { status: "blank", message: "not configured" };
6662
6795
  } else if (!cfg.logscale.baseUrl) {
@@ -6669,14 +6802,66 @@ function registerConfigCommands(program2) {
6669
6802
  results.logscale = categorize2(err);
6670
6803
  }
6671
6804
  }
6805
+ if (!cfg.splitio.adminApiKey) {
6806
+ results.splitio = { status: "blank", message: "not configured" };
6807
+ } else if (!cfg.splitio.baseUrl) {
6808
+ results.splitio = { status: "error", message: "baseUrl not configured" };
6809
+ } else {
6810
+ try {
6811
+ await http.splitio("/internal/api/v2/workspaces", { timeoutMs: 1e4 });
6812
+ results.splitio = { status: "valid", message: "ok" };
6813
+ } catch (err) {
6814
+ results.splitio = categorize2(err);
6815
+ }
6816
+ }
6817
+ if (!cfg.figma.token) {
6818
+ results.figma = { status: "blank", message: "not configured" };
6819
+ } else if (!cfg.figma.baseUrl) {
6820
+ results.figma = { status: "error", message: "baseUrl not configured" };
6821
+ } else {
6822
+ try {
6823
+ await http.figma("/v1/me", { timeoutMs: 1e4 });
6824
+ results.figma = { status: "valid", message: "ok" };
6825
+ } catch (err) {
6826
+ results.figma = categorize2(err);
6827
+ }
6828
+ }
6672
6829
  const statuses = Object.values(results).map((r) => r.status);
6673
6830
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
6674
6831
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
6832
+ const clusterKeys = Object.keys(results).filter((k) => k.startsWith("openshift:")).sort();
6833
+ const dynamicEnvKeys = Object.keys(cfg.dynatrace.environments).flatMap((envName) => {
6834
+ const keys = [`dynatrace.${envName}`];
6835
+ if (cfg.dynatrace.environments[envName]?.platformUrl) keys.push(`dynatrace.${envName}_platform`);
6836
+ return keys;
6837
+ });
6838
+ const allServices = [
6839
+ "jira",
6840
+ "bitbucket",
6841
+ "github",
6842
+ "confluence",
6843
+ "sonar",
6844
+ "sde",
6845
+ "ado",
6846
+ "jenkins",
6847
+ "artifactory",
6848
+ "checkmarx",
6849
+ "servicenow",
6850
+ "contrast",
6851
+ "sonatypeiq",
6852
+ "openshift",
6853
+ ...clusterKeys,
6854
+ "dynatrace",
6855
+ "dynatrace_platform",
6856
+ ...dynamicEnvKeys,
6857
+ "logscale",
6858
+ "splitio",
6859
+ "figma"
6860
+ ];
6675
6861
  if (cmdOpts.output === "table") {
6676
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform", "logscale"];
6677
- const labelWidth = 14;
6862
+ const labelWidth = Math.max(14, ...allServices.map((s) => s.length + 2));
6678
6863
  const statusWidth = 9;
6679
- for (const svc of services) {
6864
+ for (const svc of allServices) {
6680
6865
  const r = results[svc];
6681
6866
  if (!r) continue;
6682
6867
  const label = svc.padEnd(labelWidth);
@@ -6687,10 +6872,9 @@ function registerConfigCommands(program2) {
6687
6872
  process.stdout.write("\n");
6688
6873
  } else {
6689
6874
  if (opts.pretty) {
6690
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform", "logscale"];
6691
- const labelWidth = 14;
6875
+ const labelWidth = Math.max(14, ...allServices.map((s) => s.length + 2));
6692
6876
  const statusWidth = 9;
6693
- for (const svc of services) {
6877
+ for (const svc of allServices) {
6694
6878
  const r = results[svc];
6695
6879
  if (!r) continue;
6696
6880
  const label = svc.padEnd(labelWidth);
@@ -6707,11 +6891,6 @@ function registerConfigCommands(program2) {
6707
6891
  }
6708
6892
  });
6709
6893
  }
6710
- function normalizeBaseUrl(raw) {
6711
- const trimmed = raw.trim();
6712
- if (!trimmed) return trimmed;
6713
- return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
6714
- }
6715
6894
  async function initGlobalConfig(start) {
6716
6895
  process.stderr.write("pncli config init \u2014 Global configuration\n\n");
6717
6896
  process.stderr.write("\u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
@@ -6939,7 +7118,7 @@ async function initGlobalConfig(start) {
6939
7118
  }
6940
7119
  process.stderr.write("\n\u2500\u2500 Jenkins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
6941
7120
  const useJenkins = await confirm({
6942
- message: "Configure Jenkins Data Center for pipeline operations?",
7121
+ message: "Configure Jenkins for pipeline operations?",
6943
7122
  default: false
6944
7123
  });
6945
7124
  let jenkinsBaseUrl = "";
@@ -6958,66 +7137,6 @@ async function initGlobalConfig(start) {
6958
7137
  message: "Jenkins API token:"
6959
7138
  });
6960
7139
  }
6961
- process.stderr.write("\n\u2500\u2500 IBM UrbanCode Deploy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
6962
- const useUdeploy = await confirm({
6963
- message: "Configure IBM UrbanCode Deploy for component imports and deployment processes?",
6964
- default: false
6965
- });
6966
- let udeployBaseUrl = "";
6967
- let udeployPat = "";
6968
- let udeployUsername = "";
6969
- let udeployPassword = "";
6970
- let udeployDefaultApp = "";
6971
- let udeployDefaultEnv = "";
6972
- if (useUdeploy) {
6973
- udeployBaseUrl = await input({
6974
- message: "UDeploy base URL (e.g. ucd.imagile.dev:8443):",
6975
- default: ""
6976
- });
6977
- udeployUsername = await input({
6978
- message: "UDeploy username (leave blank to use a PAT instead):",
6979
- default: ""
6980
- });
6981
- if (udeployUsername) {
6982
- udeployPassword = await password({
6983
- message: "UDeploy password:",
6984
- validate: (v) => v.length > 0 || "Password cannot be blank"
6985
- });
6986
- } else {
6987
- udeployPat = await password({
6988
- message: "UDeploy personal access token:"
6989
- });
6990
- }
6991
- udeployDefaultApp = await input({
6992
- message: "Default application name (optional):",
6993
- default: ""
6994
- });
6995
- udeployDefaultEnv = await input({
6996
- message: "Default environment name (optional):",
6997
- default: ""
6998
- });
6999
- const hasCredentials = udeployBaseUrl && (udeployPat || udeployUsername && udeployPassword);
7000
- if (hasCredentials) {
7001
- process.stderr.write("\n Verifying connection...\n");
7002
- try {
7003
- const tempConfig = {
7004
- ...loadConfig(),
7005
- udeploy: {
7006
- baseUrl: normalizeBaseUrl(udeployBaseUrl),
7007
- pat: udeployPat || void 0,
7008
- username: udeployUsername || void 0,
7009
- password: udeployPassword || void 0
7010
- }
7011
- };
7012
- const tempHttp = createHttpClient(tempConfig);
7013
- await tempHttp.udeploy("/cli/application");
7014
- process.stderr.write(" Connected.\n");
7015
- } catch (err) {
7016
- warn(`Could not connect to UDeploy: ${err instanceof Error ? err.message : String(err)}`);
7017
- warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
7018
- }
7019
- }
7020
- }
7021
7140
  process.stderr.write("\n\u2500\u2500 Checkmarx \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
7022
7141
  const useCheckmarx = await confirm({
7023
7142
  message: "Configure Checkmarx One for vulnerability scanning?",
@@ -7344,6 +7463,70 @@ async function initGlobalConfig(start) {
7344
7463
  }
7345
7464
  }
7346
7465
  }
7466
+ process.stderr.write("\n\u2500\u2500 Split.IO \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
7467
+ const useSplitio = await confirm({
7468
+ message: "Configure Split.IO for feature flag administration?",
7469
+ default: false
7470
+ });
7471
+ let splitioBaseUrl = "";
7472
+ let splitioAdminApiKey = "";
7473
+ if (useSplitio) {
7474
+ splitioBaseUrl = await input({
7475
+ message: "Split.IO Admin API base URL (e.g. https://api.split.io):",
7476
+ default: ""
7477
+ });
7478
+ splitioAdminApiKey = await password({
7479
+ message: "Split.IO Admin API key:"
7480
+ });
7481
+ if (splitioBaseUrl && splitioAdminApiKey) {
7482
+ process.stderr.write("\n Verifying connection...\n");
7483
+ try {
7484
+ const tempConfig = {
7485
+ ...loadConfig(),
7486
+ splitio: {
7487
+ baseUrl: normalizeBaseUrl(splitioBaseUrl),
7488
+ adminApiKey: splitioAdminApiKey
7489
+ }
7490
+ };
7491
+ const tempHttp = createHttpClient(tempConfig);
7492
+ await tempHttp.splitio("/internal/api/v2/workspaces");
7493
+ process.stderr.write(" Connected.\n");
7494
+ } catch (err) {
7495
+ warn(`Could not connect to Split.IO: ${err instanceof Error ? err.message : String(err)}`);
7496
+ warn("Config will be saved anyway. Check your URL and API key and re-run pncli config init or pncli config test.");
7497
+ }
7498
+ }
7499
+ }
7500
+ process.stderr.write("\n\u2500\u2500 Figma \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
7501
+ const useFigma = await confirm({
7502
+ message: "Configure Figma for reading design files and comments?",
7503
+ default: false
7504
+ });
7505
+ let figmaToken = "";
7506
+ if (useFigma) {
7507
+ process.stderr.write(" Generate a personal access token in Figma under Account Settings \u2192 Personal access tokens.\n");
7508
+ figmaToken = await password({
7509
+ message: "Figma personal access token:"
7510
+ });
7511
+ if (figmaToken) {
7512
+ process.stderr.write("\n Verifying connection...\n");
7513
+ try {
7514
+ const tempConfig = {
7515
+ ...loadConfig(),
7516
+ figma: {
7517
+ baseUrl: "https://api.figma.com",
7518
+ token: figmaToken
7519
+ }
7520
+ };
7521
+ const tempHttp = createHttpClient(tempConfig);
7522
+ await tempHttp.figma("/v1/me");
7523
+ process.stderr.write(" Connected.\n");
7524
+ } catch (err) {
7525
+ warn(`Could not connect to Figma: ${err instanceof Error ? err.message : String(err)}`);
7526
+ warn("Config will be saved anyway. Check your token and re-run pncli config init or pncli config test.");
7527
+ }
7528
+ }
7529
+ }
7347
7530
  process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
7348
7531
  const jiraProject = await input({
7349
7532
  message: "Default Jira project key (optional):",
@@ -7367,7 +7550,13 @@ async function initGlobalConfig(start) {
7367
7550
  process.exitCode = ExitCode.SUCCESS;
7368
7551
  return;
7369
7552
  }
7553
+ const existingGlobal = loadJsonFile(getGlobalConfigPath());
7554
+ const existingInstances = Array.isArray(existingGlobal?.jenkinsInstances) ? existingGlobal.jenkinsInstances : [];
7555
+ const existingDynatraceEnvs = existingGlobal?.dynatrace?.environments;
7556
+ const existingDynatraceDefault = existingGlobal?.dynatrace?.defaultEnvironment;
7557
+ const existingOpenshift = existingGlobal?.openshift;
7370
7558
  writeGlobalConfig({
7559
+ jenkinsInstances: existingInstances,
7371
7560
  user: {
7372
7561
  email: userEmail || void 0,
7373
7562
  userId: userId || void 0
@@ -7428,14 +7617,6 @@ async function initGlobalConfig(start) {
7428
7617
  ...jenkinsApiToken ? { apiToken: jenkinsApiToken } : {}
7429
7618
  }
7430
7619
  } : {},
7431
- ...useUdeploy && udeployBaseUrl ? {
7432
- udeploy: {
7433
- baseUrl: normalizeBaseUrl(udeployBaseUrl),
7434
- ...udeployPat ? { pat: udeployPat } : {},
7435
- ...udeployUsername ? { username: udeployUsername } : {},
7436
- ...udeployPassword ? { password: udeployPassword } : {}
7437
- }
7438
- } : {},
7439
7620
  ...useCheckmarx && checkmarxBaseUrl ? {
7440
7621
  checkmarx: {
7441
7622
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
@@ -7472,15 +7653,25 @@ async function initGlobalConfig(start) {
7472
7653
  ...useOpenShift && openShiftBaseUrl ? {
7473
7654
  openshift: {
7474
7655
  baseUrl: normalizeBaseUrl(openShiftBaseUrl),
7475
- token: openShiftToken || void 0
7656
+ token: openShiftToken || void 0,
7657
+ ...existingOpenshift?.defaultEnvironment ? { defaultEnvironment: existingOpenshift.defaultEnvironment } : {},
7658
+ ...existingOpenshift?.defaultInstance ? { defaultInstance: existingOpenshift.defaultInstance } : {},
7659
+ ...existingOpenshift?.environments ? { environments: existingOpenshift.environments } : {}
7476
7660
  }
7477
- } : {},
7661
+ } : existingOpenshift ? { openshift: { ...existingOpenshift } } : {},
7478
7662
  ...useDynatrace && dynatraceBaseUrl ? {
7479
7663
  dynatrace: {
7480
7664
  baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
7481
7665
  apiToken: dynatraceApiToken || void 0,
7482
7666
  platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
7483
- platformToken: dynatracePlatformToken || void 0
7667
+ platformToken: dynatracePlatformToken || void 0,
7668
+ ...existingDynatraceDefault ? { defaultEnvironment: existingDynatraceDefault } : {},
7669
+ ...existingDynatraceEnvs && Object.keys(existingDynatraceEnvs).length ? { environments: existingDynatraceEnvs } : {}
7670
+ }
7671
+ } : existingDynatraceEnvs && Object.keys(existingDynatraceEnvs).length ? {
7672
+ dynatrace: {
7673
+ ...existingDynatraceDefault ? { defaultEnvironment: existingDynatraceDefault } : {},
7674
+ environments: existingDynatraceEnvs
7484
7675
  }
7485
7676
  } : {},
7486
7677
  ...useLogscale && logscaleBaseUrl ? {
@@ -7489,6 +7680,18 @@ async function initGlobalConfig(start) {
7489
7680
  token: logscaleToken || void 0
7490
7681
  }
7491
7682
  } : {},
7683
+ ...useSplitio && splitioBaseUrl ? {
7684
+ splitio: {
7685
+ baseUrl: normalizeBaseUrl(splitioBaseUrl),
7686
+ adminApiKey: splitioAdminApiKey || void 0
7687
+ }
7688
+ } : {},
7689
+ ...useFigma && figmaToken ? {
7690
+ figma: {
7691
+ baseUrl: "https://api.figma.com",
7692
+ token: figmaToken
7693
+ }
7694
+ } : {},
7492
7695
  defaults: {
7493
7696
  jira: {
7494
7697
  project: jiraProject || void 0
@@ -7508,12 +7711,6 @@ async function initGlobalConfig(start) {
7508
7711
  collection: adoCollection || void 0,
7509
7712
  project: adoProject || void 0
7510
7713
  }
7511
- } : {},
7512
- ...useUdeploy && (udeployDefaultApp || udeployDefaultEnv) ? {
7513
- udeploy: {
7514
- application: udeployDefaultApp || void 0,
7515
- environment: udeployDefaultEnv || void 0
7516
- }
7517
7714
  } : {}
7518
7715
  }
7519
7716
  });
@@ -8245,10 +8442,10 @@ function registerAdoWorkCommands(ado) {
8245
8442
  const types = await discoverTypes(workClient, collection, project);
8246
8443
  if (opts.save) {
8247
8444
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
8248
- const fs5 = await import("fs");
8249
- const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
8445
+ const fs6 = await import("fs");
8446
+ const existing = JSON.parse(fs6.readFileSync(globalConfigPath, "utf8") || "{}");
8250
8447
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
8251
- fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
8448
+ fs6.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
8252
8449
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
8253
8450
  }
8254
8451
  success(types, "ado", "work-types", start);
@@ -8277,11 +8474,11 @@ function registerAdoWorkCommands(ado) {
8277
8474
  ) : allFields;
8278
8475
  if (opts.save) {
8279
8476
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
8280
- const fs5 = await import("fs");
8281
- const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
8477
+ const fs6 = await import("fs");
8478
+ const existing = JSON.parse(fs6.readFileSync(globalConfigPath, "utf8") || "{}");
8282
8479
  const aliases = buildDefaultAliases(allFields);
8283
8480
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
8284
- fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
8481
+ fs6.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
8285
8482
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
8286
8483
  }
8287
8484
  success(fields, "ado", "work-fields", start);
@@ -8578,11 +8775,11 @@ function registerAdoRepoCommands(ado) {
8578
8775
  const start = Date.now();
8579
8776
  try {
8580
8777
  const globalOpts = ado.optsWithGlobals();
8581
- const { loadConfig: lc } = await import("./config-SQ3YHJHI.js");
8778
+ const { loadConfig: lc } = await import("./config-JGBUUXQX.js");
8582
8779
  const config = lc({ configPath: globalOpts.config });
8583
8780
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
8584
8781
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
8585
- const { createHttpClient: createHttpClient2 } = await import("./http-WCDXXCKQ.js");
8782
+ const { createHttpClient: createHttpClient2 } = await import("./http-6QJU2SWP.js");
8586
8783
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
8587
8784
  const coreClient = new AdoCoreClient2(http);
8588
8785
  const me = await coreClient.getConnectionData(collection);
@@ -8840,11 +9037,22 @@ var JenkinsClient = class {
8840
9037
  };
8841
9038
 
8842
9039
  // src/services/jenkins/commands.ts
8843
- function getClient7(program2) {
8844
- const opts = program2.optsWithGlobals();
9040
+ function getClient7(cmd) {
9041
+ const opts = cmd.optsWithGlobals();
8845
9042
  const config = loadConfig({ configPath: opts.config });
8846
- if (!config.jenkins.baseUrl) throw new PncliError("Jenkins not configured. Run: pncli config init");
8847
- const http = createHttpClient(config, Boolean(opts.dryRun));
9043
+ let jenkins = config.jenkins;
9044
+ if (opts.instance) {
9045
+ const inst = config.jenkinsInstances.find((i) => i.name === opts.instance);
9046
+ if (!inst) throw new PncliError(`Jenkins instance "${opts.instance}" not found. Add it to your global config using pncli config set jenkinsInstances.`);
9047
+ jenkins = { baseUrl: inst.baseUrl, username: inst.username, apiToken: inst.apiToken };
9048
+ }
9049
+ if (!jenkins.baseUrl) {
9050
+ if (opts.instance) {
9051
+ throw new PncliError(`Jenkins instance "${opts.instance}" has no baseUrl configured. Add a baseUrl to that entry in jenkinsInstances.`);
9052
+ }
9053
+ throw new PncliError("Jenkins not configured. Run: pncli config init");
9054
+ }
9055
+ const http = createHttpClient({ ...config, jenkins }, Boolean(opts.dryRun));
8848
9056
  return new JenkinsClient(http);
8849
9057
  }
8850
9058
  async function pollQueueItem(client, queueItemId, pollMs, timeoutMs) {
@@ -8869,12 +9077,70 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
8869
9077
  throw new PncliError(`Timed out waiting for build #${buildNumber} to complete after ${timeoutMs / 1e3}s`, 1);
8870
9078
  }
8871
9079
  function registerJenkinsCommands(program2) {
8872
- const jenkins = program2.command("jenkins").description("Jenkins Data Center operations");
9080
+ const jenkins = program2.command("jenkins").description("Jenkins operations").option("--instance <name>", "Named Jenkins instance from global config (overrides default jenkins config)");
8873
9081
  const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
9082
+ const instance = jenkins.command("instance").description("Manage named Jenkins instances in the global config");
9083
+ instance.command("list").description("List configured Jenkins instances (API tokens masked)").action(() => {
9084
+ const start = Date.now();
9085
+ try {
9086
+ const opts = jenkins.optsWithGlobals();
9087
+ const config = loadConfig({ configPath: opts.config });
9088
+ const instances = config.jenkinsInstances.map((i) => ({
9089
+ ...i,
9090
+ apiToken: i.apiToken ? "***" : void 0
9091
+ }));
9092
+ success({ instances }, "jenkins", "instance-list", start);
9093
+ } catch (err) {
9094
+ fail(err, "jenkins", "instance-list", start);
9095
+ }
9096
+ });
9097
+ instance.command("add").description("Add a named Jenkins instance to the global config (appends \u2014 existing instances are preserved)").requiredOption("--name <name>", "Instance name used with --instance").requiredOption("--base-url <url>", "Jenkins base URL (e.g. jenkins.imagile.dev)").option("--username <user>", "Jenkins username").option("--api-token <token>", "Jenkins API token (prompted for when omitted on an interactive terminal, keeping it out of shell history)").option("--force", "Overwrite an instance that already uses this name").action(async (o) => {
9098
+ const start = Date.now();
9099
+ try {
9100
+ const opts = jenkins.optsWithGlobals();
9101
+ const configPath = opts.config;
9102
+ const existing = loadConfig({ configPath }).jenkinsInstances;
9103
+ const idx = existing.findIndex((i) => i.name === o.name);
9104
+ if (idx >= 0 && !o.force) {
9105
+ throw new PncliError(`Jenkins instance "${o.name}" already exists. Pass --force to overwrite it, or run: pncli jenkins instance remove --name ${o.name}`, 1);
9106
+ }
9107
+ let apiToken = o.apiToken;
9108
+ if (!apiToken && process.stdin.isTTY) {
9109
+ const { default: password2 } = await import("@inquirer/password");
9110
+ apiToken = await password2({ message: `API token for "${o.name}" (leave blank to skip):` });
9111
+ }
9112
+ const entry = {
9113
+ name: o.name,
9114
+ baseUrl: normalizeBaseUrl(o.baseUrl),
9115
+ ...o.username ? { username: o.username } : {},
9116
+ ...apiToken ? { apiToken } : {}
9117
+ };
9118
+ const next = idx >= 0 ? existing.map((i, n) => n === idx ? entry : i) : [...existing, entry];
9119
+ setConfigValue("jenkinsInstances", JSON.stringify(next), configPath);
9120
+ success({ name: entry.name, baseUrl: entry.baseUrl, overwritten: idx >= 0 }, "jenkins", "instance-add", start);
9121
+ } catch (err) {
9122
+ fail(err, "jenkins", "instance-add", start);
9123
+ }
9124
+ });
9125
+ instance.command("remove").description("Remove a named Jenkins instance from the global config").requiredOption("--name <name>", "Instance name to remove").action((o) => {
9126
+ const start = Date.now();
9127
+ try {
9128
+ const opts = jenkins.optsWithGlobals();
9129
+ const configPath = opts.config;
9130
+ const existing = loadConfig({ configPath }).jenkinsInstances;
9131
+ if (!existing.some((i) => i.name === o.name)) {
9132
+ throw new PncliError(`Jenkins instance "${o.name}" not found. Run: pncli jenkins instance list`, 1);
9133
+ }
9134
+ setConfigValue("jenkinsInstances", JSON.stringify(existing.filter((i) => i.name !== o.name)), configPath);
9135
+ success({ removed: o.name }, "jenkins", "instance-remove", start);
9136
+ } catch (err) {
9137
+ fail(err, "jenkins", "instance-remove", start);
9138
+ }
9139
+ });
8874
9140
  pipeline.command("list").description("List all jobs on the Jenkins instance (or within a folder)").option("--folder <name>", "Folder name to enumerate (supports nested folders: parentFolder/childFolder)").action(async (opts) => {
8875
9141
  const start = Date.now();
8876
9142
  try {
8877
- const client = getClient7(program2);
9143
+ const client = getClient7(jenkins);
8878
9144
  const data = await client.listJobs(opts.folder);
8879
9145
  success(data, "jenkins", "pipeline-list", start);
8880
9146
  } catch (err) {
@@ -8884,7 +9150,7 @@ function registerJenkinsCommands(program2) {
8884
9150
  pipeline.command("get").description("Get details for a specific job").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").action(async (opts) => {
8885
9151
  const start = Date.now();
8886
9152
  try {
8887
- const client = getClient7(program2);
9153
+ const client = getClient7(jenkins);
8888
9154
  const data = await client.getJob(opts.name);
8889
9155
  success(data, "jenkins", "pipeline-get", start);
8890
9156
  } catch (err) {
@@ -8897,7 +9163,7 @@ function registerJenkinsCommands(program2) {
8897
9163
  }, []).option("--wait", "Wait for the build to complete before returning").option("--timeout <s>", "Max wait time in seconds per phase (queue-wait and build-wait each get this budget; default 600)", "600").option("--poll <s>", "Poll interval in seconds (default 10)", "10").action(async (opts) => {
8898
9164
  const start = Date.now();
8899
9165
  try {
8900
- const client = getClient7(program2);
9166
+ const client = getClient7(jenkins);
8901
9167
  for (const p of opts.parameter) {
8902
9168
  if (!p.includes("=")) throw new PncliError(`Invalid --parameter "${p}". Expected format: key=value`, 1);
8903
9169
  }
@@ -8930,7 +9196,7 @@ function registerJenkinsCommands(program2) {
8930
9196
  pipeline.command("list-runs").description("List recent builds for a job").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").option("--top <n>", "Maximum number of builds to return (default 25)", "25").action(async (opts) => {
8931
9197
  const start = Date.now();
8932
9198
  try {
8933
- const client = getClient7(program2);
9199
+ const client = getClient7(jenkins);
8934
9200
  const top = parseInt(opts.top, 10);
8935
9201
  if (isNaN(top) || top <= 0) throw new PncliError(`Invalid --top "${opts.top}". Expected a positive integer.`, 1);
8936
9202
  const data = await client.listBuilds(opts.name, top);
@@ -8942,7 +9208,7 @@ function registerJenkinsCommands(program2) {
8942
9208
  pipeline.command("get-run").description("Get details for a specific build").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").requiredOption("--number <n>", "Build number").action(async (opts) => {
8943
9209
  const start = Date.now();
8944
9210
  try {
8945
- const client = getClient7(program2);
9211
+ const client = getClient7(jenkins);
8946
9212
  const buildNumber = parseInt(opts.number, 10);
8947
9213
  if (isNaN(buildNumber)) throw new PncliError(`Invalid --number "${opts.number}". Expected an integer.`, 1);
8948
9214
  const data = await client.getBuild(opts.name, buildNumber);
@@ -8954,7 +9220,7 @@ function registerJenkinsCommands(program2) {
8954
9220
  pipeline.command("logs").description("Fetch console log for a build (consider --output-file for large logs)").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").requiredOption("--number <n>", "Build number").action(async (opts) => {
8955
9221
  const start = Date.now();
8956
9222
  try {
8957
- const client = getClient7(program2);
9223
+ const client = getClient7(jenkins);
8958
9224
  const buildNumber = parseInt(opts.number, 10);
8959
9225
  if (isNaN(buildNumber)) throw new PncliError(`Invalid --number "${opts.number}". Expected an integer.`, 1);
8960
9226
  const text = await client.getConsoleLog(opts.name, buildNumber);
@@ -9031,7 +9297,7 @@ var ArtifactoryClient = class {
9031
9297
  }));
9032
9298
  const result = { builds, uri: raw.uri };
9033
9299
  if (builds.length === 0) {
9034
- result.hint = 'No builds are registered in Artifactory Build Integration. Build names may differ from ADO pipeline or uDeploy component names. Use "artifactory repos" to list repositories, then "artifactory search --repo <repo>" to locate artifacts directly.';
9300
+ result.hint = 'No builds are registered in Artifactory Build Integration. Build names may differ from ADO pipeline names. Use "artifactory repos" to list repositories, then "artifactory search --repo <repo>" to locate artifacts directly.';
9035
9301
  }
9036
9302
  return result;
9037
9303
  }
@@ -9134,7 +9400,7 @@ function registerArtifactoryCommands(program2) {
9134
9400
  fail(err, "artifactory", "search", start);
9135
9401
  }
9136
9402
  });
9137
- art.command("builds").description('List builds registered in Artifactory Build Integration (names may differ from ADO pipeline or uDeploy component names; use "search --repo" when empty)').option("--timeout <s>", "Request timeout in seconds (default 60; increase for large Artifactory instances)", "60").action(async (opts) => {
9403
+ art.command("builds").description('List builds registered in Artifactory Build Integration (names may differ from ADO pipeline names; use "search --repo" when empty)').option("--timeout <s>", "Request timeout in seconds (default 60; increase for large Artifactory instances)", "60").action(async (opts) => {
9138
9404
  const start = Date.now();
9139
9405
  try {
9140
9406
  const client = getClient8(program2);
@@ -9169,337 +9435,117 @@ function registerArtifactoryCommands(program2) {
9169
9435
  });
9170
9436
  }
9171
9437
 
9172
- // src/services/udeploy/client.ts
9173
- var UdeployClient = class {
9438
+ // src/services/checkmarx/client.ts
9439
+ var CheckmarxClient = class {
9174
9440
  constructor(http) {
9175
9441
  this.http = http;
9176
9442
  }
9177
9443
  http;
9178
- listApplications() {
9179
- return this.http.udeploy("/cli/application");
9180
- }
9181
- listEnvironments(application) {
9182
- return this.http.udeploy("/cli/application/environmentsInApplication", {
9183
- params: { application }
9184
- });
9185
- }
9186
- listComponents() {
9187
- return this.http.udeploy("/cli/component");
9188
- }
9189
- listVersions(component) {
9190
- return this.http.udeploy("/cli/component/versions", {
9191
- params: { component }
9192
- });
9193
- }
9194
- createVersion(component, name) {
9195
- return this.http.udeploy("/cli/version/createVersion", {
9196
- method: "POST",
9197
- params: { component, name }
9198
- });
9444
+ async listProjects() {
9445
+ const res = await this.http.checkmarx("projects", { params: { limit: 100 } });
9446
+ const projects = res.projects ?? [];
9447
+ if (res.filteredTotalCount > projects.length) {
9448
+ process.stderr.write(`warning: ${res.filteredTotalCount} projects found; only showing first ${projects.length}
9449
+ `);
9450
+ }
9451
+ return projects;
9199
9452
  }
9200
- finishImporting(component, version) {
9201
- return this.http.udeploy("/cli/version/finishedImporting", {
9202
- method: "POST",
9203
- params: { component, version }
9204
- });
9453
+ async getProject(id) {
9454
+ return this.http.checkmarx(`projects/${id}`);
9205
9455
  }
9206
- runProcess(opts) {
9207
- return this.http.udeploy("/cli/applicationProcessRequest/request", {
9208
- method: "PUT",
9209
- body: {
9210
- application: opts.application,
9211
- applicationProcess: opts.applicationProcess,
9212
- environment: opts.environment,
9213
- onlyChanged: opts.onlyChanged ?? false,
9214
- ...opts.versions ? { versions: opts.versions } : {},
9215
- ...opts.snapshot ? { snapshot: opts.snapshot } : {},
9216
- ...opts.properties ? { properties: opts.properties } : {}
9217
- }
9218
- });
9456
+ async listScans(opts = {}) {
9457
+ const params = { limit: opts.last ?? 100 };
9458
+ if (opts.projectId) params["project-id"] = opts.projectId;
9459
+ const res = await this.http.checkmarx("scans", { params });
9460
+ const scans = res.scans ?? [];
9461
+ if (res.filteredTotalCount > scans.length) {
9462
+ process.stderr.write(`warning: ${res.filteredTotalCount} scans found; only showing first ${scans.length}
9463
+ `);
9464
+ }
9465
+ return scans;
9219
9466
  }
9220
- getRequestStatus(requestId) {
9221
- return this.http.udeploy("/cli/applicationProcessRequest/requestStatus", {
9222
- params: { requestId }
9223
- });
9467
+ async getScan(id) {
9468
+ return this.http.checkmarx(`scans/${id}`);
9224
9469
  }
9225
- getRequestInfo(requestId) {
9226
- return this.http.udeploy("/cli/applicationProcessRequest/requestInfo", {
9227
- params: { requestId }
9470
+ async getScanResultsStatistics(scanId) {
9471
+ return this.http.checkmarx("results/summary", {
9472
+ params: { "scan-id": scanId }
9228
9473
  });
9229
9474
  }
9230
9475
  };
9231
9476
 
9232
- // src/services/udeploy/commands.ts
9233
- var POLL_INTERVAL_MS2 = 5e3;
9234
- var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
9477
+ // src/services/checkmarx/commands.ts
9235
9478
  function getClient9(program2) {
9236
9479
  const opts = program2.optsWithGlobals();
9237
9480
  const config = loadConfig({ configPath: opts.config });
9238
- if (!config.udeploy.baseUrl) {
9239
- throw new PncliError("UDeploy baseUrl not configured. Run: pncli config init", 78);
9240
- }
9241
- const http = createHttpClient(config, opts.dryRun);
9242
- return { client: new UdeployClient(http), config };
9243
- }
9244
- function resolveApplication(config, cliValue) {
9245
- const app = cliValue ?? config.defaults.udeploy.application;
9246
- if (!app) throw new PncliError("--application is required (or set defaults.udeploy.application in config)", 2);
9247
- return app;
9248
- }
9249
- function resolveEnvironment(config, cliValue) {
9250
- const env = cliValue ?? config.defaults.udeploy.environment;
9251
- if (!env) throw new PncliError("--environment is required (or set defaults.udeploy.environment in config)", 2);
9252
- return env;
9481
+ if (!config.checkmarx.baseUrl) throw new PncliError("Checkmarx not configured. Run: pncli config init");
9482
+ const http = createHttpClient(config, Boolean(opts.dryRun));
9483
+ return new CheckmarxClient(http);
9253
9484
  }
9254
- function registerUdeployCommands(program2) {
9255
- const udeploy = program2.command("udeploy").description("IBM UrbanCode Deploy \u2014 component versions and deployment processes").option("--application <name>", "Application name or ID").option("--environment <name>", "Environment name or ID");
9256
- udeploy.command("apps").description("List all applications").action(async () => {
9485
+ function registerCheckmarxCommands(program2) {
9486
+ const cx = program2.command("checkmarx").description("Checkmarx One operations");
9487
+ const project = cx.command("project").description("Project operations");
9488
+ const scan = cx.command("scan").description("Scan operations");
9489
+ project.command("list").description("List all projects").action(async () => {
9257
9490
  const start = Date.now();
9258
9491
  try {
9259
- const { client } = getClient9(program2);
9260
- const apps = await client.listApplications();
9261
- success(apps, "udeploy", "apps", start);
9492
+ const data = await getClient9(program2).listProjects();
9493
+ success(data, "checkmarx", "project-list", start);
9262
9494
  } catch (err) {
9263
- fail(err, "udeploy", "apps", start);
9495
+ fail(err, "checkmarx", "project-list", start);
9264
9496
  }
9265
9497
  });
9266
- udeploy.command("environments").description("List environments for an application").action(async () => {
9498
+ project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project UUID").action(async (opts) => {
9267
9499
  const start = Date.now();
9268
9500
  try {
9269
- const { client, config } = getClient9(program2);
9270
- const parentOpts = udeploy.opts();
9271
- const application = resolveApplication(config, parentOpts.application);
9272
- const envs = await client.listEnvironments(application);
9273
- success(envs, "udeploy", "environments", start);
9501
+ const data = await getClient9(program2).getProject(opts.id);
9502
+ success(data, "checkmarx", "project-get", start);
9274
9503
  } catch (err) {
9275
- fail(err, "udeploy", "environments", start);
9504
+ fail(err, "checkmarx", "project-get", start);
9276
9505
  }
9277
9506
  });
9278
- udeploy.command("components").description("List all components").action(async () => {
9507
+ scan.command("list").description("List scans").option("--project <id>", "Filter by project UUID").option("--last <n>", "Return only the last N scans").action(async (opts) => {
9279
9508
  const start = Date.now();
9280
9509
  try {
9281
- const { client } = getClient9(program2);
9282
- const components = await client.listComponents();
9283
- success(components, "udeploy", "components", start);
9510
+ const last = opts.last ? parseInt(opts.last, 10) : void 0;
9511
+ if (opts.last && isNaN(last)) throw new PncliError(`Invalid --last "${opts.last}". Expected an integer.`, 1);
9512
+ const data = await getClient9(program2).listScans({ projectId: opts.project, last });
9513
+ success(data, "checkmarx", "scan-list", start);
9284
9514
  } catch (err) {
9285
- fail(err, "udeploy", "components", start);
9515
+ fail(err, "checkmarx", "scan-list", start);
9286
9516
  }
9287
9517
  });
9288
- udeploy.command("versions").description("List versions for a component").requiredOption("--component <name>", "Component name or ID").action(async (cmdOpts) => {
9518
+ scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
9289
9519
  const start = Date.now();
9290
9520
  try {
9291
- const { client } = getClient9(program2);
9292
- const versions = await client.listVersions(cmdOpts.component);
9293
- success(versions, "udeploy", "versions", start);
9521
+ const data = await getClient9(program2).getScan(opts.id);
9522
+ success(data, "checkmarx", "scan-get", start);
9294
9523
  } catch (err) {
9295
- fail(err, "udeploy", "versions", start);
9524
+ fail(err, "checkmarx", "scan-get", start);
9296
9525
  }
9297
9526
  });
9298
- udeploy.command("import-version").description("Create a component version and mark it ready for deployment").requiredOption("--component <name>", "Component name or ID").requiredOption("--name <name>", "Version name").option("--no-finish", "Skip marking the version as finished importing").action(async (cmdOpts) => {
9527
+ scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
9299
9528
  const start = Date.now();
9300
9529
  try {
9301
- const { client } = getClient9(program2);
9302
- const created = await client.createVersion(cmdOpts.component, cmdOpts.name);
9303
- if (cmdOpts.finish) {
9304
- await client.finishImporting(cmdOpts.component, cmdOpts.name);
9305
- }
9306
- success({ ...created, finishedImporting: cmdOpts.finish }, "udeploy", "import-version", start);
9530
+ const data = await getClient9(program2).getScanResultsStatistics(opts.id);
9531
+ success(data, "checkmarx", "scan-stats", start);
9307
9532
  } catch (err) {
9308
- fail(err, "udeploy", "import-version", start);
9533
+ fail(err, "checkmarx", "scan-stats", start);
9309
9534
  }
9310
9535
  });
9311
- udeploy.command("run").description("Run an application deployment process").requiredOption("--process <name>", "Application process name or ID").option("--component <name>", "Component name (repeatable)", collect, []).option("--component-version <name>", "Component version (repeatable; positionally paired with --component)", collect, []).option("--snapshot <name>", "Snapshot name or ID (alternative to specifying versions)").option("--only-changed", "Deploy only changed components", false).option("--wait", "Poll until the process completes", false).option("--timeout <ms>", "Max wait time in milliseconds", String(DEFAULT_TIMEOUT_MS)).action(async (cmdOpts) => {
9312
- const start = Date.now();
9313
- try {
9314
- const { client, config } = getClient9(program2);
9315
- const parentOpts = udeploy.opts();
9316
- const application = resolveApplication(config, parentOpts.application);
9317
- const environment = resolveEnvironment(config, parentOpts.environment);
9318
- const components = cmdOpts.component;
9319
- const versions = cmdOpts.componentVersion;
9320
- const versionEntries = components.map((c, i) => ({
9321
- component: c,
9322
- version: versions[i] ?? "latest"
9323
- }));
9324
- const result = await client.runProcess({
9325
- application,
9326
- applicationProcess: cmdOpts.process,
9327
- environment,
9328
- onlyChanged: cmdOpts.onlyChanged,
9329
- ...versionEntries.length > 0 ? { versions: versionEntries } : {},
9330
- ...cmdOpts.snapshot ? { snapshot: cmdOpts.snapshot } : {}
9331
- });
9332
- if (!cmdOpts.wait) {
9333
- success(result, "udeploy", "run", start);
9334
- return;
9335
- }
9336
- const timeoutMs = parseInt(cmdOpts.timeout, 10);
9337
- const requestId = result.requestId;
9338
- log(`Process started. Request ID: ${requestId}. Polling for completion...`);
9339
- while (true) {
9340
- const elapsed = Date.now() - start;
9341
- if (elapsed >= timeoutMs) {
9342
- throw new PncliError(`Timed out after ${timeoutMs}ms waiting for request ${requestId}`, 1);
9343
- }
9344
- await sleep(POLL_INTERVAL_MS2);
9345
- const status = await client.getRequestStatus(requestId);
9346
- if (status.status === "CLOSED") {
9347
- const outcome = { requestId, status: status.status, result: status.result, elapsed_ms: Date.now() - start };
9348
- if (status.result === "SUCCEEDED") {
9349
- success(outcome, "udeploy", "run", start);
9350
- } else {
9351
- throw new PncliError(
9352
- `Deployment ${status.result ?? "failed"} (requestId: ${requestId})`,
9353
- 1
9354
- );
9355
- }
9356
- return;
9357
- }
9358
- log(`Status: ${status.status} (${Math.round((Date.now() - start) / 1e3)}s elapsed)`);
9359
- }
9360
- } catch (err) {
9361
- fail(err, "udeploy", "run", start);
9362
- }
9363
- });
9364
- udeploy.command("request-status").description("Get the current status of a deployment process request").requiredOption("--request-id <id>", "Request ID returned by udeploy run").action(async (cmdOpts) => {
9365
- const start = Date.now();
9366
- try {
9367
- const { client } = getClient9(program2);
9368
- const status = await client.getRequestStatus(cmdOpts.requestId);
9369
- success(status, "udeploy", "request-status", start);
9370
- } catch (err) {
9371
- fail(err, "udeploy", "request-status", start);
9372
- }
9373
- });
9374
- udeploy.command("request-info").description("Get full details of a deployment process request").requiredOption("--request-id <id>", "Request ID returned by udeploy run").action(async (cmdOpts) => {
9375
- const start = Date.now();
9376
- try {
9377
- const { client } = getClient9(program2);
9378
- const info = await client.getRequestInfo(cmdOpts.requestId);
9379
- success(info, "udeploy", "request-info", start);
9380
- } catch (err) {
9381
- fail(err, "udeploy", "request-info", start);
9382
- }
9383
- });
9384
- }
9385
- function collect(val, prev) {
9386
- return [...prev, val];
9387
- }
9388
- function sleep(ms) {
9389
- return new Promise((resolve) => setTimeout(resolve, ms));
9390
- }
9391
-
9392
- // src/services/checkmarx/client.ts
9393
- var CheckmarxClient = class {
9394
- constructor(http) {
9395
- this.http = http;
9396
- }
9397
- http;
9398
- async listProjects() {
9399
- const res = await this.http.checkmarx("projects", { params: { limit: 100 } });
9400
- const projects = res.projects ?? [];
9401
- if (res.filteredTotalCount > projects.length) {
9402
- process.stderr.write(`warning: ${res.filteredTotalCount} projects found; only showing first ${projects.length}
9403
- `);
9404
- }
9405
- return projects;
9406
- }
9407
- async getProject(id) {
9408
- return this.http.checkmarx(`projects/${id}`);
9409
- }
9410
- async listScans(opts = {}) {
9411
- const params = { limit: opts.last ?? 100 };
9412
- if (opts.projectId) params["project-id"] = opts.projectId;
9413
- const res = await this.http.checkmarx("scans", { params });
9414
- const scans = res.scans ?? [];
9415
- if (res.filteredTotalCount > scans.length) {
9416
- process.stderr.write(`warning: ${res.filteredTotalCount} scans found; only showing first ${scans.length}
9417
- `);
9418
- }
9419
- return scans;
9420
- }
9421
- async getScan(id) {
9422
- return this.http.checkmarx(`scans/${id}`);
9423
- }
9424
- async getScanResultsStatistics(scanId) {
9425
- return this.http.checkmarx("results/summary", {
9426
- params: { "scan-id": scanId }
9427
- });
9428
- }
9429
- };
9430
-
9431
- // src/services/checkmarx/commands.ts
9432
- function getClient10(program2) {
9433
- const opts = program2.optsWithGlobals();
9434
- const config = loadConfig({ configPath: opts.config });
9435
- if (!config.checkmarx.baseUrl) throw new PncliError("Checkmarx not configured. Run: pncli config init");
9436
- const http = createHttpClient(config, Boolean(opts.dryRun));
9437
- return new CheckmarxClient(http);
9438
- }
9439
- function registerCheckmarxCommands(program2) {
9440
- const cx = program2.command("checkmarx").description("Checkmarx One operations");
9441
- const project = cx.command("project").description("Project operations");
9442
- const scan = cx.command("scan").description("Scan operations");
9443
- project.command("list").description("List all projects").action(async () => {
9444
- const start = Date.now();
9445
- try {
9446
- const data = await getClient10(program2).listProjects();
9447
- success(data, "checkmarx", "project-list", start);
9448
- } catch (err) {
9449
- fail(err, "checkmarx", "project-list", start);
9450
- }
9451
- });
9452
- project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project UUID").action(async (opts) => {
9453
- const start = Date.now();
9454
- try {
9455
- const data = await getClient10(program2).getProject(opts.id);
9456
- success(data, "checkmarx", "project-get", start);
9457
- } catch (err) {
9458
- fail(err, "checkmarx", "project-get", start);
9459
- }
9460
- });
9461
- scan.command("list").description("List scans").option("--project <id>", "Filter by project UUID").option("--last <n>", "Return only the last N scans").action(async (opts) => {
9462
- const start = Date.now();
9463
- try {
9464
- const last = opts.last ? parseInt(opts.last, 10) : void 0;
9465
- if (opts.last && isNaN(last)) throw new PncliError(`Invalid --last "${opts.last}". Expected an integer.`, 1);
9466
- const data = await getClient10(program2).listScans({ projectId: opts.project, last });
9467
- success(data, "checkmarx", "scan-list", start);
9468
- } catch (err) {
9469
- fail(err, "checkmarx", "scan-list", start);
9470
- }
9471
- });
9472
- scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
9473
- const start = Date.now();
9474
- try {
9475
- const data = await getClient10(program2).getScan(opts.id);
9476
- success(data, "checkmarx", "scan-get", start);
9477
- } catch (err) {
9478
- fail(err, "checkmarx", "scan-get", start);
9479
- }
9480
- });
9481
- scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
9482
- const start = Date.now();
9483
- try {
9484
- const data = await getClient10(program2).getScanResultsStatistics(opts.id);
9485
- success(data, "checkmarx", "scan-stats", start);
9486
- } catch (err) {
9487
- fail(err, "checkmarx", "scan-stats", start);
9488
- }
9489
- });
9490
- }
9491
-
9492
- // src/services/servicenow/commands.ts
9493
- function getHttp(program2) {
9494
- const opts = program2.optsWithGlobals();
9495
- const config = loadConfig({ configPath: opts.config });
9496
- if (!config.servicenow.baseUrl) throw new PncliError("ServiceNow not configured. Run: pncli config init");
9497
- return createHttpClient(config, Boolean(opts.dryRun));
9498
- }
9499
- function registerServiceNowCommands(program2) {
9500
- const sn = program2.command("servicenow").description("ServiceNow operations");
9501
- const change = sn.command("change").description("Change request operations");
9502
- change.command("list").description("List change requests").option("--state <state>", "Filter by state (e.g. -1=Pending, 1=Open, 2=Work in Progress, 3=Closed Complete)").option("--assigned-to <user>", "Filter by assigned user sys_id or display name").option("--limit <n>", "Maximum number of results", "25").option("--fields <fields>", "Comma-separated field names to return").action(async (opts) => {
9536
+ }
9537
+
9538
+ // src/services/servicenow/commands.ts
9539
+ function getHttp(program2) {
9540
+ const opts = program2.optsWithGlobals();
9541
+ const config = loadConfig({ configPath: opts.config });
9542
+ if (!config.servicenow.baseUrl) throw new PncliError("ServiceNow not configured. Run: pncli config init");
9543
+ return createHttpClient(config, Boolean(opts.dryRun));
9544
+ }
9545
+ function registerServiceNowCommands(program2) {
9546
+ const sn = program2.command("servicenow").description("ServiceNow operations");
9547
+ const change = sn.command("change").description("Change request operations");
9548
+ change.command("list").description("List change requests").option("--state <state>", "Filter by state (e.g. -1=Pending, 1=Open, 2=Work in Progress, 3=Closed Complete)").option("--assigned-to <user>", "Filter by assigned user sys_id or display name").option("--limit <n>", "Maximum number of results", "25").option("--fields <fields>", "Comma-separated field names to return").action(async (opts) => {
9503
9549
  const start = Date.now();
9504
9550
  try {
9505
9551
  const http = getHttp(program2);
@@ -9770,24 +9816,51 @@ function assertInteractive(hint) {
9770
9816
  }
9771
9817
  }
9772
9818
  var AGENT_PATHS = {
9773
- "github-copilot": { project: ".agents/skills", user: path8.join(os.homedir(), ".agents/skills") },
9819
+ "codex": { project: ".agents/skills", user: path8.join(os.homedir(), ".agents/skills") },
9820
+ "github-copilot": { project: ".github/skills", user: path8.join(os.homedir(), ".copilot/skills") },
9774
9821
  "claude-code": { project: ".claude/skills", user: path8.join(os.homedir(), ".claude/skills") }
9775
9822
  };
9823
+ var DEFAULT_AGENT = "codex";
9824
+ var AGENT_CHOICES = Object.keys(AGENT_PATHS).join(" | ");
9825
+ var warnedRetargetedAgent = false;
9826
+ function warnIfRetargetedAgent(agentName) {
9827
+ if (agentName !== "github-copilot" || warnedRetargetedAgent) return;
9828
+ warnedRetargetedAgent = true;
9829
+ warn(`--agent github-copilot now targets .github/skills (project) and ~/.copilot/skills (user). It previously meant .agents/skills \u2014 use --agent codex for that.`);
9830
+ }
9776
9831
  function resolveAgentName(opts) {
9777
- return opts.claude ? "claude-code" : opts.agent ?? "github-copilot";
9832
+ return opts.claude ? "claude-code" : opts.agent ?? DEFAULT_AGENT;
9778
9833
  }
9779
9834
  function resolveAgentPaths(agentName) {
9780
9835
  const agentConfig = AGENT_PATHS[agentName];
9781
9836
  if (!agentConfig) {
9782
- throw new Error(`Unknown agent: "${agentName}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
9837
+ throw new Error(`Unknown agent: "${agentName}". Use: ${AGENT_CHOICES}`);
9783
9838
  }
9839
+ warnIfRetargetedAgent(agentName);
9784
9840
  return agentConfig;
9785
9841
  }
9842
+ function findGitRoot(from = process.cwd()) {
9843
+ try {
9844
+ const root = execFileSync3("git", ["-C", from, "rev-parse", "--show-toplevel"], {
9845
+ encoding: "utf8",
9846
+ stdio: ["pipe", "pipe", "pipe"]
9847
+ }).trim();
9848
+ return root ? path8.resolve(root) : null;
9849
+ } catch {
9850
+ return null;
9851
+ }
9852
+ }
9853
+ function resolveProjectPath(relativePath) {
9854
+ const root = findGitRoot();
9855
+ return root ? path8.resolve(root, relativePath) : path8.resolve(relativePath);
9856
+ }
9857
+ function resolveScopedPath(agentPaths, scope) {
9858
+ return scope === "project" ? resolveProjectPath(agentPaths.project) : path8.resolve(agentPaths.user);
9859
+ }
9786
9860
  function resolveTargetDir(opts) {
9787
9861
  if (opts.target) return path8.resolve(opts.target);
9788
9862
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9789
- const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
9790
- return path8.resolve(scopePath);
9863
+ return resolveScopedPath(agentConfig, opts.scope ?? "user");
9791
9864
  }
9792
9865
  function getBundledSkillsDir() {
9793
9866
  try {
@@ -9839,14 +9912,16 @@ function resolveSkillRecord(meta, targetDir, skillName) {
9839
9912
  };
9840
9913
  }
9841
9914
  function listActiveSkillDirs(targetDir) {
9915
+ if (!fs4.existsSync(targetDir)) return [];
9842
9916
  return fs4.readdirSync(targetDir).filter((name) => {
9843
9917
  if (name.startsWith(".")) return false;
9844
- const p = path8.join(targetDir, name);
9918
+ const dir = path8.join(targetDir, name);
9845
9919
  try {
9846
- return fs4.statSync(p).isDirectory();
9920
+ if (!fs4.statSync(dir).isDirectory()) return false;
9847
9921
  } catch {
9848
9922
  return false;
9849
9923
  }
9924
+ return fs4.existsSync(path8.join(dir, "SKILL.md"));
9850
9925
  });
9851
9926
  }
9852
9927
  function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
@@ -9958,6 +10033,142 @@ function detectRepoBranch(repoPath) {
9958
10033
  return void 0;
9959
10034
  }
9960
10035
  }
10036
+ function getRepoRemoteUrl(repoPath) {
10037
+ try {
10038
+ const raw = execFileSync3("git", ["-C", repoPath, "remote", "get-url", "origin"], {
10039
+ encoding: "utf8",
10040
+ stdio: ["pipe", "pipe", "pipe"]
10041
+ }).trim();
10042
+ return raw ? scrubToken(raw) : null;
10043
+ } catch {
10044
+ return null;
10045
+ }
10046
+ }
10047
+ function getInstalledPluginsForMarketplace(targetDir, marketplaceName, repoUrl) {
10048
+ const meta = readInstalledMeta(targetDir);
10049
+ const plugins = /* @__PURE__ */ new Set();
10050
+ for (const records of [meta.skills, meta.disabled ?? {}]) {
10051
+ for (const record of Object.values(records)) {
10052
+ if (record.source === "marketplace" && record.plugin) {
10053
+ if (record.marketplace === marketplaceName || repoUrl && record.installedFrom === repoUrl) {
10054
+ plugins.add(record.plugin);
10055
+ }
10056
+ }
10057
+ }
10058
+ }
10059
+ return [...plugins];
10060
+ }
10061
+ function readCustomTargets(globalConfig) {
10062
+ const raw = globalConfig.skillsTargets;
10063
+ if (!Array.isArray(raw)) return [];
10064
+ return [...new Set(raw.filter((t) => typeof t === "string" && t.length > 0).map((t) => path8.resolve(t)))];
10065
+ }
10066
+ function rememberCustomTarget(configPath, targetDir) {
10067
+ const resolved = path8.resolve(targetDir);
10068
+ const builtIn = Object.values(AGENT_PATHS).flatMap((a) => [path8.resolve(a.user), resolveProjectPath(a.project)]);
10069
+ if (builtIn.includes(resolved)) return false;
10070
+ const existing = loadJsonFile(configPath) ?? {};
10071
+ const targets = readCustomTargets(existing);
10072
+ if (targets.includes(resolved)) return false;
10073
+ writeGlobalConfig({ ...existing, skillsTargets: [...targets, resolved] }, configPath);
10074
+ return true;
10075
+ }
10076
+ function forgetCustomTarget(configPath, targetDir) {
10077
+ const resolved = path8.resolve(targetDir);
10078
+ const existing = loadJsonFile(configPath) ?? {};
10079
+ const targets = readCustomTargets(existing);
10080
+ if (!targets.includes(resolved)) return false;
10081
+ writeGlobalConfig({ ...existing, skillsTargets: targets.filter((t) => t !== resolved) }, configPath);
10082
+ return true;
10083
+ }
10084
+ function summarizeLocation(agent, scope, targetDir) {
10085
+ const resolved = path8.resolve(targetDir);
10086
+ const base = {
10087
+ agent,
10088
+ scope,
10089
+ path: resolved,
10090
+ exists: fs4.existsSync(resolved),
10091
+ totalSkills: 0,
10092
+ marketplaceSkills: 0,
10093
+ bundledSkills: 0,
10094
+ untrackedSkills: 0,
10095
+ disabledSkills: 0,
10096
+ disabledStashMissing: []
10097
+ };
10098
+ if (!base.exists) return base;
10099
+ const meta = readInstalledMeta(resolved);
10100
+ for (const skillName of listActiveSkillDirs(resolved)) {
10101
+ base.totalSkills++;
10102
+ const record = resolveSkillRecord(meta, resolved, skillName);
10103
+ if (record?.source === "marketplace") base.marketplaceSkills++;
10104
+ else if (record?.source === "bundled") base.bundledSkills++;
10105
+ else base.untrackedSkills++;
10106
+ }
10107
+ const disabledNames = Object.keys(meta.disabled ?? {});
10108
+ base.disabledSkills = disabledNames.length;
10109
+ const stashDir = path8.join(resolved, DISABLED_SUBDIR);
10110
+ base.disabledStashMissing = disabledNames.filter((name) => !fs4.existsSync(path8.join(stashDir, name)));
10111
+ return base;
10112
+ }
10113
+ function listKnownLocations(globalConfig) {
10114
+ const builtIn = Object.entries(AGENT_PATHS).flatMap(
10115
+ ([agent, paths]) => ["project", "user"].map((scope) => summarizeLocation(agent, scope, resolveScopedPath(paths, scope)))
10116
+ );
10117
+ const seen = new Set(builtIn.map((l) => l.path));
10118
+ const custom = readCustomTargets(globalConfig).filter((dir) => !seen.has(dir)).map((dir) => summarizeLocation("custom", "custom", dir));
10119
+ return [...builtIn, ...custom];
10120
+ }
10121
+ function buildMarketplaceIndex(marketplaces) {
10122
+ const byName = /* @__PURE__ */ new Map();
10123
+ const byUrl = /* @__PURE__ */ new Map();
10124
+ for (const m of marketplaces) {
10125
+ const name = marketplaceLabel(m);
10126
+ if (name) byName.set(name, m);
10127
+ if (m.repoUrl) byUrl.set(m.repoUrl, m);
10128
+ }
10129
+ return { byName, byUrl };
10130
+ }
10131
+ function collectSkillStatus(globalConfig, locations) {
10132
+ const { byName, byUrl } = buildMarketplaceIndex(getAllMarketplaces(globalConfig));
10133
+ const remoteCache = /* @__PURE__ */ new Map();
10134
+ const remoteFor = (localPath) => {
10135
+ if (!localPath || !fs4.existsSync(localPath)) return null;
10136
+ if (!remoteCache.has(localPath)) remoteCache.set(localPath, getRepoRemoteUrl(localPath));
10137
+ return remoteCache.get(localPath) ?? null;
10138
+ };
10139
+ const records = [];
10140
+ for (const location of locations) {
10141
+ if (!location.exists) continue;
10142
+ const meta = readInstalledMeta(location.path);
10143
+ const push = (skillName, record, enabled) => {
10144
+ const recordedName = record?.marketplace ?? null;
10145
+ const registered = (recordedName ? byName.get(recordedName) : void 0) ?? (record?.installedFrom ? byUrl.get(record.installedFrom) : void 0) ?? null;
10146
+ records.push({
10147
+ skill: skillName,
10148
+ path: enabled ? path8.join(location.path, skillName) : path8.join(location.path, DISABLED_SUBDIR, skillName),
10149
+ agent: location.agent,
10150
+ scope: location.scope,
10151
+ location: location.path,
10152
+ enabled,
10153
+ source: record?.source ?? "untracked",
10154
+ plugin: record?.plugin ?? null,
10155
+ marketplace: recordedName ?? (registered ? marketplaceLabel(registered) : null),
10156
+ repoUrl: registered?.repoUrl ?? record?.installedFrom ?? null,
10157
+ localPath: registered?.localPath ?? null,
10158
+ upstreamRemote: remoteFor(registered?.localPath),
10159
+ branch: record?.branch ?? null,
10160
+ installedAt: record?.installedAt ?? null
10161
+ });
10162
+ };
10163
+ for (const skillName of listActiveSkillDirs(location.path)) {
10164
+ push(skillName, resolveSkillRecord(meta, location.path, skillName), true);
10165
+ }
10166
+ for (const [skillName, record] of Object.entries(meta.disabled ?? {})) {
10167
+ push(skillName, record, false);
10168
+ }
10169
+ }
10170
+ return records;
10171
+ }
9961
10172
  function recordInstalledSkills(targetDir, skillNames, record) {
9962
10173
  if (skillNames.length === 0) return;
9963
10174
  const meta = readInstalledMeta(targetDir);
@@ -10121,7 +10332,7 @@ function installAllPlugins(resolvedPath, marketplaceName, url, targetDir) {
10121
10332
  const { results, totalInstalled } = installPluginsForMarketplace(resolvedPath, marketplaceName, url, pluginChoices.map((p) => p.name), targetDir);
10122
10333
  return { pluginResults: results, totalInstalled };
10123
10334
  }
10124
- function syncMarketplacePlugins(m, targetDir, force, pluginFilter) {
10335
+ function syncMarketplacePlugins(m, targetDir, force, pluginFilter, installedOnly = false) {
10125
10336
  const marketplaceName = marketplaceLabel(m);
10126
10337
  try {
10127
10338
  const marketplacePath = m.localPath;
@@ -10140,7 +10351,16 @@ function syncMarketplacePlugins(m, targetDir, force, pluginFilter) {
10140
10351
  }
10141
10352
  let pluginNames;
10142
10353
  if (pluginFilter === "all") {
10143
- pluginNames = pluginChoices.map((p) => p.name);
10354
+ if (installedOnly) {
10355
+ const installed = getInstalledPluginsForMarketplace(targetDir, marketplaceName, m.repoUrl).filter((name) => pluginChoices.some((p) => p.name === name));
10356
+ if (installed.length === 0) {
10357
+ warn(`No installed plugins found for "${marketplaceName}" \u2014 skipping. Run marketplace sync without --installed-only to install plugins.`);
10358
+ return { marketplace: marketplaceName, skipped: true, installedOnly: true, message: "No installed plugins found \u2014 nothing to sync." };
10359
+ }
10360
+ pluginNames = installed;
10361
+ } else {
10362
+ pluginNames = pluginChoices.map((p) => p.name);
10363
+ }
10144
10364
  } else if (pluginChoices.some((p) => p.name === pluginFilter)) {
10145
10365
  pluginNames = [pluginFilter];
10146
10366
  } else {
@@ -10210,7 +10430,7 @@ async function marketplaceAddAction(url, localPath, opts, commandName) {
10210
10430
  }
10211
10431
  function registerSkillsCommands(program2) {
10212
10432
  const skills = program2.command("skills").description("Manage pncli Claude Code skills");
10213
- skills.command("install").description("Install pncli skills into the current repo").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts) => {
10433
+ skills.command("install").description("Install pncli skills into the current repo").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts) => {
10214
10434
  const start = Date.now();
10215
10435
  try {
10216
10436
  let targetDir;
@@ -10218,8 +10438,7 @@ function registerSkillsCommands(program2) {
10218
10438
  targetDir = path8.resolve(opts.target);
10219
10439
  } else {
10220
10440
  const agentConfig = resolveAgentPaths(opts.agent);
10221
- const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
10222
- targetDir = path8.resolve(scopePath);
10441
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
10223
10442
  }
10224
10443
  const resolvedTarget = path8.resolve(targetDir);
10225
10444
  const bundledDir = getBundledSkillsDir();
@@ -10268,6 +10487,14 @@ function registerSkillsCommands(program2) {
10268
10487
  warn(`Failed to install: ${failed.join(", ")}`);
10269
10488
  }
10270
10489
  recordInstalledSkills(targetDir, installed, { source: "bundled" });
10490
+ let tracked = false;
10491
+ if (opts.target && installed.length > 0) {
10492
+ try {
10493
+ tracked = rememberCustomTarget(getGlobalConfigPath(), targetDir);
10494
+ } catch {
10495
+ warn(`Installed to ${targetDir}, but could not record it in the global config; it will not appear in "skills locations".`);
10496
+ }
10497
+ }
10271
10498
  success({
10272
10499
  installed,
10273
10500
  failed,
@@ -10275,13 +10502,14 @@ function registerSkillsCommands(program2) {
10275
10502
  total: installed.length,
10276
10503
  agent: opts.target ? "custom" : opts.agent,
10277
10504
  scope: opts.target ? "custom" : opts.scope,
10278
- source: "bundled"
10505
+ source: "bundled",
10506
+ ...opts.target ? { trackedAsCustomTarget: tracked } : {}
10279
10507
  }, "skills", "install", start);
10280
10508
  } catch (err) {
10281
10509
  fail(err, "skills", "install", start);
10282
10510
  }
10283
10511
  });
10284
- skills.command("list").description("List locally installed skills").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override skills directory to scan").action((opts) => {
10512
+ skills.command("list").description("List locally installed skills").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override skills directory to scan").action((opts) => {
10285
10513
  const start = Date.now();
10286
10514
  try {
10287
10515
  let targetDir;
@@ -10289,8 +10517,7 @@ function registerSkillsCommands(program2) {
10289
10517
  targetDir = path8.resolve(opts.target);
10290
10518
  } else {
10291
10519
  const agentConfig = resolveAgentPaths(opts.agent);
10292
- const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
10293
- targetDir = path8.resolve(scopePath);
10520
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
10294
10521
  }
10295
10522
  if (!fs4.existsSync(targetDir)) {
10296
10523
  success({ skills: [], message: `No skills directory found at ${targetDir}. Run: pncli skills install` }, "skills", "list", start);
@@ -10342,7 +10569,7 @@ function registerSkillsCommands(program2) {
10342
10569
  fail(err, "skills", "list", start);
10343
10570
  }
10344
10571
  });
10345
- skills.command("uninstall").description("Uninstall a skill (defaults to user scope, matching the marketplace install target; pass --scope project for skills installed via `skills install`)").argument("<name>", "Skill name to uninstall (the directory name under your skills folder)").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope to uninstall from: project | user", "user").option("--claude", "Shorthand for --agent claude-code").option("--target <dir>", "Override skills directory").action((name, opts) => {
10572
+ skills.command("uninstall").description("Uninstall a skill (defaults to user scope, matching the marketplace install target; pass --scope project for skills installed via `skills install`)").argument("<name>", "Skill name to uninstall (the directory name under your skills folder)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope to uninstall from: project | user", "user").option("--claude", "Shorthand for --agent claude-code").option("--target <dir>", "Override skills directory").action((name, opts) => {
10346
10573
  const start = Date.now();
10347
10574
  try {
10348
10575
  let targetDir;
@@ -10350,8 +10577,7 @@ function registerSkillsCommands(program2) {
10350
10577
  targetDir = path8.resolve(opts.target);
10351
10578
  } else {
10352
10579
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
10353
- const scopePath = opts.scope === "project" ? agentConfig.project : agentConfig.user;
10354
- targetDir = path8.resolve(scopePath);
10580
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
10355
10581
  }
10356
10582
  const resolvedTarget = path8.resolve(targetDir);
10357
10583
  const skillDir = path8.resolve(targetDir, name);
@@ -10376,9 +10602,77 @@ function registerSkillsCommands(program2) {
10376
10602
  fail(err, "skills", "uninstall", start);
10377
10603
  }
10378
10604
  });
10605
+ skills.command("locations").description("List every skills install path pncli knows about, with per-directory skill counts").action(() => {
10606
+ const start = Date.now();
10607
+ try {
10608
+ const globalConfig = loadJsonFile(getGlobalConfigPath()) ?? {};
10609
+ const locations = listKnownLocations(globalConfig);
10610
+ const gitRoot = findGitRoot();
10611
+ success({
10612
+ locations,
10613
+ activeLocations: locations.filter((l) => l.exists).length,
10614
+ totalSkills: locations.reduce((sum, l) => sum + l.totalSkills, 0),
10615
+ defaultAgent: DEFAULT_AGENT,
10616
+ projectRoot: gitRoot
10617
+ }, "skills", "locations", start);
10618
+ } catch (err) {
10619
+ fail(err, "skills", "locations", start);
10620
+ }
10621
+ });
10622
+ skills.command("status").description("Show every installed skill with its plugin, marketplace, and upstream remote").option("--marketplace <name>", "Only show skills installed from this marketplace (name or clone URL)").option("--plugin <name>", "Only show skills belonging to this plugin").option("--source <source>", "Filter by provenance: marketplace | bundled | untracked").option("--agent <agent>", `Only show locations for this agent host: ${AGENT_CHOICES}`).option("--scope <scope>", "Only show this scope: project | user | custom").action((opts) => {
10623
+ const start = Date.now();
10624
+ try {
10625
+ if (opts.agent && !AGENT_PATHS[opts.agent]) {
10626
+ throw new Error(`Unknown agent: "${opts.agent}". Use: ${AGENT_CHOICES}`);
10627
+ }
10628
+ if (opts.source && !["marketplace", "bundled", "untracked"].includes(opts.source)) {
10629
+ throw new Error(`Unknown source: "${opts.source}". Use: marketplace | bundled | untracked`);
10630
+ }
10631
+ if (opts.scope && !["project", "user", "custom"].includes(opts.scope)) {
10632
+ throw new Error(`Unknown scope: "${opts.scope}". Use: project | user | custom`);
10633
+ }
10634
+ const globalConfig = loadJsonFile(getGlobalConfigPath()) ?? {};
10635
+ const locations = listKnownLocations(globalConfig).filter((l) => !opts.agent || l.agent === opts.agent).filter((l) => !opts.scope || l.scope === opts.scope);
10636
+ const skillRecords = collectSkillStatus(globalConfig, locations).filter((r) => !opts.plugin || r.plugin === opts.plugin).filter((r) => !opts.source || r.source === opts.source).filter((r) => !opts.marketplace || r.marketplace === opts.marketplace || r.repoUrl === opts.marketplace).sort((a, b) => a.location.localeCompare(b.location) || a.skill.localeCompare(b.skill));
10637
+ const byMarketplace = {};
10638
+ for (const record of skillRecords) {
10639
+ const key = record.marketplace ?? "(none)";
10640
+ byMarketplace[key] = (byMarketplace[key] ?? 0) + 1;
10641
+ }
10642
+ success({
10643
+ skills: skillRecords,
10644
+ total: skillRecords.length,
10645
+ enabled: skillRecords.filter((r) => r.enabled).length,
10646
+ disabled: skillRecords.filter((r) => !r.enabled).length,
10647
+ untracked: skillRecords.filter((r) => r.source === "untracked").length,
10648
+ byMarketplace,
10649
+ locations
10650
+ }, "skills", "status", start);
10651
+ } catch (err) {
10652
+ fail(err, "skills", "status", start);
10653
+ }
10654
+ });
10655
+ skills.command("forget-target").description("Stop tracking a custom --target directory in skills locations / status (does not delete any files)").argument("<dir>", "The custom install directory to forget").action((dir) => {
10656
+ const start = Date.now();
10657
+ try {
10658
+ const configPath = getGlobalConfigPath();
10659
+ const resolved = path8.resolve(dir);
10660
+ const forgotten = forgetCustomTarget(configPath, resolved);
10661
+ if (!forgotten) {
10662
+ warn(`${resolved} is not a tracked custom target \u2014 nothing to forget.`);
10663
+ }
10664
+ success({
10665
+ target: resolved,
10666
+ forgotten,
10667
+ remaining: readCustomTargets(loadJsonFile(configPath) ?? {})
10668
+ }, "skills", "forget-target", start);
10669
+ } catch (err) {
10670
+ fail(err, "skills", "forget-target", start);
10671
+ }
10672
+ });
10379
10673
  const marketplace = skills.command("marketplace").description("Manage git-hosted skills marketplaces");
10380
10674
  function withMarketplaceAddOptions(cmd) {
10381
- return cmd.argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", "Target agent host for plugin install: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code");
10675
+ return cmd.argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code");
10382
10676
  }
10383
10677
  withMarketplaceAddOptions(
10384
10678
  marketplace.command("add").description("Register a new marketplace, clone it, and install all its plugins")
@@ -10396,7 +10690,8 @@ function registerSkillsCommands(program2) {
10396
10690
  name: marketplaceLabel(m),
10397
10691
  repoUrl: m.repoUrl,
10398
10692
  localPath: m.localPath,
10399
- tokenConfigured: !!m.token
10693
+ tokenConfigured: !!m.token,
10694
+ upstreamRemote: m.localPath && fs4.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
10400
10695
  })),
10401
10696
  total: all.length
10402
10697
  }, "skills", "marketplace-list", start);
@@ -10434,7 +10729,7 @@ function registerSkillsCommands(program2) {
10434
10729
  fail(err, "skills", "marketplace-remove", start);
10435
10730
  }
10436
10731
  });
10437
- marketplace.command("sync").description("Pull latest marketplace content and install plugin skills (interactive picker when no plugin is given)").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').action(async (plugin, opts) => {
10732
+ marketplace.command("sync").description("Pull latest marketplace content and install plugin skills (interactive picker when no plugin is given)").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').option("--installed-only", "Only sync plugins that are already installed \u2014 skip plugins newly added to the marketplace").action(async (plugin, opts) => {
10438
10733
  const start = Date.now();
10439
10734
  try {
10440
10735
  const configPath = getGlobalConfigPath();
@@ -10444,8 +10739,9 @@ function registerSkillsCommands(program2) {
10444
10739
  }
10445
10740
  const targetDir = resolveAgentPaths(resolveAgentName(opts)).user;
10446
10741
  const force = opts.force ?? false;
10742
+ const installedOnly = opts.installedOnly ?? false;
10447
10743
  if (opts.marketplace === "all") {
10448
- const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, plugin ?? "all"));
10744
+ const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, plugin ?? "all", installedOnly));
10449
10745
  success({ allMarketplaces: true, marketplaces: results, target: targetDir }, "skills", "marketplace-sync", start);
10450
10746
  return;
10451
10747
  }
@@ -10473,7 +10769,7 @@ function registerSkillsCommands(program2) {
10473
10769
  ]
10474
10770
  });
10475
10771
  if (chosen === ALL_MARKETPLACES) {
10476
- const results2 = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, selectedPlugin ?? "all"));
10772
+ const results2 = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, selectedPlugin ?? "all", installedOnly));
10477
10773
  success({ allMarketplaces: true, marketplaces: results2, target: targetDir }, "skills", "marketplace-sync", start);
10478
10774
  return;
10479
10775
  }
@@ -10520,10 +10816,29 @@ function registerSkillsCommands(program2) {
10520
10816
  }, "skills", "marketplace-sync", start);
10521
10817
  return;
10522
10818
  }
10523
- const pluginNames = selectedPlugin === "all" ? pluginChoices.map((p) => p.name) : [selectedPlugin];
10819
+ let pluginNames;
10820
+ if (selectedPlugin === "all") {
10821
+ if (installedOnly) {
10822
+ const installed = getInstalledPluginsForMarketplace(targetDir, marketplaceName, selectedMarketplace.repoUrl).filter((name) => pluginChoices.some((p) => p.name === name));
10823
+ if (installed.length === 0) {
10824
+ success({
10825
+ marketplace: marketplaceName,
10826
+ skipped: true,
10827
+ installedOnly: true,
10828
+ message: `No installed plugins found for "${marketplaceName}" \u2014 run marketplace sync without --installed-only to install plugins.`
10829
+ }, "skills", "marketplace-sync", start);
10830
+ return;
10831
+ }
10832
+ pluginNames = installed;
10833
+ } else {
10834
+ pluginNames = pluginChoices.map((p) => p.name);
10835
+ }
10836
+ } else {
10837
+ pluginNames = [selectedPlugin];
10838
+ }
10524
10839
  const { results, totalInstalled } = installPluginsForMarketplace(marketplacePath, marketplaceName, selectedMarketplace.repoUrl, pluginNames, targetDir);
10525
10840
  if (selectedPlugin === "all") {
10526
- success({ marketplace: marketplaceName, plugins: results, total: totalInstalled, target: targetDir, marketplaceUpdated: updated }, "skills", "marketplace-sync", start);
10841
+ success({ marketplace: marketplaceName, plugins: results, total: totalInstalled, target: targetDir, marketplaceUpdated: updated, ...installedOnly ? { installedOnly: true } : {} }, "skills", "marketplace-sync", start);
10527
10842
  } else {
10528
10843
  const single = results[selectedPlugin] ?? { installed: [], failed: [] };
10529
10844
  success({
@@ -10542,7 +10857,7 @@ function registerSkillsCommands(program2) {
10542
10857
  fail(err, "skills", "marketplace-sync", start);
10543
10858
  }
10544
10859
  });
10545
- marketplace.command("purge-plugin").description("Remove all skills installed from a specific plugin (or all plugins in a marketplace)").argument("<plugin>", 'Plugin name to purge, or "all" to remove every skill from the marketplace').option("--marketplace <name>", "Restrict purge to skills from this marketplace (by name or repo URL)").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10860
+ marketplace.command("purge-plugin").description("Remove all skills installed from a specific plugin (or all plugins in a marketplace)").argument("<plugin>", 'Plugin name to purge, or "all" to remove every skill from the marketplace').option("--marketplace <name>", "Restrict purge to skills from this marketplace (by name or repo URL)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10546
10861
  const start = Date.now();
10547
10862
  try {
10548
10863
  let targetDir;
@@ -10550,8 +10865,7 @@ function registerSkillsCommands(program2) {
10550
10865
  targetDir = path8.resolve(opts.target);
10551
10866
  } else {
10552
10867
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
10553
- const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
10554
- targetDir = path8.resolve(scopePath);
10868
+ targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
10555
10869
  }
10556
10870
  const resolvedTarget = path8.resolve(targetDir);
10557
10871
  if (!fs4.existsSync(targetDir)) {
@@ -10616,7 +10930,7 @@ function registerSkillsCommands(program2) {
10616
10930
  fail(err, "skills", "marketplace-purge-plugin", start);
10617
10931
  }
10618
10932
  });
10619
- marketplace.command("disable").description("Temporarily deactivate a plugin's skills without deleting them (re-enable with `marketplace enable`)").argument("<plugin>", "Plugin name to disable").option("--marketplace <name>", "Restrict to skills from this marketplace (by name or repo URL)").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10933
+ marketplace.command("disable").description("Temporarily deactivate a plugin's skills without deleting them (re-enable with `marketplace enable`)").argument("<plugin>", "Plugin name to disable").option("--marketplace <name>", "Restrict to skills from this marketplace (by name or repo URL)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10620
10934
  const start = Date.now();
10621
10935
  try {
10622
10936
  const targetDir = resolveTargetDir(opts);
@@ -10639,7 +10953,7 @@ function registerSkillsCommands(program2) {
10639
10953
  fail(err, "skills", "marketplace-disable", start);
10640
10954
  }
10641
10955
  });
10642
- marketplace.command("enable").description("Re-activate a previously disabled plugin's skills").argument("<plugin>", "Plugin name to enable").option("--marketplace <name>", "Restrict to skills from this marketplace (by name or repo URL)").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10956
+ marketplace.command("enable").description("Re-activate a previously disabled plugin's skills").argument("<plugin>", "Plugin name to enable").option("--marketplace <name>", "Restrict to skills from this marketplace (by name or repo URL)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
10643
10957
  const start = Date.now();
10644
10958
  try {
10645
10959
  const targetDir = resolveTargetDir(opts);
@@ -10668,7 +10982,7 @@ function registerSkillsCommands(program2) {
10668
10982
  fail(err, "skills", "marketplace-enable", start);
10669
10983
  }
10670
10984
  });
10671
- marketplace.command("manage").description("Manage marketplaces and plugins: toggle plugins on/off, add or remove marketplaces (interactive)").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action(async (opts) => {
10985
+ marketplace.command("manage").description("Manage marketplaces and plugins: toggle plugins on/off, add or remove marketplaces (interactive)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action(async (opts) => {
10672
10986
  const start = Date.now();
10673
10987
  try {
10674
10988
  assertInteractive("Use `pncli skills marketplace enable|disable <plugin>`, `add <url>`, and `remove <name>` to manage non-interactively.");
@@ -10771,7 +11085,7 @@ function registerSkillsCommands(program2) {
10771
11085
  fail(err, "skills", "marketplace-manage", start);
10772
11086
  }
10773
11087
  });
10774
- skills.command("purge-user").description("Remove all skills from the user-level skills folder for the target agent").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--force", "Skip confirmation \u2014 remove all skills without prompting").action((opts) => {
11088
+ skills.command("purge-user").description("Remove all skills from the user-level skills folder for the target agent").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--force", "Skip confirmation \u2014 remove all skills without prompting").action((opts) => {
10775
11089
  const start = Date.now();
10776
11090
  try {
10777
11091
  const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
@@ -10915,13 +11229,47 @@ function containerState(s) {
10915
11229
  if (s.running) return "Running";
10916
11230
  return "unknown";
10917
11231
  }
10918
- function getHttp2(program2) {
10919
- const opts = program2.optsWithGlobals();
10920
- const config = loadConfig({ configPath: opts.config });
11232
+ function resolveCluster(config, env, instance) {
11233
+ const effectiveEnv = env ?? config.openshift.defaultEnvironment;
11234
+ const effectiveInstance = instance ?? config.openshift.defaultInstance;
11235
+ if (effectiveEnv && effectiveInstance) {
11236
+ const envCfg = config.openshift.environments[effectiveEnv];
11237
+ const instCfg = envCfg?.instances?.[effectiveInstance];
11238
+ if (!instCfg?.baseUrl) {
11239
+ throw new PncliError(
11240
+ `OpenShift cluster "${effectiveEnv}/${effectiveInstance}" is not configured or missing baseUrl. Run: pncli config set openshift.environments.${effectiveEnv}.instances.${effectiveInstance}.baseUrl <url>`
11241
+ );
11242
+ }
11243
+ if (!instCfg.token) {
11244
+ throw new PncliError(
11245
+ `OpenShift cluster "${effectiveEnv}/${effectiveInstance}" is missing a token. Run: pncli config set openshift.environments.${effectiveEnv}.instances.${effectiveInstance}.token <token>`
11246
+ );
11247
+ }
11248
+ return { baseUrl: instCfg.baseUrl, token: instCfg.token };
11249
+ }
11250
+ if (effectiveEnv && !effectiveInstance) {
11251
+ throw new PncliError(
11252
+ `OpenShift environment "${effectiveEnv}" requires a cluster instance. Pass --instance <name> or run: pncli config set openshift.defaultInstance <name>`
11253
+ );
11254
+ }
11255
+ if (!effectiveEnv && effectiveInstance) {
11256
+ throw new PncliError(
11257
+ `OpenShift instance "${effectiveInstance}" requires an environment. Pass --env <name> or run: pncli config set openshift.defaultEnvironment <name>`
11258
+ );
11259
+ }
10921
11260
  if (!config.openshift.baseUrl) {
10922
11261
  throw new PncliError("OpenShift not configured. Run: pncli config init");
10923
11262
  }
10924
- return createHttpClient(config, Boolean(opts.dryRun));
11263
+ return { baseUrl: config.openshift.baseUrl, token: config.openshift.token };
11264
+ }
11265
+ function getHttp2(oc) {
11266
+ const opts = oc.optsWithGlobals();
11267
+ const config = loadConfig({ configPath: opts.config });
11268
+ const cluster = resolveCluster(config, opts.env, opts.instance);
11269
+ return createHttpClient(
11270
+ { ...config, openshift: { ...config.openshift, ...cluster } },
11271
+ Boolean(opts.dryRun)
11272
+ );
10925
11273
  }
10926
11274
  function podAge(creationTimestamp) {
10927
11275
  if (!creationTimestamp) return "unknown";
@@ -10934,12 +11282,63 @@ function podAge(creationTimestamp) {
10934
11282
  if (ageHours < 24) return `${ageHours}h`;
10935
11283
  return `${Math.floor(ageHours / 24)}d`;
10936
11284
  }
11285
+ function parseCpuMillicores(cpu) {
11286
+ if (!cpu) return null;
11287
+ if (cpu.endsWith("n")) return Math.round(Number(cpu.slice(0, -1)) / 1e6);
11288
+ if (cpu.endsWith("u")) return Math.round(Number(cpu.slice(0, -1)) / 1e3);
11289
+ if (cpu.endsWith("m")) return Number(cpu.slice(0, -1));
11290
+ const whole = Number(cpu);
11291
+ if (!Number.isNaN(whole)) return Math.round(whole * 1e3);
11292
+ return null;
11293
+ }
11294
+ function parseMemMiB(mem) {
11295
+ if (!mem) return null;
11296
+ const round2 = (n) => Math.round(n * 100) / 100;
11297
+ if (mem.endsWith("Ki")) return round2(Number(mem.slice(0, -2)) / 1024);
11298
+ if (mem.endsWith("Mi")) return Number(mem.slice(0, -2));
11299
+ if (mem.endsWith("Gi")) return Math.round(Number(mem.slice(0, -2)) * 1024);
11300
+ if (mem.endsWith("Ti")) return Math.round(Number(mem.slice(0, -2)) * 1024 * 1024);
11301
+ if (mem.endsWith("K")) return round2(Number(mem.slice(0, -1)) * 1e3 / (1024 * 1024));
11302
+ if (mem.endsWith("M")) return round2(Number(mem.slice(0, -1)) * 1e3 * 1e3 / (1024 * 1024));
11303
+ if (mem.endsWith("G")) return Math.round(Number(mem.slice(0, -1)) * 1e3 * 1e3 * 1e3 / (1024 * 1024));
11304
+ const bytes = Number(mem);
11305
+ if (!Number.isNaN(bytes)) return round2(bytes / (1024 * 1024));
11306
+ return null;
11307
+ }
10937
11308
  function registerOpenShiftCommands(program2) {
10938
- const oc = program2.command("openshift").description("OpenShift / Kubernetes operations");
11309
+ const oc = program2.command("openshift").description("OpenShift / Kubernetes operations").option("--env <name>", "Named environment (openshift.environments.<name>) to target").option("--instance <name>", "Named cluster instance within the environment to target");
11310
+ const cluster = oc.command("cluster").description("Manage named OpenShift cluster configurations");
11311
+ cluster.command("list").description("List all configured OpenShift environments and cluster instances").action(() => {
11312
+ const start = Date.now();
11313
+ try {
11314
+ const opts = oc.optsWithGlobals();
11315
+ const config = loadConfig({ configPath: opts.config });
11316
+ const clusters = [];
11317
+ for (const [envName, envCfg] of Object.entries(config.openshift.environments)) {
11318
+ for (const [instName, instCfg] of Object.entries(envCfg.instances ?? {})) {
11319
+ clusters.push({ environment: envName, instance: instName, baseUrl: instCfg.baseUrl });
11320
+ }
11321
+ }
11322
+ const legacy = config.openshift.baseUrl ? [{ environment: "(default)", instance: "(default)", baseUrl: config.openshift.baseUrl }] : [];
11323
+ success(
11324
+ {
11325
+ count: clusters.length + legacy.length,
11326
+ clusters: [...legacy, ...clusters],
11327
+ defaultEnvironment: config.openshift.defaultEnvironment,
11328
+ defaultInstance: config.openshift.defaultInstance
11329
+ },
11330
+ "openshift",
11331
+ "cluster-list",
11332
+ start
11333
+ );
11334
+ } catch (err) {
11335
+ fail(err, "openshift", "cluster-list", start);
11336
+ }
11337
+ });
10939
11338
  oc.command("pods").description("List pod health summary for a namespace \u2014 pre-processed for LLM analysis. Use --raw to return the full unmodified Kubernetes pod objects.").requiredOption("--namespace <ns>", "Kubernetes namespace").option("--label-selector <selector>", "Label selector (e.g. app=my-app)").option("--raw", "Return full Kubernetes pod objects without summarization (includes env vars, images, node name, annotations, resources, etc.)", false).action(async (opts) => {
10940
11339
  const start = Date.now();
10941
11340
  try {
10942
- const http = getHttp2(program2);
11341
+ const http = getHttp2(oc);
10943
11342
  const params = {};
10944
11343
  if (opts.labelSelector) params["labelSelector"] = opts.labelSelector;
10945
11344
  const podList = await http.openshift(
@@ -11048,7 +11447,7 @@ function registerOpenShiftCommands(program2) {
11048
11447
  oc.command("events").description("List Warning events for a namespace \u2014 pre-filtered for LLM analysis").requiredOption("--namespace <ns>", "Kubernetes namespace").option("--field-selector <selector>", "Additional field selector (e.g. involvedObject.name=my-pod)").option("--all", "Include Normal events in addition to Warning events", false).action(async (opts) => {
11049
11448
  const start = Date.now();
11050
11449
  try {
11051
- const http = getHttp2(program2);
11450
+ const http = getHttp2(oc);
11052
11451
  const params = {};
11053
11452
  const selectors = [];
11054
11453
  if (!opts.all) selectors.push("type=Warning");
@@ -11082,7 +11481,7 @@ function registerOpenShiftCommands(program2) {
11082
11481
  oc.command("logs").description("Get recent log lines from a pod container").requiredOption("--namespace <ns>", "Kubernetes namespace").requiredOption("--pod <name>", "Pod name").option("--container <name>", "Container name (defaults to first container)").option("--lines <n>", "Number of most-recent lines to return", "100").option("--previous", "Return logs from the previous container instance", false).action(async (opts) => {
11083
11482
  const start = Date.now();
11084
11483
  try {
11085
- const http = getHttp2(program2);
11484
+ const http = getHttp2(oc);
11086
11485
  const lines = opts.lines ? parseInt(opts.lines, 10) : 100;
11087
11486
  const logText = await http.openshiftText(
11088
11487
  `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods/${encodeURIComponent(opts.pod)}/log`,
@@ -11114,7 +11513,7 @@ function registerOpenShiftCommands(program2) {
11114
11513
  oc.command("pod-metrics").description("Get CPU and memory usage for pods in a namespace (requires metrics-server)").requiredOption("--namespace <ns>", "Kubernetes namespace").action(async (opts) => {
11115
11514
  const start = Date.now();
11116
11515
  try {
11117
- const http = getHttp2(program2);
11516
+ const http = getHttp2(oc);
11118
11517
  const metricsList = await http.openshift(
11119
11518
  `/apis/metrics.k8s.io/v1beta1/namespaces/${encodeURIComponent(opts.namespace)}/pods`
11120
11519
  );
@@ -11131,15 +11530,121 @@ function registerOpenShiftCommands(program2) {
11131
11530
  fail(err, "openshift", "pod-metrics", start);
11132
11531
  }
11133
11532
  });
11533
+ oc.command("resource-usage").description("Combined CPU/memory usage, limits, and requests for all pods in a namespace \u2014 all values normalized to millicores (m) and mebibytes (Mi). Requires metrics-server. Use --csv to emit a spreadsheet-friendly CSV.").requiredOption("--namespace <ns>", "Kubernetes namespace").option("--label-selector <selector>", "Label selector (e.g. app=my-app)").option("--csv", "Output as CSV instead of JSON (suitable for Excel)", false).action(async (opts) => {
11534
+ const start = Date.now();
11535
+ try {
11536
+ const http = getHttp2(oc);
11537
+ const nsEncoded = encodeURIComponent(opts.namespace);
11538
+ const podParams = {};
11539
+ if (opts.labelSelector) podParams["labelSelector"] = opts.labelSelector;
11540
+ const [podList, metricsList] = await Promise.all([
11541
+ http.openshift(`/api/v1/namespaces/${nsEncoded}/pods`, { params: podParams }),
11542
+ http.openshift(`/apis/metrics.k8s.io/v1beta1/namespaces/${nsEncoded}/pods`, { params: podParams })
11543
+ ]);
11544
+ const metricsIndex = /* @__PURE__ */ new Map();
11545
+ for (const pm of metricsList.items) {
11546
+ const containerMap = /* @__PURE__ */ new Map();
11547
+ for (const c of pm.containers) {
11548
+ containerMap.set(c.name, c.usage);
11549
+ }
11550
+ metricsIndex.set(pm.metadata.name, containerMap);
11551
+ }
11552
+ const rows = [];
11553
+ for (const pod of podList.items) {
11554
+ const podName = pod.metadata.name;
11555
+ const podMetrics = metricsIndex.get(podName);
11556
+ const containers = pod.spec.containers ?? [];
11557
+ for (const container of containers) {
11558
+ const usage = podMetrics?.get(container.name);
11559
+ rows.push({
11560
+ pod: podName,
11561
+ container: container.name,
11562
+ cpuUsageMillicores: parseCpuMillicores(usage?.cpu),
11563
+ memUsageMiB: parseMemMiB(usage?.memory),
11564
+ cpuLimitsMillicores: parseCpuMillicores(container.resources?.limits?.cpu),
11565
+ memLimitsMiB: parseMemMiB(container.resources?.limits?.memory),
11566
+ cpuRequestsMillicores: parseCpuMillicores(container.resources?.requests?.cpu),
11567
+ memRequestsMiB: parseMemMiB(container.resources?.requests?.memory)
11568
+ });
11569
+ }
11570
+ }
11571
+ if (opts.csv) {
11572
+ const header = "Pod,Container,CPU Usage (m),Memory Usage (Mi),CPU Limits (m),Memory Limits (Mi),CPU Requests (m),Memory Requests (Mi)";
11573
+ const lines = rows.map(
11574
+ (r) => [
11575
+ r.pod,
11576
+ r.container,
11577
+ r.cpuUsageMillicores ?? "",
11578
+ r.memUsageMiB ?? "",
11579
+ r.cpuLimitsMillicores ?? "",
11580
+ r.memLimitsMiB ?? "",
11581
+ r.cpuRequestsMillicores ?? "",
11582
+ r.memRequestsMiB ?? ""
11583
+ ].join(",")
11584
+ );
11585
+ writeRawOutput([header, ...lines].join("\n") + "\n");
11586
+ } else {
11587
+ success(
11588
+ { namespace: opts.namespace, count: rows.length, units: { cpu: "m", memory: "Mi" }, rows },
11589
+ "openshift",
11590
+ "resource-usage",
11591
+ start
11592
+ );
11593
+ }
11594
+ } catch (err) {
11595
+ fail(err, "openshift", "resource-usage", start);
11596
+ }
11597
+ });
11134
11598
  }
11135
11599
 
11136
11600
  // src/services/dynatrace/commands.ts
11137
- function getHttp3(program2) {
11601
+ function getHttp3(program2, envName) {
11138
11602
  const opts = program2.optsWithGlobals();
11139
- return createHttpClient(
11140
- loadConfig({ configPath: opts.config }),
11141
- Boolean(opts.dryRun)
11142
- );
11603
+ const cfg = loadConfig({ configPath: opts.config });
11604
+ if (envName) {
11605
+ const envConfig = cfg.dynatrace.environments[envName];
11606
+ if (!envConfig) {
11607
+ throw new PncliError(
11608
+ `Dynatrace environment "${envName}" not configured. Run: pncli config set dynatrace.environments.${envName}.baseUrl <url>`
11609
+ );
11610
+ }
11611
+ return createHttpClient(
11612
+ {
11613
+ ...cfg,
11614
+ dynatrace: {
11615
+ ...cfg.dynatrace,
11616
+ baseUrl: envConfig.baseUrl ?? cfg.dynatrace.baseUrl,
11617
+ apiToken: envConfig.apiToken ?? cfg.dynatrace.apiToken,
11618
+ platformUrl: envConfig.platformUrl ?? cfg.dynatrace.platformUrl,
11619
+ platformToken: envConfig.platformToken ?? cfg.dynatrace.platformToken
11620
+ }
11621
+ },
11622
+ Boolean(opts.dryRun)
11623
+ );
11624
+ }
11625
+ const defaultEnv = cfg.dynatrace.defaultEnvironment;
11626
+ if (defaultEnv) {
11627
+ const envConfig = cfg.dynatrace.environments[defaultEnv];
11628
+ if (!envConfig) {
11629
+ throw new PncliError(
11630
+ `Dynatrace defaultEnvironment "${defaultEnv}" is not configured. Run: pncli config set dynatrace.environments.${defaultEnv}.baseUrl <url> or pncli config set dynatrace.defaultEnvironment <name>`
11631
+ );
11632
+ }
11633
+ return createHttpClient(
11634
+ {
11635
+ ...cfg,
11636
+ dynatrace: {
11637
+ ...cfg.dynatrace,
11638
+ baseUrl: envConfig.baseUrl ?? cfg.dynatrace.baseUrl,
11639
+ apiToken: envConfig.apiToken ?? cfg.dynatrace.apiToken,
11640
+ platformUrl: envConfig.platformUrl ?? cfg.dynatrace.platformUrl,
11641
+ platformToken: envConfig.platformToken ?? cfg.dynatrace.platformToken
11642
+ }
11643
+ },
11644
+ Boolean(opts.dryRun)
11645
+ );
11646
+ }
11647
+ return createHttpClient(cfg, Boolean(opts.dryRun));
11143
11648
  }
11144
11649
  async function allPages(fetchPage) {
11145
11650
  const items = [];
@@ -11211,12 +11716,13 @@ async function runTraceQuery(http, traceId) {
11211
11716
  return response.result;
11212
11717
  }
11213
11718
  function registerDynatraceCommands(program2) {
11214
- const dynatrace = program2.command("dynatrace").description("Dynatrace observability operations");
11719
+ const dynatrace = program2.command("dynatrace").description("Dynatrace observability operations").option("--env <name>", "Named Dynatrace environment to use (from dynatrace.environments.<name>)");
11215
11720
  const entities = dynatrace.command("entities").description("Monitored entity operations");
11216
11721
  entities.command("list").description("List monitored entities using an entity selector").requiredOption("--selector <selector>", 'Dynatrace entity selector, e.g. type("SERVICE")').option("--from <time>", "Start time (ISO 8601 or relative, e.g. now-2h)").option("--to <time>", "End time (ISO 8601 or relative)").option("--fields <fields>", "Additional entity fields").action(async (opts) => {
11217
11722
  const start = Date.now();
11218
11723
  try {
11219
- const data = await listEntities(getHttp3(program2), opts.selector, opts);
11724
+ const envName = dynatrace.opts().env;
11725
+ const data = await listEntities(getHttp3(program2, envName), opts.selector, opts);
11220
11726
  success({ totalCount: data.totalCount, entities: data.items }, "dynatrace", "entities list", start);
11221
11727
  } catch (err) {
11222
11728
  fail(err, "dynatrace", "entities list", start);
@@ -11225,7 +11731,8 @@ function registerDynatraceCommands(program2) {
11225
11731
  entities.command("get").description("Get a monitored entity by ID").requiredOption("--id <id>", "Entity ID, e.g. SERVICE-1234567890ABCDEF").option("--from <time>", "Start time").option("--to <time>", "End time").option("--fields <fields>", "Additional entity fields").action(async (opts) => {
11226
11732
  const start = Date.now();
11227
11733
  try {
11228
- const data = await getHttp3(program2).dynatrace(
11734
+ const envName = dynatrace.opts().env;
11735
+ const data = await getHttp3(program2, envName).dynatrace(
11229
11736
  `/api/v2/entities/${encodeURIComponent(opts.id)}`,
11230
11737
  { params: { from: opts.from, to: opts.to, fields: opts.fields } }
11231
11738
  );
@@ -11237,7 +11744,8 @@ function registerDynatraceCommands(program2) {
11237
11744
  dynatrace.command("services").description("List monitored services").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
11238
11745
  const start = Date.now();
11239
11746
  try {
11240
- const data = await listEntities(getHttp3(program2), 'type("SERVICE")', opts);
11747
+ const envName = dynatrace.opts().env;
11748
+ const data = await listEntities(getHttp3(program2, envName), 'type("SERVICE")', opts);
11241
11749
  success({ totalCount: data.totalCount, services: data.items }, "dynatrace", "services", start);
11242
11750
  } catch (err) {
11243
11751
  fail(err, "dynatrace", "services", start);
@@ -11246,8 +11754,9 @@ function registerDynatraceCommands(program2) {
11246
11754
  dynatrace.command("workloads").description("List monitored Kubernetes workloads").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
11247
11755
  const start = Date.now();
11248
11756
  try {
11757
+ const envName = dynatrace.opts().env;
11249
11758
  const selector = 'type("CLOUD_APPLICATION")';
11250
- const data = await listEntities(getHttp3(program2), selector, opts);
11759
+ const data = await listEntities(getHttp3(program2, envName), selector, opts);
11251
11760
  success({ totalCount: data.totalCount, workloads: data.items }, "dynatrace", "workloads", start);
11252
11761
  } catch (err) {
11253
11762
  fail(err, "dynatrace", "workloads", start);
@@ -11257,8 +11766,9 @@ function registerDynatraceCommands(program2) {
11257
11766
  problems.command("list").description("List problems").option("--from <time>", "Start time (default: now-2h)").option("--to <time>", "End time (default: now)").option("--problem-selector <selector>", "Dynatrace problem selector").option("--entity-selector <selector>", "Affected entity selector").action(async (opts) => {
11258
11767
  const start = Date.now();
11259
11768
  try {
11769
+ const envName = dynatrace.opts().env;
11260
11770
  const data = await allPages(async (nextPageKey) => {
11261
- const page = await getHttp3(program2).dynatrace("/api/v2/problems", {
11771
+ const page = await getHttp3(program2, envName).dynatrace("/api/v2/problems", {
11262
11772
  params: nextPageKey ? { nextPageKey } : {
11263
11773
  from: opts.from ?? "now-2h",
11264
11774
  to: opts.to ?? "now",
@@ -11277,7 +11787,8 @@ function registerDynatraceCommands(program2) {
11277
11787
  problems.command("get").description("Get a problem by ID").requiredOption("--id <id>", "Problem ID").option("--fields <fields>", "Additional fields").action(async (opts) => {
11278
11788
  const start = Date.now();
11279
11789
  try {
11280
- const data = await getHttp3(program2).dynatrace(
11790
+ const envName = dynatrace.opts().env;
11791
+ const data = await getHttp3(program2, envName).dynatrace(
11281
11792
  `/api/v2/problems/${encodeURIComponent(opts.id)}`,
11282
11793
  { params: { fields: opts.fields } }
11283
11794
  );
@@ -11289,7 +11800,8 @@ function registerDynatraceCommands(program2) {
11289
11800
  dynatrace.command("trace").description("Get distributed-trace spans from Grail").requiredOption("--id <trace-id>", "16- or 32-character hexadecimal trace ID").action(async (opts) => {
11290
11801
  const start = Date.now();
11291
11802
  try {
11292
- const result = await runTraceQuery(getHttp3(program2), opts.id);
11803
+ const envName = dynatrace.opts().env;
11804
+ const result = await runTraceQuery(getHttp3(program2, envName), opts.id);
11293
11805
  success(result, "dynatrace", "trace", start);
11294
11806
  } catch (err) {
11295
11807
  fail(err, "dynatrace", "trace", start);
@@ -11356,6 +11868,500 @@ function registerLogscaleCommands(program2) {
11356
11868
  });
11357
11869
  }
11358
11870
 
11871
+ // src/services/splitio/commands.ts
11872
+ import fs5 from "fs";
11873
+ import confirm2 from "@inquirer/confirm";
11874
+ function getHttp5(program2) {
11875
+ const opts = program2.optsWithGlobals();
11876
+ return createHttpClient(
11877
+ loadConfig({ configPath: opts.config }),
11878
+ Boolean(opts.dryRun)
11879
+ );
11880
+ }
11881
+ function loadInputFile(filePath) {
11882
+ let raw;
11883
+ if (filePath === "-") {
11884
+ raw = fs5.readFileSync(process.stdin.fd, "utf8");
11885
+ } else {
11886
+ raw = fs5.readFileSync(filePath, "utf8");
11887
+ }
11888
+ try {
11889
+ return JSON.parse(raw);
11890
+ } catch {
11891
+ throw new PncliError(`Could not parse JSON from ${filePath === "-" ? "stdin" : filePath}`);
11892
+ }
11893
+ }
11894
+ async function confirmWrite(opts, opLabel) {
11895
+ if (opts.yes) return;
11896
+ const proceed = await confirm2({
11897
+ message: `Submit Change Request "${opts.mnemonic}" (${opLabel})? This creates an approval request in Split.`,
11898
+ default: false
11899
+ });
11900
+ if (!proceed) throw new PncliError("Aborted by user.", 0);
11901
+ }
11902
+ function buildCrBase(opts) {
11903
+ const base = {
11904
+ title: opts.mnemonic,
11905
+ comment: opts.description,
11906
+ crNumber: opts.changeNumber
11907
+ };
11908
+ if (opts.approvers) {
11909
+ base["approvers"] = opts.approvers.split(",").map((s) => s.trim()).filter(Boolean).map((id) => ({ id }));
11910
+ }
11911
+ return base;
11912
+ }
11913
+ function registerSplitioCommands(program2) {
11914
+ const splitio = program2.command("splitio").description("Split.IO feature flag administration");
11915
+ const workspaces = splitio.command("workspaces").description("Workspace operations");
11916
+ workspaces.command("list").description("List all Split.IO workspaces").action(async () => {
11917
+ const start = Date.now();
11918
+ try {
11919
+ const http = getHttp5(program2);
11920
+ const data = await http.splitio("/internal/api/v2/workspaces");
11921
+ success(
11922
+ { count: data.objects.length, workspaces: data.objects },
11923
+ "splitio",
11924
+ "workspaces list",
11925
+ start
11926
+ );
11927
+ } catch (err) {
11928
+ fail(err, "splitio", "workspaces list", start);
11929
+ }
11930
+ });
11931
+ const environments = splitio.command("environments").description("Environment operations");
11932
+ environments.command("list").description("List environments in a workspace").requiredOption("--workspace <id>", "Workspace ID").action(async (opts) => {
11933
+ const start = Date.now();
11934
+ try {
11935
+ const http = getHttp5(program2);
11936
+ const data = await http.splitio(
11937
+ `/internal/api/v2/workspaces/${encodeURIComponent(opts.workspace)}/environments`
11938
+ );
11939
+ success(
11940
+ { count: data.objects.length, environments: data.objects },
11941
+ "splitio",
11942
+ "environments list",
11943
+ start
11944
+ );
11945
+ } catch (err) {
11946
+ fail(err, "splitio", "environments list", start);
11947
+ }
11948
+ });
11949
+ const flags = splitio.command("flags").description("Feature flag operations");
11950
+ flags.command("list").description("List feature flags in a workspace").requiredOption("--workspace <id>", "Workspace ID").option("--limit <n>", "Maximum number of flags to return", "50").option("--offset <n>", "Pagination offset", "0").option("--flag-set <name>", "Filter by flag set name").action(async (opts) => {
11951
+ const start = Date.now();
11952
+ try {
11953
+ const http = getHttp5(program2);
11954
+ const params = {
11955
+ wsId: opts.workspace,
11956
+ limit: parseInt(opts.limit, 10),
11957
+ offset: parseInt(opts.offset, 10)
11958
+ };
11959
+ if (opts.flagSet) params["flagSetName"] = opts.flagSet;
11960
+ const data = await http.splitio("/internal/api/v2/splits", { params });
11961
+ success(
11962
+ { count: data.objects.length, totalCount: data.totalCount, flags: data.objects },
11963
+ "splitio",
11964
+ "flags list",
11965
+ start
11966
+ );
11967
+ } catch (err) {
11968
+ fail(err, "splitio", "flags list", start);
11969
+ }
11970
+ });
11971
+ flags.command("get").description("Get a flag definition").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").option("--environment <id>", "Environment ID \u2014 include targeting rules for this environment").action(async (opts) => {
11972
+ const start = Date.now();
11973
+ try {
11974
+ const http = getHttp5(program2);
11975
+ if (opts.environment) {
11976
+ const data = await http.splitio(
11977
+ `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
11978
+ { params: { wsId: opts.workspace } }
11979
+ );
11980
+ success(data, "splitio", "flags get", start);
11981
+ } else {
11982
+ const data = await http.splitio(
11983
+ `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}`,
11984
+ { params: { wsId: opts.workspace } }
11985
+ );
11986
+ success(data, "splitio", "flags get", start);
11987
+ }
11988
+ } catch (err) {
11989
+ fail(err, "splitio", "flags get", start);
11990
+ }
11991
+ });
11992
+ flags.command("update").description("Submit a Change Request to replace a flag's full definition in an environment").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--input-file <path>", "JSON file with the complete flag definition body (use - for stdin)").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
11993
+ const start = Date.now();
11994
+ try {
11995
+ if (!opts.inputFile) throw new PncliError("--input-file is required for flags update");
11996
+ const definition = loadInputFile(opts.inputFile);
11997
+ await confirmWrite(opts, `UPDATE ${opts.flag} in ${opts.environment}`);
11998
+ const http = getHttp5(program2);
11999
+ const body = {
12000
+ ...buildCrBase(opts),
12001
+ operationType: "UPDATE_SPLIT_DEFINITION",
12002
+ split: {
12003
+ name: opts.flag,
12004
+ environment: { id: opts.environment },
12005
+ definition
12006
+ },
12007
+ workspace: { id: opts.workspace }
12008
+ };
12009
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12010
+ method: "POST",
12011
+ body
12012
+ });
12013
+ success(data, "splitio", "flags update", start);
12014
+ } catch (err) {
12015
+ fail(err, "splitio", "flags update", start);
12016
+ }
12017
+ });
12018
+ flags.command("set-rule").description("Change one targeting rule without altering the rest of the flag definition").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--rule-index <n>", "Zero-based index of the targeting rule to update").option("--treatment <name>", "Treatment to assign to the entire rule bucket").option("--weights <json>", "JSON array of {treatment, size} objects for weighted distribution").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12019
+ const start = Date.now();
12020
+ try {
12021
+ if (!opts.treatment && !opts.weights) {
12022
+ throw new PncliError("Either --treatment or --weights must be specified");
12023
+ }
12024
+ const http = getHttp5(program2);
12025
+ const current = await http.splitio(
12026
+ `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
12027
+ { params: { wsId: opts.workspace } }
12028
+ );
12029
+ const rules = current.rules ?? [];
12030
+ const idx = parseInt(opts.ruleIndex, 10);
12031
+ if (Number.isNaN(idx) || idx < 0 || idx >= rules.length) {
12032
+ throw new PncliError(`Rule index ${Number.isNaN(idx) ? JSON.stringify(opts.ruleIndex) : idx} is out of range (flag has ${rules.length} rules)`);
12033
+ }
12034
+ if (opts.treatment) {
12035
+ rules[idx] = { ...rules[idx], buckets: [{ treatment: opts.treatment, size: 1e5 }] };
12036
+ } else {
12037
+ const parsed = JSON.parse(opts.weights);
12038
+ rules[idx] = { ...rules[idx], buckets: parsed };
12039
+ }
12040
+ await confirmWrite(opts, `SET_RULE ${idx} on ${opts.flag} in ${opts.environment}`);
12041
+ const body = {
12042
+ ...buildCrBase(opts),
12043
+ operationType: "UPDATE_SPLIT_DEFINITION",
12044
+ split: {
12045
+ name: opts.flag,
12046
+ environment: { id: opts.environment },
12047
+ definition: { ...current, rules }
12048
+ },
12049
+ workspace: { id: opts.workspace }
12050
+ };
12051
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12052
+ method: "POST",
12053
+ body
12054
+ });
12055
+ success(data, "splitio", "flags set-rule", start);
12056
+ } catch (err) {
12057
+ fail(err, "splitio", "flags set-rule", start);
12058
+ }
12059
+ });
12060
+ flags.command("set-default").description("Update the fallback default rule for traffic matching no targeting rule").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--treatment <name>", "Treatment to assign as the default").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12061
+ const start = Date.now();
12062
+ try {
12063
+ const http = getHttp5(program2);
12064
+ const current = await http.splitio(
12065
+ `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
12066
+ { params: { wsId: opts.workspace } }
12067
+ );
12068
+ await confirmWrite(opts, `SET_DEFAULT ${opts.treatment} on ${opts.flag} in ${opts.environment}`);
12069
+ const body = {
12070
+ ...buildCrBase(opts),
12071
+ operationType: "UPDATE_SPLIT_DEFINITION",
12072
+ split: {
12073
+ name: opts.flag,
12074
+ environment: { id: opts.environment },
12075
+ definition: {
12076
+ ...current,
12077
+ defaultRule: [{ treatment: opts.treatment, size: 1e5 }]
12078
+ }
12079
+ },
12080
+ workspace: { id: opts.workspace }
12081
+ };
12082
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12083
+ method: "POST",
12084
+ body
12085
+ });
12086
+ success(data, "splitio", "flags set-default", start);
12087
+ } catch (err) {
12088
+ fail(err, "splitio", "flags set-default", start);
12089
+ }
12090
+ });
12091
+ flags.command("toggle").description("Enable or disable a single flag via a Change Request").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--enabled <bool>", "true to restore / false to kill the flag").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12092
+ const start = Date.now();
12093
+ try {
12094
+ const enable = opts.enabled.toLowerCase() !== "false" && opts.enabled !== "0";
12095
+ const opType = enable ? "RESTORE_SPLIT" : "KILL_SPLIT";
12096
+ await confirmWrite(opts, `${opType} ${opts.flag} in ${opts.environment}`);
12097
+ const http = getHttp5(program2);
12098
+ const body = {
12099
+ ...buildCrBase(opts),
12100
+ operationType: opType,
12101
+ split: { name: opts.flag, environment: { id: opts.environment } },
12102
+ workspace: { id: opts.workspace }
12103
+ };
12104
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12105
+ method: "POST",
12106
+ body
12107
+ });
12108
+ success(data, "splitio", "flags toggle", start);
12109
+ } catch (err) {
12110
+ fail(err, "splitio", "flags toggle", start);
12111
+ }
12112
+ });
12113
+ flags.command("batch-toggle").description("Enable or disable multiple flags from a JSON file (each creates its own Change Request)").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--environment <id>", "Environment ID").requiredOption("--mnemonic <text>", "Short Change Request title prefix (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve each CR").option("--input-file <path>", "JSON file: array of {flag, enabled} objects (use - for stdin)").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12114
+ const start = Date.now();
12115
+ try {
12116
+ if (!opts.inputFile) throw new PncliError("--input-file is required for batch-toggle");
12117
+ const entries = loadInputFile(opts.inputFile);
12118
+ if (!Array.isArray(entries)) throw new PncliError("Input file must be a JSON array of {flag, enabled} objects");
12119
+ await confirmWrite(opts, `BATCH_TOGGLE ${entries.length} flags in ${opts.environment}`);
12120
+ const http = getHttp5(program2);
12121
+ const results = [];
12122
+ for (const entry of entries) {
12123
+ try {
12124
+ const opType = entry.enabled ? "RESTORE_SPLIT" : "KILL_SPLIT";
12125
+ const body = {
12126
+ ...buildCrBase(opts),
12127
+ title: `${opts.mnemonic} \u2014 ${entry.flag}`,
12128
+ operationType: opType,
12129
+ split: { name: entry.flag, environment: { id: opts.environment } },
12130
+ workspace: { id: opts.workspace }
12131
+ };
12132
+ const cr = await http.splitio("/internal/api/v2/changeRequests", {
12133
+ method: "POST",
12134
+ body
12135
+ });
12136
+ results.push({ flag: entry.flag, ok: true, id: cr.id });
12137
+ } catch (err) {
12138
+ results.push({
12139
+ flag: entry.flag,
12140
+ ok: false,
12141
+ error: err instanceof Error ? err.message : String(err)
12142
+ });
12143
+ }
12144
+ }
12145
+ success({ submitted: results.length, results }, "splitio", "flags batch-toggle", start);
12146
+ } catch (err) {
12147
+ fail(err, "splitio", "flags batch-toggle", start);
12148
+ }
12149
+ });
12150
+ flags.command("kill").description("Submit a Change Request to kill (disable) a flag").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12151
+ const start = Date.now();
12152
+ try {
12153
+ await confirmWrite(opts, `KILL ${opts.flag} in ${opts.environment}`);
12154
+ const http = getHttp5(program2);
12155
+ const body = {
12156
+ ...buildCrBase(opts),
12157
+ operationType: "KILL_SPLIT",
12158
+ split: { name: opts.flag, environment: { id: opts.environment } },
12159
+ workspace: { id: opts.workspace }
12160
+ };
12161
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12162
+ method: "POST",
12163
+ body
12164
+ });
12165
+ success(data, "splitio", "flags kill", start);
12166
+ } catch (err) {
12167
+ fail(err, "splitio", "flags kill", start);
12168
+ }
12169
+ });
12170
+ flags.command("restore").description("Submit a Change Request to restore a killed flag").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--environment <id>", "Environment ID").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--approvers <ids>", "Comma-separated Split user IDs required to approve this CR").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12171
+ const start = Date.now();
12172
+ try {
12173
+ await confirmWrite(opts, `RESTORE ${opts.flag} in ${opts.environment}`);
12174
+ const http = getHttp5(program2);
12175
+ const body = {
12176
+ ...buildCrBase(opts),
12177
+ operationType: "RESTORE_SPLIT",
12178
+ split: { name: opts.flag, environment: { id: opts.environment } },
12179
+ workspace: { id: opts.workspace }
12180
+ };
12181
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12182
+ method: "POST",
12183
+ body
12184
+ });
12185
+ success(data, "splitio", "flags restore", start);
12186
+ } catch (err) {
12187
+ fail(err, "splitio", "flags restore", start);
12188
+ }
12189
+ });
12190
+ flags.command("archive").description("Submit a Change Request to archive (delete definition of) a flag").requiredOption("--workspace <id>", "Workspace ID").requiredOption("--flag <name>", "Feature flag name").requiredOption("--mnemonic <text>", "Short Change Request title (mnemonic)").requiredOption("--description <text>", "Change Request description").requiredOption("--change-number <CHG>", "ServiceNow change request number (e.g. CHG0001234)").option("--yes", "Skip interactive confirmation prompt").action(async (opts) => {
12191
+ const start = Date.now();
12192
+ try {
12193
+ await confirmWrite(opts, `ARCHIVE ${opts.flag}`);
12194
+ const http = getHttp5(program2);
12195
+ const body = {
12196
+ ...buildCrBase(opts),
12197
+ operationType: "ARCHIVE_SPLIT",
12198
+ split: { name: opts.flag },
12199
+ workspace: { id: opts.workspace }
12200
+ };
12201
+ const data = await http.splitio("/internal/api/v2/changeRequests", {
12202
+ method: "POST",
12203
+ body
12204
+ });
12205
+ success(data, "splitio", "flags archive", start);
12206
+ } catch (err) {
12207
+ fail(err, "splitio", "flags archive", start);
12208
+ }
12209
+ });
12210
+ const changeRequests = splitio.command("change-requests").description("Change Request tracking");
12211
+ changeRequests.command("list").description("List Change Requests for a workspace").requiredOption("--workspace <id>", "Workspace ID").option("--environment <id>", "Filter by environment ID").option("--status <status>", "Filter by status (REQUESTED, APPROVED, REJECTED, PUBLISHED)").option("--limit <n>", "Maximum number of results", "25").option("--offset <n>", "Pagination offset", "0").action(async (opts) => {
12212
+ const start = Date.now();
12213
+ try {
12214
+ const http = getHttp5(program2);
12215
+ const params = {
12216
+ wsId: opts.workspace,
12217
+ limit: parseInt(opts.limit, 10),
12218
+ offset: parseInt(opts.offset, 10)
12219
+ };
12220
+ if (opts.environment) params["environmentId"] = opts.environment;
12221
+ if (opts.status) params["status"] = opts.status;
12222
+ const data = await http.splitio(
12223
+ "/internal/api/v2/changeRequests",
12224
+ { params }
12225
+ );
12226
+ success(
12227
+ { count: data.objects.length, totalCount: data.totalCount, changeRequests: data.objects },
12228
+ "splitio",
12229
+ "change-requests list",
12230
+ start
12231
+ );
12232
+ } catch (err) {
12233
+ fail(err, "splitio", "change-requests list", start);
12234
+ }
12235
+ });
12236
+ changeRequests.command("get").description("Get a Change Request by ID").requiredOption("--id <id>", "Change Request ID").action(async (opts) => {
12237
+ const start = Date.now();
12238
+ try {
12239
+ const http = getHttp5(program2);
12240
+ const data = await http.splitio(
12241
+ `/internal/api/v2/changeRequests/${encodeURIComponent(opts.id)}`
12242
+ );
12243
+ success(data, "splitio", "change-requests get", start);
12244
+ } catch (err) {
12245
+ fail(err, "splitio", "change-requests get", start);
12246
+ }
12247
+ });
12248
+ }
12249
+
12250
+ // src/services/figma/commands.ts
12251
+ function parseFileKey(input3) {
12252
+ if (input3.startsWith("http://") || input3.startsWith("https://")) {
12253
+ const match = /figma\.com\/(?:design|file)\/([A-Za-z0-9]+)/i.exec(input3);
12254
+ if (!match?.[1]) {
12255
+ throw new Error(`Could not extract Figma file key from URL: ${input3}
12256
+ Expected format: https://www.figma.com/design/<file-key>/...`);
12257
+ }
12258
+ return match[1];
12259
+ }
12260
+ return input3;
12261
+ }
12262
+ function getHttp6(program2) {
12263
+ const opts = program2.optsWithGlobals();
12264
+ return createHttpClient(
12265
+ loadConfig({ configPath: opts.config }),
12266
+ Boolean(opts.dryRun)
12267
+ );
12268
+ }
12269
+ function registerFigmaCommands(program2) {
12270
+ const figma = program2.command("figma").description("Figma design operations (files, comments, versions)");
12271
+ figma.command("file").description("Get a Figma file \u2014 metadata, structure, and component/style inventory").argument("<file-key-or-url>", "Figma file key or full Figma URL (https://www.figma.com/design/<key>/...)").option("--document", "Include the full document node tree (can be large; omitted by default)").action(async (fileKeyOrUrl, opts) => {
12272
+ const start = Date.now();
12273
+ const { success: success2, fail: fail2 } = await import("./output-XT3WT5L5.js");
12274
+ try {
12275
+ const fileKey = parseFileKey(fileKeyOrUrl);
12276
+ const params = {};
12277
+ if (!opts.document) {
12278
+ params["geometry"] = "paths";
12279
+ params["depth"] = 0;
12280
+ }
12281
+ const data = await getHttp6(program2).figma(`/v1/files/${encodeURIComponent(fileKey)}`, { params });
12282
+ success2(
12283
+ {
12284
+ fileKey,
12285
+ name: data.name,
12286
+ lastModified: data.lastModified,
12287
+ version: data.version,
12288
+ thumbnailUrl: data.thumbnailUrl,
12289
+ role: data.role,
12290
+ editorType: data.editorType,
12291
+ schemaVersion: data.schemaVersion,
12292
+ ...opts.document ? { document: data.document } : {},
12293
+ componentCount: data.components ? Object.keys(data.components).length : 0,
12294
+ styleCount: data.styles ? Object.keys(data.styles).length : 0
12295
+ },
12296
+ "figma",
12297
+ "file",
12298
+ start
12299
+ );
12300
+ } catch (err) {
12301
+ fail2(err, "figma", "file", start);
12302
+ }
12303
+ });
12304
+ figma.command("comments").description("Get comments on a Figma file").argument("<file-key-or-url>", "Figma file key or full Figma URL").option("--as-of <date>", "ISO 8601 timestamp \u2014 return only comments created before this time").action(async (fileKeyOrUrl, opts) => {
12305
+ const start = Date.now();
12306
+ const { success: success2, fail: fail2 } = await import("./output-XT3WT5L5.js");
12307
+ try {
12308
+ const fileKey = parseFileKey(fileKeyOrUrl);
12309
+ const params = {};
12310
+ if (opts.asOf) params["as_of"] = opts.asOf;
12311
+ const data = await getHttp6(program2).figma(`/v1/files/${encodeURIComponent(fileKey)}/comments`, { params });
12312
+ success2(
12313
+ { fileKey, count: data.comments.length, comments: data.comments },
12314
+ "figma",
12315
+ "comments",
12316
+ start
12317
+ );
12318
+ } catch (err) {
12319
+ fail2(err, "figma", "comments", start);
12320
+ }
12321
+ });
12322
+ figma.command("versions").description("Get the version history of a Figma file").argument("<file-key-or-url>", "Figma file key or full Figma URL").action(async (fileKeyOrUrl) => {
12323
+ const start = Date.now();
12324
+ const { success: success2, fail: fail2 } = await import("./output-XT3WT5L5.js");
12325
+ try {
12326
+ const fileKey = parseFileKey(fileKeyOrUrl);
12327
+ const data = await getHttp6(program2).figma(`/v1/files/${encodeURIComponent(fileKey)}/versions`);
12328
+ success2(
12329
+ { fileKey, count: data.versions.length, versions: data.versions },
12330
+ "figma",
12331
+ "versions",
12332
+ start
12333
+ );
12334
+ } catch (err) {
12335
+ fail2(err, "figma", "versions", start);
12336
+ }
12337
+ });
12338
+ figma.command("project-files").description("List files in a Figma project").argument("<project-id>", "Figma project ID (visible in the project URL)").action(async (projectId) => {
12339
+ const start = Date.now();
12340
+ const { success: success2, fail: fail2 } = await import("./output-XT3WT5L5.js");
12341
+ try {
12342
+ const data = await getHttp6(program2).figma(`/v1/projects/${encodeURIComponent(projectId)}/files`);
12343
+ success2(
12344
+ { projectId, count: data.files.length, files: data.files },
12345
+ "figma",
12346
+ "project-files",
12347
+ start
12348
+ );
12349
+ } catch (err) {
12350
+ fail2(err, "figma", "project-files", start);
12351
+ }
12352
+ });
12353
+ figma.command("me").description("Get the current Figma user (useful for verifying credentials)").action(async () => {
12354
+ const start = Date.now();
12355
+ const { success: success2, fail: fail2 } = await import("./output-XT3WT5L5.js");
12356
+ try {
12357
+ const data = await getHttp6(program2).figma("/v1/me");
12358
+ success2(data, "figma", "me", start);
12359
+ } catch (err) {
12360
+ fail2(err, "figma", "me", start);
12361
+ }
12362
+ });
12363
+ }
12364
+
11359
12365
  // src/cli.ts
11360
12366
  if (!process.env.PNCLI_VERIFY_TLS) {
11361
12367
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -11392,7 +12398,6 @@ registerConfigCommands(program);
11392
12398
  registerAdoCommands(program);
11393
12399
  registerJenkinsCommands(program);
11394
12400
  registerArtifactoryCommands(program);
11395
- registerUdeployCommands(program);
11396
12401
  registerCheckmarxCommands(program);
11397
12402
  registerServiceNowCommands(program);
11398
12403
  registerContrastCommands(program);
@@ -11402,6 +12407,8 @@ registerJwtCommands(program);
11402
12407
  registerOpenShiftCommands(program);
11403
12408
  registerDynatraceCommands(program);
11404
12409
  registerLogscaleCommands(program);
12410
+ registerSplitioCommands(program);
12411
+ registerFigmaCommands(program);
11405
12412
  program.addHelpText("after", `
11406
12413
  Services:
11407
12414
  git Local git operations (status, diff, log, branch)
@@ -11413,9 +12420,8 @@ Services:
11413
12420
  sonar SonarQube Server (quality gates, issues, metrics, hotspots)
11414
12421
  sde SDElements (threat modeling, countermeasures, compliance)
11415
12422
  ado Azure DevOps Server (work items, repos, PRs, pipelines)
11416
- jenkins Jenkins Data Center (jobs, builds, logs)
12423
+ jenkins Jenkins (jobs, builds, logs, named instances)
11417
12424
  artifactory Artifactory (repos, artifact search, build info, properties)
11418
- udeploy IBM UrbanCode Deploy (component versions, deployment processes)
11419
12425
  checkmarx Checkmarx One (projects, scans, scan statistics)
11420
12426
  servicenow ServiceNow (change requests)
11421
12427
  contrast Contrast IAST (applications, vulnerability findings)
@@ -11423,8 +12429,10 @@ Services:
11423
12429
  openshift OpenShift / Kubernetes (pods, events, logs, metrics)
11424
12430
  dynatrace Dynatrace (services, entities, problems, traces, Kubernetes workloads)
11425
12431
  logscale LogScale (log query, repositories)
12432
+ splitio Split.IO (feature flags, Change Requests)
12433
+ figma Figma (files, comments, version history)
11426
12434
  config Manage pncli configuration
11427
- skills Download and manage Claude Code skills
12435
+ skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
11428
12436
  jwt JWT token utilities (decode header and payload)
11429
12437
  `);
11430
12438
  program.parseAsync(process.argv).catch((err) => {