@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 +2 -1
- package/copilot-instructions.md +44 -0
- package/dist/{chunk-JORIICS2.js → chunk-VEHCNGUP.js} +21 -2
- package/dist/chunk-VEHCNGUP.js.map +1 -0
- package/dist/cli.js +342 -26
- package/dist/cli.js.map +1 -1
- package/dist/{config-KECF7FJ2.js → config-IJ57IIQA.js} +4 -2
- package/package.json +5 -1
- package/dist/chunk-JORIICS2.js.map +0 -1
- /package/dist/{config-KECF7FJ2.js.map → config-IJ57IIQA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pncli — The Paperwork Nightmare CLI
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@kolatts/pncli)
|
|
4
|
+
[](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
|
|
package/copilot-instructions.md
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
212
|
+
let path6 = remoteUrl;
|
|
212
213
|
let remoteHostname = "";
|
|
213
|
-
const sshProtoMatch =
|
|
214
|
+
const sshProtoMatch = path6.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
|
|
214
215
|
if (sshProtoMatch) {
|
|
215
216
|
remoteHostname = sshProtoMatch[1].toLowerCase();
|
|
216
|
-
|
|
217
|
+
path6 = sshProtoMatch[2];
|
|
217
218
|
} else {
|
|
218
|
-
const gitAtMatch =
|
|
219
|
+
const gitAtMatch = path6.match(/^git@([^:]+)(?::\d+)?:(.*)/);
|
|
219
220
|
if (gitAtMatch) {
|
|
220
221
|
remoteHostname = gitAtMatch[1].toLowerCase();
|
|
221
|
-
|
|
222
|
+
path6 = "/" + gitAtMatch[2];
|
|
222
223
|
} else {
|
|
223
|
-
const httpsMatch =
|
|
224
|
+
const httpsMatch = path6.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
|
|
224
225
|
if (httpsMatch) {
|
|
225
226
|
remoteHostname = httpsMatch[1].toLowerCase();
|
|
226
|
-
|
|
227
|
+
path6 = httpsMatch[2];
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
if (!remoteHostname || remoteHostname !== baseHostname) return null;
|
|
231
|
-
|
|
232
|
-
const gitIdx =
|
|
233
|
-
const sshIdx =
|
|
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 =
|
|
237
|
+
const repo = path6.slice(markerIdx + 6);
|
|
237
238
|
if (!repo) return null;
|
|
238
|
-
const before =
|
|
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
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
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
|
|
3600
|
-
await http.ado(
|
|
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
|
|
4436
|
-
const existing = JSON.parse(
|
|
4599
|
+
const fs4 = await import("fs");
|
|
4600
|
+
const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
4437
4601
|
existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
|
|
4438
|
-
|
|
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
|
|
4468
|
-
const existing = JSON.parse(
|
|
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
|
-
|
|
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-
|
|
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;
|