@kolatts/pncli 2.0.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
@@ -9,7 +9,7 @@ import {
9
9
  setRepoConfigValue,
10
10
  writeGlobalConfig,
11
11
  writeRepoConfig
12
- } from "./chunk-PXKUQPPF.js";
12
+ } from "./chunk-LH7WBP7W.js";
13
13
  import {
14
14
  createHttpClient
15
15
  } from "./chunk-ZDJYOI3W.js";
@@ -6365,6 +6365,25 @@ function registerConfigCommands(program2) {
6365
6365
  } else {
6366
6366
  results.openshift = { ok: null, message: "not configured" };
6367
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
+ }
6368
6387
  if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {
6369
6388
  try {
6370
6389
  await http.dynatrace("/api/v2/entities", {
@@ -6374,8 +6393,10 @@ function registerConfigCommands(program2) {
6374
6393
  } catch (err) {
6375
6394
  results.dynatrace = { ok: false, message: err instanceof Error ? err.message : String(err) };
6376
6395
  }
6377
- } else {
6396
+ } else if (Object.keys(cfg.dynatrace.environments).length === 0) {
6378
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" };
6379
6400
  }
6380
6401
  if (cfg.dynatrace.platformUrl && cfg.dynatrace.platformToken) {
6381
6402
  try {
@@ -6396,6 +6417,46 @@ function registerConfigCommands(program2) {
6396
6417
  } else {
6397
6418
  results.dynatrace_platform = { ok: null, message: "not configured" };
6398
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
+ }
6399
6460
  if (cfg.logscale.baseUrl && cfg.logscale.token) {
6400
6461
  try {
6401
6462
  await http.logscale("/api/v1/repositories");
@@ -6621,8 +6682,33 @@ function registerConfigCommands(program2) {
6621
6682
  results.openshift = categorize2(err);
6622
6683
  }
6623
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
+ }
6624
6706
  if (!cfg.dynatrace.apiToken) {
6625
- 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
+ }
6626
6712
  } else if (!cfg.dynatrace.baseUrl) {
6627
6713
  results.dynatrace = { status: "error", message: "baseUrl not configured" };
6628
6714
  } else {
@@ -6658,6 +6744,52 @@ function registerConfigCommands(program2) {
6658
6744
  results.dynatrace_platform = categorize2(err);
6659
6745
  }
6660
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
+ }
6661
6793
  if (!cfg.logscale.token) {
6662
6794
  results.logscale = { status: "blank", message: "not configured" };
6663
6795
  } else if (!cfg.logscale.baseUrl) {
@@ -6697,11 +6829,39 @@ function registerConfigCommands(program2) {
6697
6829
  const statuses = Object.values(results).map((r) => r.status);
6698
6830
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
6699
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
+ ];
6700
6861
  if (cmdOpts.output === "table") {
6701
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform", "logscale", "splitio", "figma"];
6702
- const labelWidth = 14;
6862
+ const labelWidth = Math.max(14, ...allServices.map((s) => s.length + 2));
6703
6863
  const statusWidth = 9;
6704
- for (const svc of services) {
6864
+ for (const svc of allServices) {
6705
6865
  const r = results[svc];
6706
6866
  if (!r) continue;
6707
6867
  const label = svc.padEnd(labelWidth);
@@ -6712,10 +6872,9 @@ function registerConfigCommands(program2) {
6712
6872
  process.stdout.write("\n");
6713
6873
  } else {
6714
6874
  if (opts.pretty) {
6715
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform", "logscale", "splitio", "figma"];
6716
- const labelWidth = 14;
6875
+ const labelWidth = Math.max(14, ...allServices.map((s) => s.length + 2));
6717
6876
  const statusWidth = 9;
6718
- for (const svc of services) {
6877
+ for (const svc of allServices) {
6719
6878
  const r = results[svc];
6720
6879
  if (!r) continue;
6721
6880
  const label = svc.padEnd(labelWidth);
@@ -7393,6 +7552,9 @@ async function initGlobalConfig(start) {
7393
7552
  }
7394
7553
  const existingGlobal = loadJsonFile(getGlobalConfigPath());
7395
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;
7396
7558
  writeGlobalConfig({
7397
7559
  jenkinsInstances: existingInstances,
7398
7560
  user: {
@@ -7491,15 +7653,25 @@ async function initGlobalConfig(start) {
7491
7653
  ...useOpenShift && openShiftBaseUrl ? {
7492
7654
  openshift: {
7493
7655
  baseUrl: normalizeBaseUrl(openShiftBaseUrl),
7494
- 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 } : {}
7495
7660
  }
7496
- } : {},
7661
+ } : existingOpenshift ? { openshift: { ...existingOpenshift } } : {},
7497
7662
  ...useDynatrace && dynatraceBaseUrl ? {
7498
7663
  dynatrace: {
7499
7664
  baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
7500
7665
  apiToken: dynatraceApiToken || void 0,
7501
7666
  platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
7502
- 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
7503
7675
  }
7504
7676
  } : {},
7505
7677
  ...useLogscale && logscaleBaseUrl ? {
@@ -8603,7 +8775,7 @@ function registerAdoRepoCommands(ado) {
8603
8775
  const start = Date.now();
8604
8776
  try {
8605
8777
  const globalOpts = ado.optsWithGlobals();
8606
- const { loadConfig: lc } = await import("./config-YHYQTGKT.js");
8778
+ const { loadConfig: lc } = await import("./config-JGBUUXQX.js");
8607
8779
  const config = lc({ configPath: globalOpts.config });
8608
8780
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
8609
8781
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
@@ -9644,24 +9816,51 @@ function assertInteractive(hint) {
9644
9816
  }
9645
9817
  }
9646
9818
  var AGENT_PATHS = {
9647
- "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") },
9648
9821
  "claude-code": { project: ".claude/skills", user: path8.join(os.homedir(), ".claude/skills") }
9649
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
+ }
9650
9831
  function resolveAgentName(opts) {
9651
- return opts.claude ? "claude-code" : opts.agent ?? "github-copilot";
9832
+ return opts.claude ? "claude-code" : opts.agent ?? DEFAULT_AGENT;
9652
9833
  }
9653
9834
  function resolveAgentPaths(agentName) {
9654
9835
  const agentConfig = AGENT_PATHS[agentName];
9655
9836
  if (!agentConfig) {
9656
- throw new Error(`Unknown agent: "${agentName}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
9837
+ throw new Error(`Unknown agent: "${agentName}". Use: ${AGENT_CHOICES}`);
9657
9838
  }
9839
+ warnIfRetargetedAgent(agentName);
9658
9840
  return agentConfig;
9659
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
+ }
9660
9860
  function resolveTargetDir(opts) {
9661
9861
  if (opts.target) return path8.resolve(opts.target);
9662
9862
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9663
- const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
9664
- return path8.resolve(scopePath);
9863
+ return resolveScopedPath(agentConfig, opts.scope ?? "user");
9665
9864
  }
9666
9865
  function getBundledSkillsDir() {
9667
9866
  try {
@@ -9713,14 +9912,16 @@ function resolveSkillRecord(meta, targetDir, skillName) {
9713
9912
  };
9714
9913
  }
9715
9914
  function listActiveSkillDirs(targetDir) {
9915
+ if (!fs4.existsSync(targetDir)) return [];
9716
9916
  return fs4.readdirSync(targetDir).filter((name) => {
9717
9917
  if (name.startsWith(".")) return false;
9718
- const p = path8.join(targetDir, name);
9918
+ const dir = path8.join(targetDir, name);
9719
9919
  try {
9720
- return fs4.statSync(p).isDirectory();
9920
+ if (!fs4.statSync(dir).isDirectory()) return false;
9721
9921
  } catch {
9722
9922
  return false;
9723
9923
  }
9924
+ return fs4.existsSync(path8.join(dir, "SKILL.md"));
9724
9925
  });
9725
9926
  }
9726
9927
  function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
@@ -9832,6 +10033,142 @@ function detectRepoBranch(repoPath) {
9832
10033
  return void 0;
9833
10034
  }
9834
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
+ }
9835
10172
  function recordInstalledSkills(targetDir, skillNames, record) {
9836
10173
  if (skillNames.length === 0) return;
9837
10174
  const meta = readInstalledMeta(targetDir);
@@ -9995,7 +10332,7 @@ function installAllPlugins(resolvedPath, marketplaceName, url, targetDir) {
9995
10332
  const { results, totalInstalled } = installPluginsForMarketplace(resolvedPath, marketplaceName, url, pluginChoices.map((p) => p.name), targetDir);
9996
10333
  return { pluginResults: results, totalInstalled };
9997
10334
  }
9998
- function syncMarketplacePlugins(m, targetDir, force, pluginFilter) {
10335
+ function syncMarketplacePlugins(m, targetDir, force, pluginFilter, installedOnly = false) {
9999
10336
  const marketplaceName = marketplaceLabel(m);
10000
10337
  try {
10001
10338
  const marketplacePath = m.localPath;
@@ -10014,7 +10351,16 @@ function syncMarketplacePlugins(m, targetDir, force, pluginFilter) {
10014
10351
  }
10015
10352
  let pluginNames;
10016
10353
  if (pluginFilter === "all") {
10017
- 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
+ }
10018
10364
  } else if (pluginChoices.some((p) => p.name === pluginFilter)) {
10019
10365
  pluginNames = [pluginFilter];
10020
10366
  } else {
@@ -10084,7 +10430,7 @@ async function marketplaceAddAction(url, localPath, opts, commandName) {
10084
10430
  }
10085
10431
  function registerSkillsCommands(program2) {
10086
10432
  const skills = program2.command("skills").description("Manage pncli Claude Code skills");
10087
- 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) => {
10088
10434
  const start = Date.now();
10089
10435
  try {
10090
10436
  let targetDir;
@@ -10092,8 +10438,7 @@ function registerSkillsCommands(program2) {
10092
10438
  targetDir = path8.resolve(opts.target);
10093
10439
  } else {
10094
10440
  const agentConfig = resolveAgentPaths(opts.agent);
10095
- const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
10096
- targetDir = path8.resolve(scopePath);
10441
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
10097
10442
  }
10098
10443
  const resolvedTarget = path8.resolve(targetDir);
10099
10444
  const bundledDir = getBundledSkillsDir();
@@ -10142,6 +10487,14 @@ function registerSkillsCommands(program2) {
10142
10487
  warn(`Failed to install: ${failed.join(", ")}`);
10143
10488
  }
10144
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
+ }
10145
10498
  success({
10146
10499
  installed,
10147
10500
  failed,
@@ -10149,13 +10502,14 @@ function registerSkillsCommands(program2) {
10149
10502
  total: installed.length,
10150
10503
  agent: opts.target ? "custom" : opts.agent,
10151
10504
  scope: opts.target ? "custom" : opts.scope,
10152
- source: "bundled"
10505
+ source: "bundled",
10506
+ ...opts.target ? { trackedAsCustomTarget: tracked } : {}
10153
10507
  }, "skills", "install", start);
10154
10508
  } catch (err) {
10155
10509
  fail(err, "skills", "install", start);
10156
10510
  }
10157
10511
  });
10158
- 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) => {
10159
10513
  const start = Date.now();
10160
10514
  try {
10161
10515
  let targetDir;
@@ -10163,8 +10517,7 @@ function registerSkillsCommands(program2) {
10163
10517
  targetDir = path8.resolve(opts.target);
10164
10518
  } else {
10165
10519
  const agentConfig = resolveAgentPaths(opts.agent);
10166
- const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
10167
- targetDir = path8.resolve(scopePath);
10520
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
10168
10521
  }
10169
10522
  if (!fs4.existsSync(targetDir)) {
10170
10523
  success({ skills: [], message: `No skills directory found at ${targetDir}. Run: pncli skills install` }, "skills", "list", start);
@@ -10216,7 +10569,7 @@ function registerSkillsCommands(program2) {
10216
10569
  fail(err, "skills", "list", start);
10217
10570
  }
10218
10571
  });
10219
- 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) => {
10220
10573
  const start = Date.now();
10221
10574
  try {
10222
10575
  let targetDir;
@@ -10224,8 +10577,7 @@ function registerSkillsCommands(program2) {
10224
10577
  targetDir = path8.resolve(opts.target);
10225
10578
  } else {
10226
10579
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
10227
- const scopePath = opts.scope === "project" ? agentConfig.project : agentConfig.user;
10228
- targetDir = path8.resolve(scopePath);
10580
+ targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
10229
10581
  }
10230
10582
  const resolvedTarget = path8.resolve(targetDir);
10231
10583
  const skillDir = path8.resolve(targetDir, name);
@@ -10250,9 +10602,77 @@ function registerSkillsCommands(program2) {
10250
10602
  fail(err, "skills", "uninstall", start);
10251
10603
  }
10252
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
+ });
10253
10673
  const marketplace = skills.command("marketplace").description("Manage git-hosted skills marketplaces");
10254
10674
  function withMarketplaceAddOptions(cmd) {
10255
- 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");
10256
10676
  }
10257
10677
  withMarketplaceAddOptions(
10258
10678
  marketplace.command("add").description("Register a new marketplace, clone it, and install all its plugins")
@@ -10270,7 +10690,8 @@ function registerSkillsCommands(program2) {
10270
10690
  name: marketplaceLabel(m),
10271
10691
  repoUrl: m.repoUrl,
10272
10692
  localPath: m.localPath,
10273
- tokenConfigured: !!m.token
10693
+ tokenConfigured: !!m.token,
10694
+ upstreamRemote: m.localPath && fs4.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
10274
10695
  })),
10275
10696
  total: all.length
10276
10697
  }, "skills", "marketplace-list", start);
@@ -10308,7 +10729,7 @@ function registerSkillsCommands(program2) {
10308
10729
  fail(err, "skills", "marketplace-remove", start);
10309
10730
  }
10310
10731
  });
10311
- 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) => {
10312
10733
  const start = Date.now();
10313
10734
  try {
10314
10735
  const configPath = getGlobalConfigPath();
@@ -10318,8 +10739,9 @@ function registerSkillsCommands(program2) {
10318
10739
  }
10319
10740
  const targetDir = resolveAgentPaths(resolveAgentName(opts)).user;
10320
10741
  const force = opts.force ?? false;
10742
+ const installedOnly = opts.installedOnly ?? false;
10321
10743
  if (opts.marketplace === "all") {
10322
- const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, plugin ?? "all"));
10744
+ const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, plugin ?? "all", installedOnly));
10323
10745
  success({ allMarketplaces: true, marketplaces: results, target: targetDir }, "skills", "marketplace-sync", start);
10324
10746
  return;
10325
10747
  }
@@ -10347,7 +10769,7 @@ function registerSkillsCommands(program2) {
10347
10769
  ]
10348
10770
  });
10349
10771
  if (chosen === ALL_MARKETPLACES) {
10350
- const results2 = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, selectedPlugin ?? "all"));
10772
+ const results2 = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, selectedPlugin ?? "all", installedOnly));
10351
10773
  success({ allMarketplaces: true, marketplaces: results2, target: targetDir }, "skills", "marketplace-sync", start);
10352
10774
  return;
10353
10775
  }
@@ -10394,10 +10816,29 @@ function registerSkillsCommands(program2) {
10394
10816
  }, "skills", "marketplace-sync", start);
10395
10817
  return;
10396
10818
  }
10397
- 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
+ }
10398
10839
  const { results, totalInstalled } = installPluginsForMarketplace(marketplacePath, marketplaceName, selectedMarketplace.repoUrl, pluginNames, targetDir);
10399
10840
  if (selectedPlugin === "all") {
10400
- 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);
10401
10842
  } else {
10402
10843
  const single = results[selectedPlugin] ?? { installed: [], failed: [] };
10403
10844
  success({
@@ -10416,7 +10857,7 @@ function registerSkillsCommands(program2) {
10416
10857
  fail(err, "skills", "marketplace-sync", start);
10417
10858
  }
10418
10859
  });
10419
- 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) => {
10420
10861
  const start = Date.now();
10421
10862
  try {
10422
10863
  let targetDir;
@@ -10424,8 +10865,7 @@ function registerSkillsCommands(program2) {
10424
10865
  targetDir = path8.resolve(opts.target);
10425
10866
  } else {
10426
10867
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
10427
- const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
10428
- targetDir = path8.resolve(scopePath);
10868
+ targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
10429
10869
  }
10430
10870
  const resolvedTarget = path8.resolve(targetDir);
10431
10871
  if (!fs4.existsSync(targetDir)) {
@@ -10490,7 +10930,7 @@ function registerSkillsCommands(program2) {
10490
10930
  fail(err, "skills", "marketplace-purge-plugin", start);
10491
10931
  }
10492
10932
  });
10493
- 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) => {
10494
10934
  const start = Date.now();
10495
10935
  try {
10496
10936
  const targetDir = resolveTargetDir(opts);
@@ -10513,7 +10953,7 @@ function registerSkillsCommands(program2) {
10513
10953
  fail(err, "skills", "marketplace-disable", start);
10514
10954
  }
10515
10955
  });
10516
- 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) => {
10517
10957
  const start = Date.now();
10518
10958
  try {
10519
10959
  const targetDir = resolveTargetDir(opts);
@@ -10542,7 +10982,7 @@ function registerSkillsCommands(program2) {
10542
10982
  fail(err, "skills", "marketplace-enable", start);
10543
10983
  }
10544
10984
  });
10545
- 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) => {
10546
10986
  const start = Date.now();
10547
10987
  try {
10548
10988
  assertInteractive("Use `pncli skills marketplace enable|disable <plugin>`, `add <url>`, and `remove <name>` to manage non-interactively.");
@@ -10645,7 +11085,7 @@ function registerSkillsCommands(program2) {
10645
11085
  fail(err, "skills", "marketplace-manage", start);
10646
11086
  }
10647
11087
  });
10648
- 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) => {
10649
11089
  const start = Date.now();
10650
11090
  try {
10651
11091
  const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
@@ -10789,13 +11229,47 @@ function containerState(s) {
10789
11229
  if (s.running) return "Running";
10790
11230
  return "unknown";
10791
11231
  }
10792
- function getHttp2(program2) {
10793
- const opts = program2.optsWithGlobals();
10794
- 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
+ }
10795
11260
  if (!config.openshift.baseUrl) {
10796
11261
  throw new PncliError("OpenShift not configured. Run: pncli config init");
10797
11262
  }
10798
- 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
+ );
10799
11273
  }
10800
11274
  function podAge(creationTimestamp) {
10801
11275
  if (!creationTimestamp) return "unknown";
@@ -10832,11 +11306,39 @@ function parseMemMiB(mem) {
10832
11306
  return null;
10833
11307
  }
10834
11308
  function registerOpenShiftCommands(program2) {
10835
- 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
+ });
10836
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) => {
10837
11339
  const start = Date.now();
10838
11340
  try {
10839
- const http = getHttp2(program2);
11341
+ const http = getHttp2(oc);
10840
11342
  const params = {};
10841
11343
  if (opts.labelSelector) params["labelSelector"] = opts.labelSelector;
10842
11344
  const podList = await http.openshift(
@@ -10945,7 +11447,7 @@ function registerOpenShiftCommands(program2) {
10945
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) => {
10946
11448
  const start = Date.now();
10947
11449
  try {
10948
- const http = getHttp2(program2);
11450
+ const http = getHttp2(oc);
10949
11451
  const params = {};
10950
11452
  const selectors = [];
10951
11453
  if (!opts.all) selectors.push("type=Warning");
@@ -10979,7 +11481,7 @@ function registerOpenShiftCommands(program2) {
10979
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) => {
10980
11482
  const start = Date.now();
10981
11483
  try {
10982
- const http = getHttp2(program2);
11484
+ const http = getHttp2(oc);
10983
11485
  const lines = opts.lines ? parseInt(opts.lines, 10) : 100;
10984
11486
  const logText = await http.openshiftText(
10985
11487
  `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods/${encodeURIComponent(opts.pod)}/log`,
@@ -11011,7 +11513,7 @@ function registerOpenShiftCommands(program2) {
11011
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) => {
11012
11514
  const start = Date.now();
11013
11515
  try {
11014
- const http = getHttp2(program2);
11516
+ const http = getHttp2(oc);
11015
11517
  const metricsList = await http.openshift(
11016
11518
  `/apis/metrics.k8s.io/v1beta1/namespaces/${encodeURIComponent(opts.namespace)}/pods`
11017
11519
  );
@@ -11031,7 +11533,7 @@ function registerOpenShiftCommands(program2) {
11031
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) => {
11032
11534
  const start = Date.now();
11033
11535
  try {
11034
- const http = getHttp2(program2);
11536
+ const http = getHttp2(oc);
11035
11537
  const nsEncoded = encodeURIComponent(opts.namespace);
11036
11538
  const podParams = {};
11037
11539
  if (opts.labelSelector) podParams["labelSelector"] = opts.labelSelector;
@@ -11096,12 +11598,53 @@ function registerOpenShiftCommands(program2) {
11096
11598
  }
11097
11599
 
11098
11600
  // src/services/dynatrace/commands.ts
11099
- function getHttp3(program2) {
11601
+ function getHttp3(program2, envName) {
11100
11602
  const opts = program2.optsWithGlobals();
11101
- return createHttpClient(
11102
- loadConfig({ configPath: opts.config }),
11103
- Boolean(opts.dryRun)
11104
- );
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));
11105
11648
  }
11106
11649
  async function allPages(fetchPage) {
11107
11650
  const items = [];
@@ -11173,12 +11716,13 @@ async function runTraceQuery(http, traceId) {
11173
11716
  return response.result;
11174
11717
  }
11175
11718
  function registerDynatraceCommands(program2) {
11176
- 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>)");
11177
11720
  const entities = dynatrace.command("entities").description("Monitored entity operations");
11178
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) => {
11179
11722
  const start = Date.now();
11180
11723
  try {
11181
- 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);
11182
11726
  success({ totalCount: data.totalCount, entities: data.items }, "dynatrace", "entities list", start);
11183
11727
  } catch (err) {
11184
11728
  fail(err, "dynatrace", "entities list", start);
@@ -11187,7 +11731,8 @@ function registerDynatraceCommands(program2) {
11187
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) => {
11188
11732
  const start = Date.now();
11189
11733
  try {
11190
- const data = await getHttp3(program2).dynatrace(
11734
+ const envName = dynatrace.opts().env;
11735
+ const data = await getHttp3(program2, envName).dynatrace(
11191
11736
  `/api/v2/entities/${encodeURIComponent(opts.id)}`,
11192
11737
  { params: { from: opts.from, to: opts.to, fields: opts.fields } }
11193
11738
  );
@@ -11199,7 +11744,8 @@ function registerDynatraceCommands(program2) {
11199
11744
  dynatrace.command("services").description("List monitored services").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
11200
11745
  const start = Date.now();
11201
11746
  try {
11202
- 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);
11203
11749
  success({ totalCount: data.totalCount, services: data.items }, "dynatrace", "services", start);
11204
11750
  } catch (err) {
11205
11751
  fail(err, "dynatrace", "services", start);
@@ -11208,8 +11754,9 @@ function registerDynatraceCommands(program2) {
11208
11754
  dynatrace.command("workloads").description("List monitored Kubernetes workloads").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
11209
11755
  const start = Date.now();
11210
11756
  try {
11757
+ const envName = dynatrace.opts().env;
11211
11758
  const selector = 'type("CLOUD_APPLICATION")';
11212
- const data = await listEntities(getHttp3(program2), selector, opts);
11759
+ const data = await listEntities(getHttp3(program2, envName), selector, opts);
11213
11760
  success({ totalCount: data.totalCount, workloads: data.items }, "dynatrace", "workloads", start);
11214
11761
  } catch (err) {
11215
11762
  fail(err, "dynatrace", "workloads", start);
@@ -11219,8 +11766,9 @@ function registerDynatraceCommands(program2) {
11219
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) => {
11220
11767
  const start = Date.now();
11221
11768
  try {
11769
+ const envName = dynatrace.opts().env;
11222
11770
  const data = await allPages(async (nextPageKey) => {
11223
- const page = await getHttp3(program2).dynatrace("/api/v2/problems", {
11771
+ const page = await getHttp3(program2, envName).dynatrace("/api/v2/problems", {
11224
11772
  params: nextPageKey ? { nextPageKey } : {
11225
11773
  from: opts.from ?? "now-2h",
11226
11774
  to: opts.to ?? "now",
@@ -11239,7 +11787,8 @@ function registerDynatraceCommands(program2) {
11239
11787
  problems.command("get").description("Get a problem by ID").requiredOption("--id <id>", "Problem ID").option("--fields <fields>", "Additional fields").action(async (opts) => {
11240
11788
  const start = Date.now();
11241
11789
  try {
11242
- const data = await getHttp3(program2).dynatrace(
11790
+ const envName = dynatrace.opts().env;
11791
+ const data = await getHttp3(program2, envName).dynatrace(
11243
11792
  `/api/v2/problems/${encodeURIComponent(opts.id)}`,
11244
11793
  { params: { fields: opts.fields } }
11245
11794
  );
@@ -11251,7 +11800,8 @@ function registerDynatraceCommands(program2) {
11251
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) => {
11252
11801
  const start = Date.now();
11253
11802
  try {
11254
- const result = await runTraceQuery(getHttp3(program2), opts.id);
11803
+ const envName = dynatrace.opts().env;
11804
+ const result = await runTraceQuery(getHttp3(program2, envName), opts.id);
11255
11805
  success(result, "dynatrace", "trace", start);
11256
11806
  } catch (err) {
11257
11807
  fail(err, "dynatrace", "trace", start);
@@ -11882,7 +12432,7 @@ Services:
11882
12432
  splitio Split.IO (feature flags, Change Requests)
11883
12433
  figma Figma (files, comments, version history)
11884
12434
  config Manage pncli configuration
11885
- skills Download and manage Claude Code skills
12435
+ skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
11886
12436
  jwt JWT token utilities (decode header and payload)
11887
12437
  `);
11888
12438
  program.parseAsync(process.argv).catch((err) => {