@kolatts/pncli 1.4.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # pncli — The Paperwork Nightmare CLI
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/%40kolatts%2Fpncli?style=flat-square&color=cb3837&logo=npm)](https://www.npmjs.com/package/@kolatts/pncli)
4
+ [![docs](https://img.shields.io/badge/docs-getting%20started-FF5C39?style=flat-square)](https://kolatts.github.io/pncli/getting-started/)
4
5
 
5
6
  > One command does what three meetings couldn't.
6
7
 
@@ -26,7 +27,7 @@ pncli config init
26
27
  pncli config init --repo
27
28
  ```
28
29
 
29
- For workflow patterns, command examples, and agent integration, see [`copilot-instructions.md`](./copilot-instructions.md).
30
+ For workflow patterns, command examples, and agent integration, see the [Getting Started page](https://kolatts.github.io/pncli/getting-started/) or the raw [`copilot-instructions.md`](./copilot-instructions.md).
30
31
 
31
32
  ## Configuration
32
33
 
@@ -13,8 +13,37 @@ pncli is a CLI tool that provides structured JSON access to Jira, Bitbucket, Con
13
13
  - Run all commands from the repository root.
14
14
  - Project and repo are auto-detected from git remotes. You rarely need `--project` or `--repo` flags.
15
15
 
16
+ ## Provider Detection
17
+
18
+ Before running provider-specific commands, ask the user which tools this repository uses:
19
+
20
+ 1. **Work item tracking:** "Does this repo use **Jira** or **Azure DevOps** for work items and tickets?" — this determines whether to use `pncli jira ...` or `pncli ado work ...` commands.
21
+ 2. **Source control:** "Does this repo use **Bitbucket** or **Azure DevOps** for pull requests and code review?" — this determines whether to use `pncli bitbucket ...` or `pncli ado repo ...` commands.
22
+
23
+ Cache the answers for the session. If the user doesn't know, run `git remote -v` — if a URL contains `/_git/` it's Azure DevOps; if it contains `/scm/` it's Bitbucket.
24
+
25
+ ## Installing Skills
26
+
27
+ pncli ships with Claude Code skills — step-by-step workflow guides that agents can follow. To install them into your repo:
28
+
29
+ ```
30
+ pncli skills install
31
+ ```
32
+
33
+ This downloads the latest pncli skills from GitHub into `.claude/skills/` and also fetches `copilot-instructions.md` into the repo root. Only pncli-managed skills are replaced — any custom skills you've created in `.claude/skills/` are left untouched.
34
+
35
+ Use `--skip-instructions` to skip downloading `copilot-instructions.md`.
36
+
37
+ To see what's installed locally:
38
+
39
+ ```
40
+ pncli skills list
41
+ ```
42
+
16
43
  ## Common Workflows
17
44
 
45
+ > These workflows are also available as Claude Code skills — run `pncli skills install` to download them into your repo, or copy `.claude/skills/` into `~/.claude/skills/` for global access.
46
+
18
47
  ### Review a Pull Request
19
48
 
20
49
  1. Find the PR for the current branch:
@@ -496,8 +525,11 @@ pncli config init
496
525
  pncli config show
497
526
 
498
527
  pncli config set
528
+ --repo Write to repo config (.pncli.json) instead of global config
499
529
 
500
530
  pncli config test
531
+
532
+ pncli config check
501
533
  ```
502
534
 
503
535
  ### Ado
@@ -516,6 +548,18 @@ pncli ado repo
516
548
  pncli ado pipeline
517
549
  ```
518
550
 
551
+ ### Skills
552
+
553
+ ```
554
+ pncli skills install
555
+ --target <dir> Target directory for skills (default: ".claude/skills")
556
+ --skip-instructions Skip downloading copilot-instructions.md (always written
557
+ to repo root)
558
+
559
+ pncli skills list
560
+ --target <dir> Skills directory to scan (default: ".claude/skills")
561
+ ```
562
+
519
563
  <!-- COMMAND-REFERENCE:END -->
520
564
 
521
565
  ## Output Format
@@ -95,7 +95,8 @@ function loadConfig(opts = {}) {
95
95
  },
96
96
  confluence: {
97
97
  baseUrl: process.env[ENV_KEYS.CONFLUENCE_BASE_URL] ?? globalConfig.confluence?.baseUrl,
98
- apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken
98
+ apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
99
+ apiTokenExplicit: !!(process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken)
99
100
  },
100
101
  artifactory: {
101
102
  baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,
@@ -151,6 +152,23 @@ function setConfigValue(key, value, configPath) {
151
152
  current[parts[parts.length - 1]] = value;
152
153
  writeGlobalConfig(existing, configPath);
153
154
  }
155
+ function setRepoConfigValue(key, value) {
156
+ const repoRoot = getRepoRoot();
157
+ const targetDir = repoRoot ?? process.cwd();
158
+ const filePath = path.join(targetDir, ".pncli.json");
159
+ const existing = loadJsonFile(filePath) ?? {};
160
+ const parts = key.split(".");
161
+ let current = existing;
162
+ for (let i = 0; i < parts.length - 1; i++) {
163
+ const part = parts[i];
164
+ if (typeof current[part] !== "object" || current[part] === null) {
165
+ current[part] = {};
166
+ }
167
+ current = current[part];
168
+ }
169
+ current[parts[parts.length - 1]] = value;
170
+ writeRepoConfig(existing);
171
+ }
154
172
  function maskConfig(config) {
155
173
  return {
156
174
  ...config,
@@ -192,6 +210,7 @@ export {
192
210
  writeGlobalConfig,
193
211
  writeRepoConfig,
194
212
  setConfigValue,
213
+ setRepoConfigValue,
195
214
  maskConfig
196
215
  };
197
- //# sourceMappingURL=chunk-JORIICS2.js.map
216
+ //# sourceMappingURL=chunk-VEHCNGUP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/config.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { execSync } from 'child_process';\nimport type { GlobalConfig, RepoConfig, ResolvedConfig, JiraDefaults, BitbucketDefaults, SonarDefaults, SdeDefaults, AdoDefaults } from '../types/config.js';\nimport type { CustomFieldDefinition } from '../types/jira.js';\n\nconst ENV_KEYS = {\n EMAIL: 'PNCLI_EMAIL',\n USERID: 'PNCLI_USERID',\n JIRA_BASE_URL: 'PNCLI_JIRA_BASE_URL',\n JIRA_API_TOKEN: 'PNCLI_JIRA_API_TOKEN',\n BITBUCKET_BASE_URL: 'PNCLI_BITBUCKET_BASE_URL',\n BITBUCKET_PAT: 'PNCLI_BITBUCKET_PAT',\n CONFLUENCE_BASE_URL: 'PNCLI_CONFLUENCE_BASE_URL',\n CONFLUENCE_API_TOKEN: 'PNCLI_CONFLUENCE_API_TOKEN',\n ARTIFACTORY_BASE_URL: 'PNCLI_ARTIFACTORY_BASE_URL',\n ARTIFACTORY_TOKEN: 'PNCLI_ARTIFACTORY_TOKEN',\n ARTIFACTORY_REPO_NPM: 'PNCLI_ARTIFACTORY_REPO_NPM',\n ARTIFACTORY_REPO_NUGET: 'PNCLI_ARTIFACTORY_REPO_NUGET',\n ARTIFACTORY_REPO_MAVEN: 'PNCLI_ARTIFACTORY_REPO_MAVEN',\n SONAR_BASE_URL: 'PNCLI_SONAR_BASE_URL',\n SONAR_TOKEN: 'PNCLI_SONAR_TOKEN',\n SDE_CONNECTION: 'PNCLI_SDE_CONNECTION',\n ADO_BASE_URL: 'PNCLI_ADO_BASE_URL',\n ADO_PAT: 'PNCLI_ADO_PAT',\n CONFIG_PATH: 'PNCLI_CONFIG_PATH'\n} as const;\n\nfunction getGlobalConfigPath(overridePath?: string): string {\n if (overridePath) return overridePath;\n const envPath = process.env[ENV_KEYS.CONFIG_PATH];\n if (envPath) return envPath;\n return path.join(os.homedir(), '.pncli', 'config.json');\n}\n\nfunction loadJsonFile<T>(filePath: string): T | null {\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\nfunction getRepoRoot(): string | null {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();\n } catch {\n return null;\n }\n}\n\nfunction parseSdeConnection(connection: string): { token: string; baseUrl: string } | null {\n const idx = connection.indexOf('@');\n if (idx <= 0) return null;\n const token = connection.slice(0, idx);\n const host = connection.slice(idx + 1);\n const baseUrl = /^https?:\\/\\//i.test(host) ? host : `https://${host}`;\n return { token, baseUrl };\n}\n\nfunction mergeCustomFields(\n global: CustomFieldDefinition[] | undefined,\n repo: CustomFieldDefinition[] | undefined\n): CustomFieldDefinition[] {\n const map = new Map<string, CustomFieldDefinition>();\n for (const f of global ?? []) map.set(f.id, f);\n for (const f of repo ?? []) map.set(f.id, f); // repo wins\n return Array.from(map.values());\n}\n\nfunction mergeDefaults(\n global: GlobalConfig['defaults'],\n repo: RepoConfig['defaults']\n): { jira: JiraDefaults; bitbucket: BitbucketDefaults; sonar: SonarDefaults; sde: SdeDefaults; ado: AdoDefaults } {\n return {\n jira: { ...global?.jira, ...repo?.jira },\n bitbucket: { ...global?.bitbucket, ...repo?.bitbucket },\n sonar: { ...global?.sonar, ...repo?.sonar },\n sde: { ...global?.sde, ...repo?.sde },\n ado: { ...global?.ado, ...repo?.ado }\n };\n}\n\nexport interface LoadConfigOptions {\n configPath?: string;\n}\n\nexport function loadConfig(opts: LoadConfigOptions = {}): ResolvedConfig {\n const globalConfigPath = getGlobalConfigPath(opts.configPath);\n const globalConfig = loadJsonFile<GlobalConfig>(globalConfigPath) ?? {};\n\n const repoRoot = getRepoRoot();\n let repoConfig: RepoConfig = {};\n if (repoRoot) {\n repoConfig = loadJsonFile<RepoConfig>(path.join(repoRoot, '.pncli.json')) ?? {};\n }\n\n const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);\n\n return {\n user: {\n email: process.env[ENV_KEYS.EMAIL] ?? globalConfig.user?.email,\n userId: process.env[ENV_KEYS.USERID] ?? globalConfig.user?.userId\n },\n jira: {\n baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,\n apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,\n customFields: mergeCustomFields(globalConfig.jira?.customFields, repoConfig.jira?.customFields)\n },\n bitbucket: {\n baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,\n pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat\n },\n confluence: {\n baseUrl: process.env[ENV_KEYS.CONFLUENCE_BASE_URL] ?? globalConfig.confluence?.baseUrl,\n apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken\n ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,\n apiTokenExplicit: !!(\n process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken\n )\n },\n artifactory: {\n baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,\n token: process.env[ENV_KEYS.ARTIFACTORY_TOKEN] ?? globalConfig.artifactory?.token,\n npmRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NPM] ?? globalConfig.artifactory?.npmRepo,\n nugetRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NUGET] ?? globalConfig.artifactory?.nugetRepo,\n mavenRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_MAVEN] ?? globalConfig.artifactory?.mavenRepo\n },\n sonar: {\n baseUrl: process.env[ENV_KEYS.SONAR_BASE_URL] ?? globalConfig.sonar?.baseUrl,\n token: process.env[ENV_KEYS.SONAR_TOKEN] ?? globalConfig.sonar?.token\n },\n sde: (() => {\n const raw = process.env[ENV_KEYS.SDE_CONNECTION] ?? globalConfig.sde?.connection;\n const parsed = raw ? parseSdeConnection(raw) : null;\n return { baseUrl: parsed?.baseUrl, token: parsed?.token };\n })(),\n ado: {\n baseUrl: process.env[ENV_KEYS.ADO_BASE_URL] ?? globalConfig.ado?.baseUrl,\n pat: process.env[ENV_KEYS.ADO_PAT] ?? globalConfig.ado?.pat,\n fieldAliases: globalConfig.ado?.fieldAliases ?? {},\n discoveredFields: globalConfig.ado?.discoveredFields ?? [],\n discoveredTypes: globalConfig.ado?.discoveredTypes ?? []\n },\n defaults: mergedDefaults\n };\n}\n\nexport function writeGlobalConfig(config: GlobalConfig, configPath?: string): void {\n const filePath = getGlobalConfigPath(configPath);\n const dir = path.dirname(filePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\\n', 'utf8');\n}\n\nexport function writeRepoConfig(config: RepoConfig): void {\n const repoRoot = getRepoRoot();\n const targetDir = repoRoot ?? process.cwd();\n fs.writeFileSync(path.join(targetDir, '.pncli.json'), JSON.stringify(config, null, 2) + '\\n', 'utf8');\n}\n\nexport function setConfigValue(key: string, value: string, configPath?: string): void {\n const filePath = getGlobalConfigPath(configPath);\n const existing = loadJsonFile<GlobalConfig>(filePath) ?? {};\n\n const parts = key.split('.');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = existing;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!;\n if (typeof current[part] !== 'object' || current[part] === null) {\n current[part] = {};\n }\n current = current[part];\n }\n current[parts[parts.length - 1]!] = value;\n\n writeGlobalConfig(existing, configPath);\n}\n\nexport function setRepoConfigValue(key: string, value: string): void {\n const repoRoot = getRepoRoot();\n const targetDir = repoRoot ?? process.cwd();\n const filePath = path.join(targetDir, '.pncli.json');\n const existing = loadJsonFile<RepoConfig>(filePath) ?? {};\n\n const parts = key.split('.');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = existing;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!;\n if (typeof current[part] !== 'object' || current[part] === null) {\n current[part] = {};\n }\n current = current[part];\n }\n current[parts[parts.length - 1]!] = value;\n\n writeRepoConfig(existing);\n}\n\nexport function maskConfig(config: ResolvedConfig): unknown {\n return {\n ...config,\n jira: {\n ...config.jira,\n apiToken: config.jira.apiToken ? '***' : undefined,\n customFields: config.jira.customFields\n },\n bitbucket: {\n ...config.bitbucket,\n pat: config.bitbucket.pat ? '***' : undefined\n },\n confluence: {\n ...config.confluence,\n apiToken: config.confluence.apiToken ? '***' : undefined\n },\n artifactory: {\n ...config.artifactory,\n token: config.artifactory.token ? '***' : undefined\n },\n sonar: {\n ...config.sonar,\n token: config.sonar.token ? '***' : undefined\n },\n sde: {\n baseUrl: config.sde.baseUrl,\n token: config.sde.token ? '***' : undefined\n },\n ado: {\n ...config.ado,\n pat: config.ado.pat ? '***' : undefined\n }\n };\n}\n\nexport { getGlobalConfigPath };\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -4,9 +4,10 @@ import {
4
4
  loadConfig,
5
5
  maskConfig,
6
6
  setConfigValue,
7
+ setRepoConfigValue,
7
8
  writeGlobalConfig,
8
9
  writeRepoConfig
9
- } from "./chunk-JORIICS2.js";
10
+ } from "./chunk-VEHCNGUP.js";
10
11
  import {
11
12
  ExitCode,
12
13
  PncliError,
@@ -208,34 +209,34 @@ function parseAdoRemote(remoteUrl, adoBaseUrl) {
208
209
  } catch {
209
210
  baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
210
211
  }
211
- let path5 = remoteUrl;
212
+ let path6 = remoteUrl;
212
213
  let remoteHostname = "";
213
- const sshProtoMatch = path5.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
214
+ const sshProtoMatch = path6.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
214
215
  if (sshProtoMatch) {
215
216
  remoteHostname = sshProtoMatch[1].toLowerCase();
216
- path5 = sshProtoMatch[2];
217
+ path6 = sshProtoMatch[2];
217
218
  } else {
218
- const gitAtMatch = path5.match(/^git@([^:]+)(?::\d+)?:(.*)/);
219
+ const gitAtMatch = path6.match(/^git@([^:]+)(?::\d+)?:(.*)/);
219
220
  if (gitAtMatch) {
220
221
  remoteHostname = gitAtMatch[1].toLowerCase();
221
- path5 = "/" + gitAtMatch[2];
222
+ path6 = "/" + gitAtMatch[2];
222
223
  } else {
223
- const httpsMatch = path5.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
224
+ const httpsMatch = path6.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
224
225
  if (httpsMatch) {
225
226
  remoteHostname = httpsMatch[1].toLowerCase();
226
- path5 = httpsMatch[2];
227
+ path6 = httpsMatch[2];
227
228
  }
228
229
  }
229
230
  }
230
231
  if (!remoteHostname || remoteHostname !== baseHostname) return null;
231
- path5 = path5.replace(/\.git$/, "");
232
- const gitIdx = path5.indexOf("/_git/");
233
- const sshIdx = path5.indexOf("/_ssh/");
232
+ path6 = path6.replace(/\.git$/, "");
233
+ const gitIdx = path6.indexOf("/_git/");
234
+ const sshIdx = path6.indexOf("/_ssh/");
234
235
  const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
235
236
  if (markerIdx === -1) return null;
236
- const repo = path5.slice(markerIdx + 6);
237
+ const repo = path6.slice(markerIdx + 6);
237
238
  if (!repo) return null;
238
- const before = path5.slice(1, markerIdx);
239
+ const before = path6.slice(1, markerIdx);
239
240
  const parts = before.split("/").filter(Boolean);
240
241
  if (parts.length < 2) return null;
241
242
  const project = parts[parts.length - 1];
@@ -3497,6 +3498,7 @@ function buildDefaultAliases(fields) {
3497
3498
  }
3498
3499
 
3499
3500
  // src/services/config/commands.ts
3501
+ import chalk from "chalk";
3500
3502
  import fs2 from "fs";
3501
3503
  function registerConfigCommands(program2) {
3502
3504
  const config = program2.command("config").description("Manage pncli configuration");
@@ -3526,12 +3528,17 @@ function registerConfigCommands(program2) {
3526
3528
  fail(err, "config", "show", start);
3527
3529
  }
3528
3530
  });
3529
- config.command("set").description("Set a config value by dot-notation key (e.g. jira.baseUrl https://...)").argument("<key>", "Config key in dot notation").argument("<value>", "Value to set").action((key, value) => {
3531
+ config.command("set").description("Set a config value by dot-notation key (e.g. jira.baseUrl https://...)").argument("<key>", "Config key in dot notation").argument("<value>", "Value to set").option("--repo", "Write to repo config (.pncli.json) instead of global config").action((key, value, cmdOpts) => {
3530
3532
  const start = Date.now();
3531
3533
  try {
3532
- const opts = program2.optsWithGlobals();
3533
- setConfigValue(key, value, opts.config);
3534
- success({ key, value }, "config", "set", start);
3534
+ if (cmdOpts.repo) {
3535
+ setRepoConfigValue(key, value);
3536
+ success({ key, value, target: ".pncli.json" }, "config", "set", start);
3537
+ } else {
3538
+ const opts = program2.optsWithGlobals();
3539
+ setConfigValue(key, value, opts.config);
3540
+ success({ key, value }, "config", "set", start);
3541
+ }
3535
3542
  } catch (err) {
3536
3543
  fail(err, "config", "set", start);
3537
3544
  }
@@ -3596,8 +3603,8 @@ function registerConfigCommands(program2) {
3596
3603
  if (cfg.ado.baseUrl) {
3597
3604
  try {
3598
3605
  const collection = cfg.defaults.ado?.collection;
3599
- const path5 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3600
- await http.ado(path5);
3606
+ const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3607
+ await http.ado(path6);
3601
3608
  results.ado = { ok: true, message: "connected" };
3602
3609
  } catch (err) {
3603
3610
  results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -3610,6 +3617,148 @@ function registerConfigCommands(program2) {
3610
3617
  fail(err, "config", "test", start);
3611
3618
  }
3612
3619
  });
3620
+ config.command("check").description("Check PAT status for every service: Blank, Valid, or Invalid").option("--output <format>", "Output format: json or table", "json").action(async (cmdOpts) => {
3621
+ const start = Date.now();
3622
+ try {
3623
+ let categorize2 = function(err) {
3624
+ const status = err.status ?? -1;
3625
+ if (status === 401 || status === 403) {
3626
+ return { status: "invalid", message: `auth rejected (${status})` };
3627
+ }
3628
+ if (status === 0) {
3629
+ return { status: "error", message: err instanceof Error ? err.message : String(err) };
3630
+ }
3631
+ return { status: "error", message: err instanceof Error ? err.message : String(err) };
3632
+ };
3633
+ var categorize = categorize2;
3634
+ const opts = program2.optsWithGlobals();
3635
+ const cfg = loadConfig({ configPath: opts.config });
3636
+ const http = createHttpClient(cfg, false);
3637
+ const results = {};
3638
+ if (!cfg.jira.apiToken) {
3639
+ results.jira = { status: "blank", message: "not configured" };
3640
+ } else if (!cfg.jira.baseUrl) {
3641
+ results.jira = { status: "error", message: "baseUrl not configured" };
3642
+ } else {
3643
+ try {
3644
+ await http.jira("/rest/api/2/myself", { timeoutMs: 1e4 });
3645
+ results.jira = { status: "valid", message: "ok" };
3646
+ } catch (err) {
3647
+ results.jira = categorize2(err);
3648
+ }
3649
+ }
3650
+ if (!cfg.bitbucket.pat) {
3651
+ results.bitbucket = { status: "blank", message: "not configured" };
3652
+ } else if (!cfg.bitbucket.baseUrl) {
3653
+ results.bitbucket = { status: "error", message: "baseUrl not configured" };
3654
+ } else {
3655
+ try {
3656
+ await http.bitbucket("/rest/api/1.0/application-properties", { timeoutMs: 1e4 });
3657
+ results.bitbucket = { status: "valid", message: "ok" };
3658
+ } catch (err) {
3659
+ results.bitbucket = categorize2(err);
3660
+ }
3661
+ }
3662
+ if (!cfg.confluence.apiTokenExplicit && !cfg.jira.apiToken) {
3663
+ results.confluence = { status: "blank", message: "not configured" };
3664
+ } else if (!cfg.confluence.apiToken) {
3665
+ results.confluence = { status: "blank", message: "not configured" };
3666
+ } else if (!cfg.confluence.baseUrl) {
3667
+ results.confluence = { status: "error", message: "baseUrl not configured" };
3668
+ } else {
3669
+ try {
3670
+ await http.confluence("/rest/api/space", { params: { limit: 1 }, timeoutMs: 1e4 });
3671
+ const msg = cfg.confluence.apiTokenExplicit ? "ok" : "ok (inherited from Jira token)";
3672
+ results.confluence = { status: "valid", message: msg };
3673
+ } catch (err) {
3674
+ results.confluence = categorize2(err);
3675
+ }
3676
+ }
3677
+ if (!cfg.sonar.token) {
3678
+ results.sonar = { status: "blank", message: "not configured" };
3679
+ } else if (!cfg.sonar.baseUrl) {
3680
+ results.sonar = { status: "error", message: "baseUrl not configured" };
3681
+ } else {
3682
+ try {
3683
+ await http.sonar("/api/system/status", { timeoutMs: 1e4 });
3684
+ results.sonar = { status: "valid", message: "ok" };
3685
+ } catch (err) {
3686
+ results.sonar = categorize2(err);
3687
+ }
3688
+ }
3689
+ if (!cfg.sde.token) {
3690
+ results.sde = { status: "blank", message: "not configured" };
3691
+ } else if (!cfg.sde.baseUrl) {
3692
+ results.sde = { status: "error", message: "baseUrl not configured" };
3693
+ } else {
3694
+ try {
3695
+ await http.sde("/api/v2/users/me/", { timeoutMs: 1e4 });
3696
+ results.sde = { status: "valid", message: "ok" };
3697
+ } catch (err) {
3698
+ results.sde = categorize2(err);
3699
+ }
3700
+ }
3701
+ if (!cfg.ado.pat) {
3702
+ results.ado = { status: "blank", message: "not configured" };
3703
+ } else if (!cfg.ado.baseUrl) {
3704
+ results.ado = { status: "error", message: "baseUrl not configured" };
3705
+ } else {
3706
+ try {
3707
+ const collection = cfg.defaults.ado?.collection;
3708
+ const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3709
+ await http.ado(path6, { timeoutMs: 1e4 });
3710
+ results.ado = { status: "valid", message: "ok" };
3711
+ } catch (err) {
3712
+ results.ado = categorize2(err);
3713
+ }
3714
+ }
3715
+ const artResult = await checkArtifactoryConnectivity(cfg.artifactory);
3716
+ if (!artResult.configured) {
3717
+ results.artifactory = { status: "blank", message: artResult.error ?? "not configured" };
3718
+ } else if (!artResult.authenticated) {
3719
+ results.artifactory = { status: "invalid", message: artResult.error ?? "auth rejected" };
3720
+ } else if (!artResult.reachable) {
3721
+ results.artifactory = { status: "error", message: artResult.error ?? "unreachable" };
3722
+ } else {
3723
+ results.artifactory = { status: "valid", message: "ok" };
3724
+ }
3725
+ const statuses = Object.values(results).map((r) => r.status);
3726
+ if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
3727
+ else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
3728
+ if (cmdOpts.output === "table") {
3729
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "artifactory"];
3730
+ const labelWidth = 14;
3731
+ const statusWidth = 9;
3732
+ for (const svc of services) {
3733
+ const r = results[svc];
3734
+ if (!r) continue;
3735
+ const label = svc.padEnd(labelWidth);
3736
+ const coloredStatus = r.status === "valid" ? chalk.green("valid".padEnd(statusWidth)) : r.status === "blank" ? chalk.gray("blank".padEnd(statusWidth)) : r.status === "invalid" ? chalk.red("invalid".padEnd(statusWidth)) : chalk.yellow("error".padEnd(statusWidth));
3737
+ process.stdout.write(` ${label}${coloredStatus}${r.message}
3738
+ `);
3739
+ }
3740
+ process.stdout.write("\n");
3741
+ } else {
3742
+ if (opts.pretty) {
3743
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "artifactory"];
3744
+ const labelWidth = 14;
3745
+ const statusWidth = 9;
3746
+ for (const svc of services) {
3747
+ const r = results[svc];
3748
+ if (!r) continue;
3749
+ const label = svc.padEnd(labelWidth);
3750
+ const coloredStatus = r.status === "valid" ? chalk.green("valid".padEnd(statusWidth)) : r.status === "blank" ? chalk.gray("blank".padEnd(statusWidth)) : r.status === "invalid" ? chalk.red("invalid".padEnd(statusWidth)) : chalk.yellow("error".padEnd(statusWidth));
3751
+ process.stderr.write(` ${label}${coloredStatus}${r.message}
3752
+ `);
3753
+ }
3754
+ process.stderr.write("\n");
3755
+ }
3756
+ success(results, "config", "check", start);
3757
+ }
3758
+ } catch (err) {
3759
+ fail(err, "config", "check", start);
3760
+ }
3761
+ });
3613
3762
  }
3614
3763
  async function initGlobalConfig(start) {
3615
3764
  process.stderr.write("pncli config init \u2014 Global configuration\n\n");
@@ -4085,6 +4234,21 @@ var AdoGitClient = class {
4085
4234
  );
4086
4235
  }
4087
4236
  // ── Diffs / Files ─────────────────────────────────────────────────
4237
+ async getPRDiff(collection, project, repo, prId) {
4238
+ const pr = await this.getPR(collection, project, repo, prId);
4239
+ const sourceCommit = pr.lastMergeSourceCommit?.commitId;
4240
+ const targetCommit = pr.lastMergeTargetCommit?.commitId;
4241
+ if (!sourceCommit || !targetCommit) return { changes: [], aheadCount: 0, behindCount: 0 };
4242
+ return this.http.ado(
4243
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/diffs/commits?baseVersion=${encodeURIComponent(targetCommit)}&baseVersionType=commit&targetVersion=${encodeURIComponent(sourceCommit)}&targetVersionType=commit&api-version=${API7}`
4244
+ );
4245
+ }
4246
+ async getCommitStatuses(collection, project, repo, commitId) {
4247
+ const result = await this.http.ado(
4248
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/commits/${encodeURIComponent(commitId)}/statuses?api-version=${API7}`
4249
+ );
4250
+ return result.value ?? [];
4251
+ }
4088
4252
  async listPRChanges(collection, project, repo, prId) {
4089
4253
  const iterations = await this.http.ado(
4090
4254
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/iterations?api-version=${API7}`
@@ -4432,10 +4596,10 @@ function registerAdoWorkCommands(ado) {
4432
4596
  const types = await discoverTypes(workClient, collection, project);
4433
4597
  if (opts.save) {
4434
4598
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4435
- const fs3 = await import("fs");
4436
- const existing = JSON.parse(fs3.readFileSync(globalConfigPath, "utf8") || "{}");
4599
+ const fs4 = await import("fs");
4600
+ const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
4437
4601
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
4438
- fs3.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4602
+ fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4439
4603
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
4440
4604
  }
4441
4605
  success(types, "ado", "work-types", start);
@@ -4464,11 +4628,11 @@ function registerAdoWorkCommands(ado) {
4464
4628
  ) : allFields;
4465
4629
  if (opts.save) {
4466
4630
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4467
- const fs3 = await import("fs");
4468
- const existing = JSON.parse(fs3.readFileSync(globalConfigPath, "utf8") || "{}");
4631
+ const fs4 = await import("fs");
4632
+ const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
4469
4633
  const aliases = buildDefaultAliases(allFields);
4470
4634
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
4471
- fs3.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4635
+ fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4472
4636
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
4473
4637
  }
4474
4638
  success(fields, "ado", "work-fields", start);
@@ -4704,6 +4868,26 @@ function registerAdoRepoCommands(ado) {
4704
4868
  fail(err, "ado", "repo-list-files", start);
4705
4869
  }
4706
4870
  });
4871
+ repo.command("diff").description("Show files changed in a pull request with change types and commit metadata").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4872
+ const start = Date.now();
4873
+ try {
4874
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4875
+ const data = await gitClient.getPRDiff(collection, project, repo2, parseInt(opts.pr, 10));
4876
+ success(data, "ado", "repo-diff", start);
4877
+ } catch (err) {
4878
+ fail(err, "ado", "repo-diff", start);
4879
+ }
4880
+ });
4881
+ repo.command("get-build-status").description("Get CI/build statuses posted to a commit").requiredOption("--commit <sha>", "Commit SHA").action(async (opts) => {
4882
+ const start = Date.now();
4883
+ try {
4884
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4885
+ const data = await gitClient.getCommitStatuses(collection, project, repo2, opts.commit);
4886
+ success(data, "ado", "repo-get-build-status", start);
4887
+ } catch (err) {
4888
+ fail(err, "ado", "repo-get-build-status", start);
4889
+ }
4890
+ });
4707
4891
  repo.command("list-reviewers").description("List reviewers on a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4708
4892
  const start = Date.now();
4709
4893
  try {
@@ -4719,7 +4903,7 @@ function registerAdoRepoCommands(ado) {
4719
4903
  const start = Date.now();
4720
4904
  try {
4721
4905
  const globalOpts = ado.optsWithGlobals();
4722
- const { loadConfig: lc } = await import("./config-KECF7FJ2.js");
4906
+ const { loadConfig: lc } = await import("./config-IJ57IIQA.js");
4723
4907
  const config = lc({ configPath: globalOpts.config });
4724
4908
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4725
4909
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-AJYPJ2Z7.js");
@@ -4898,6 +5082,136 @@ function registerAdoCommands(program2) {
4898
5082
  registerAdoPipelineCommands(ado);
4899
5083
  }
4900
5084
 
5085
+ // src/services/skills/commands.ts
5086
+ import fs3 from "fs";
5087
+ import path5 from "path";
5088
+ var GITHUB_API = "https://api.github.com/repos/kolatts/pncli/contents/.claude/skills";
5089
+ var RAW_BASE = "https://raw.githubusercontent.com/kolatts/pncli/main/.claude/skills";
5090
+ var RAW_COPILOT_INSTRUCTIONS = "https://raw.githubusercontent.com/kolatts/pncli/main/copilot-instructions.md";
5091
+ function registerSkillsCommands(program2) {
5092
+ const skills = program2.command("skills").description("Manage pncli Claude Code skills");
5093
+ skills.command("install").description("Download latest pncli skills and copilot-instructions.md into the current repo").option("--target <dir>", "Target directory for skills", ".claude/skills").option("--skip-instructions", "Skip downloading copilot-instructions.md (always written to repo root)").action(async (opts) => {
5094
+ const start = Date.now();
5095
+ try {
5096
+ const targetDir = path5.resolve(opts.target);
5097
+ warn("Fetching skill list from GitHub...");
5098
+ const listRes = await fetch(GITHUB_API, {
5099
+ headers: { "Accept": "application/vnd.github.v3+json", "User-Agent": "pncli" }
5100
+ });
5101
+ if (!listRes.ok) {
5102
+ throw new Error(`GitHub API returned ${listRes.status}: ${await listRes.text()}`);
5103
+ }
5104
+ const entries = await listRes.json();
5105
+ const skillDirs = entries.filter((e) => e.type === "dir").map((e) => e.name);
5106
+ if (skillDirs.length === 0) {
5107
+ throw new Error("No skills found in the pncli repository");
5108
+ }
5109
+ const resolvedTarget = path5.resolve(targetDir);
5110
+ for (const skillName of skillDirs) {
5111
+ const existingDir = path5.resolve(targetDir, skillName);
5112
+ if (!existingDir.startsWith(resolvedTarget + path5.sep)) continue;
5113
+ if (fs3.existsSync(existingDir)) {
5114
+ fs3.rmSync(existingDir, { recursive: true, force: true });
5115
+ }
5116
+ }
5117
+ warn(`Found ${skillDirs.length} skills. Downloading...`);
5118
+ const installed = [];
5119
+ const failed = [];
5120
+ for (const skillName of skillDirs) {
5121
+ const skillDir = path5.resolve(targetDir, skillName);
5122
+ if (!skillDir.startsWith(resolvedTarget + path5.sep)) {
5123
+ failed.push(skillName);
5124
+ continue;
5125
+ }
5126
+ const skillUrl = `${RAW_BASE}/${skillName}/SKILL.md`;
5127
+ try {
5128
+ const res = await fetch(skillUrl, {
5129
+ headers: { "User-Agent": "pncli" }
5130
+ });
5131
+ if (!res.ok) {
5132
+ failed.push(skillName);
5133
+ continue;
5134
+ }
5135
+ const content = await res.text();
5136
+ fs3.mkdirSync(skillDir, { recursive: true });
5137
+ fs3.writeFileSync(path5.join(skillDir, "SKILL.md"), content, "utf8");
5138
+ installed.push(skillName);
5139
+ } catch {
5140
+ failed.push(skillName);
5141
+ }
5142
+ }
5143
+ warn(`Installed ${installed.length} skill(s) to ${targetDir}`);
5144
+ if (failed.length > 0) {
5145
+ warn(`Failed to download: ${failed.join(", ")}`);
5146
+ }
5147
+ let instructionsWritten = false;
5148
+ if (!opts.skipInstructions) {
5149
+ warn("Downloading copilot-instructions.md...");
5150
+ try {
5151
+ const res = await fetch(RAW_COPILOT_INSTRUCTIONS, {
5152
+ headers: { "User-Agent": "pncli" }
5153
+ });
5154
+ if (res.ok) {
5155
+ const content = await res.text();
5156
+ fs3.writeFileSync("copilot-instructions.md", content, "utf8");
5157
+ instructionsWritten = true;
5158
+ warn("Wrote copilot-instructions.md");
5159
+ } else {
5160
+ warn(`Failed to download copilot-instructions.md (${res.status})`);
5161
+ }
5162
+ } catch {
5163
+ warn("Failed to download copilot-instructions.md");
5164
+ }
5165
+ }
5166
+ success({
5167
+ installed,
5168
+ failed,
5169
+ target: targetDir,
5170
+ total: installed.length,
5171
+ copilotInstructions: instructionsWritten
5172
+ }, "skills", "install", start);
5173
+ } catch (err) {
5174
+ fail(err, "skills", "install", start);
5175
+ }
5176
+ });
5177
+ skills.command("list").description("List locally installed skills").option("--target <dir>", "Skills directory to scan", ".claude/skills").action((opts) => {
5178
+ const start = Date.now();
5179
+ try {
5180
+ const targetDir = path5.resolve(opts.target);
5181
+ if (!fs3.existsSync(targetDir)) {
5182
+ success({ skills: [], message: "No skills directory found. Run: pncli skills install" }, "skills", "list", start);
5183
+ return;
5184
+ }
5185
+ const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
5186
+ const skillPath = path5.join(targetDir, name, "SKILL.md");
5187
+ return fs3.existsSync(skillPath);
5188
+ });
5189
+ const skills2 = skillDirs.map((name) => {
5190
+ const content = fs3.readFileSync(path5.join(targetDir, name, "SKILL.md"), "utf8");
5191
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
5192
+ const data = {};
5193
+ if (frontmatter) {
5194
+ for (const line of frontmatter[1].split("\n")) {
5195
+ const colonIdx = line.indexOf(":");
5196
+ if (colonIdx === -1) continue;
5197
+ data[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
5198
+ }
5199
+ }
5200
+ return {
5201
+ name: data.name || name,
5202
+ slug: name,
5203
+ category: data.category || "other",
5204
+ services: data.services || "",
5205
+ providers: data.providers || "none"
5206
+ };
5207
+ });
5208
+ success({ skills: skills2, total: skills2.length }, "skills", "list", start);
5209
+ } catch (err) {
5210
+ fail(err, "skills", "list", start);
5211
+ }
5212
+ });
5213
+ }
5214
+
4901
5215
  // src/cli.ts
4902
5216
  if (!process.env.PNCLI_VERIFY_TLS) {
4903
5217
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -4928,6 +5242,7 @@ registerSdeCommands(program);
4928
5242
  registerDepsCommands(program);
4929
5243
  registerConfigCommands(program);
4930
5244
  registerAdoCommands(program);
5245
+ registerSkillsCommands(program);
4931
5246
  program.addHelpText("after", `
4932
5247
  Services:
4933
5248
  git Local git operations (status, diff, log, branch)
@@ -4939,6 +5254,7 @@ Services:
4939
5254
  sde SDElements (threat modeling, countermeasures, compliance)
4940
5255
  ado Azure DevOps Server (work items, repos, PRs, pipelines)
4941
5256
  config Manage pncli configuration
5257
+ skills Download and manage Claude Code skills
4942
5258
  `);
4943
5259
  program.parseAsync(process.argv).catch((err) => {
4944
5260
  if (process.exitCode !== void 0) return;