@kolatts/pncli 1.17.0 → 1.19.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
@@ -8,7 +8,7 @@ import {
8
8
  setRepoConfigValue,
9
9
  writeGlobalConfig,
10
10
  writeRepoConfig
11
- } from "./chunk-FFVQ4PA7.js";
11
+ } from "./chunk-FXY262BR.js";
12
12
  import {
13
13
  ExitCode,
14
14
  PncliError,
@@ -20,7 +20,7 @@ import {
20
20
  success,
21
21
  warn,
22
22
  writeRawOutput
23
- } from "./chunk-XPPEXJBP.js";
23
+ } from "./chunk-WM5NYVEO.js";
24
24
  import {
25
25
  AdoCoreClient
26
26
  } from "./chunk-VDL32PSG.js";
@@ -683,9 +683,9 @@ var BitbucketClient = class {
683
683
  async getDiff(project, repo, prId, file, contextLines) {
684
684
  const params = {};
685
685
  if (contextLines !== void 0) params.contextLines = contextLines;
686
- if (file) params.path = file;
686
+ const diffPath = file ? `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff/${file}` : `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff`;
687
687
  const result = await this.http.bitbucket(
688
- `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff`,
688
+ diffPath,
689
689
  { params }
690
690
  );
691
691
  return typeof result === "string" ? result : JSON.stringify(result);
@@ -734,6 +734,19 @@ var BitbucketClient = class {
734
734
  );
735
735
  return result.values ?? [];
736
736
  }
737
+ async createRepo(opts) {
738
+ return this.http.bitbucket(
739
+ `${API}/projects/${opts.project}/repos`,
740
+ {
741
+ method: "POST",
742
+ body: {
743
+ name: opts.name,
744
+ scmId: "git",
745
+ ...opts.description !== void 0 ? { description: opts.description } : {}
746
+ }
747
+ }
748
+ );
749
+ }
737
750
  };
738
751
 
739
752
  // src/services/git/commands.ts
@@ -837,7 +850,7 @@ function registerGitCommands(program2) {
837
850
  }
838
851
 
839
852
  // src/services/jira/commands.ts
840
- import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
853
+ import { readFileSync as readFileSync3, existsSync, writeFileSync, mkdirSync } from "fs";
841
854
  import path from "path";
842
855
 
843
856
  // src/services/jira/client.ts
@@ -911,7 +924,7 @@ var JiraClient = class {
911
924
  if (opts.description) fields.description = opts.description;
912
925
  if (opts.priority) fields.priority = { name: opts.priority };
913
926
  if (opts.assignee) fields.assignee = { name: opts.assignee };
914
- if (opts.labels) fields.labels = opts.labels;
927
+ if (opts.labels?.length) fields.labels = opts.labels;
915
928
  Object.assign(fields, opts.customFieldValues ?? {});
916
929
  await this.http.jira(`${API2}/issue/${key}`, {
917
930
  method: "PUT",
@@ -1207,7 +1220,119 @@ function formatFieldValue(value, type) {
1207
1220
  }
1208
1221
  }
1209
1222
 
1223
+ // src/services/jira/input-schema.ts
1224
+ var JIRA_INPUT_FILE_SCHEMA = {
1225
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1226
+ title: "pncli Jira issue input file",
1227
+ description: "Shape accepted by --input-file on `pncli jira create-issue` / `update-issue`. CLI flags (--summary, --description, --priority, --field, ...) override matching keys here; overridden keys are reported in meta.overrides.",
1228
+ type: "object",
1229
+ properties: {
1230
+ project: { type: "string", description: "Project key (create-issue only)" },
1231
+ issueType: { type: "string", description: "Issue type name, e.g. Bug, Story, Task (create-issue only)" },
1232
+ fields: {
1233
+ type: "object",
1234
+ description: 'Field dictionary. Built-in keys: summary, description, priority, assignee, labels, parent. Any other key resolves as a custom field: a friendly name registered via `pncli jira fields`, or a raw field id (e.g. customfield_10032) passed through untouched \u2014 custom fields never need to be pre-registered in config. Any string value may be "@path/to/file" to read that field\'s content from a file instead of inlining it (useful for large rich-text fields).',
1235
+ additionalProperties: true
1236
+ }
1237
+ },
1238
+ additionalProperties: false
1239
+ };
1240
+ var JIRA_INPUT_FILE_EXAMPLE = {
1241
+ project: "PROJ",
1242
+ issueType: "Bug",
1243
+ fields: {
1244
+ summary: "Login broken on Safari",
1245
+ description: "@desc.html",
1246
+ priority: "High",
1247
+ labels: ["frontend", "urgent"],
1248
+ // Raw field id — passed through untouched, no pre-registration needed. A friendly
1249
+ // custom-field name (e.g. "Story Points") also works, but only once it's registered
1250
+ // via `pncli config set jira.customFields` / discovered with `pncli jira fields`.
1251
+ customfield_10032: "@ac.md"
1252
+ }
1253
+ };
1254
+
1255
+ // src/lib/input.ts
1256
+ import { readFileSync as readFileSync2 } from "fs";
1257
+ function resolveTextInput(inline, file, optionName) {
1258
+ if (inline !== void 0 && file !== void 0) {
1259
+ throw new PncliError(`Cannot specify both --${optionName} and --${optionName}-file`, 1);
1260
+ }
1261
+ if (file !== void 0) {
1262
+ try {
1263
+ return readFileSync2(file === "-" ? 0 : file, "utf8");
1264
+ } catch (e) {
1265
+ const source = file === "-" ? "stdin" : `file "${file}"`;
1266
+ throw new PncliError(`Cannot read ${optionName} ${source}: ${e.message}`, 1);
1267
+ }
1268
+ }
1269
+ return inline;
1270
+ }
1271
+ function readJsonInputFile(filePath, optionName = "input-file") {
1272
+ let raw;
1273
+ try {
1274
+ raw = readFileSync2(filePath === "-" ? 0 : filePath, "utf8");
1275
+ } catch (e) {
1276
+ const source = filePath === "-" ? "stdin" : `file "${filePath}"`;
1277
+ throw new PncliError(`Cannot read --${optionName} ${source}: ${e.message}`, 1);
1278
+ }
1279
+ try {
1280
+ return JSON.parse(raw);
1281
+ } catch (e) {
1282
+ throw new PncliError(`--${optionName} must contain valid JSON: ${e.message}`, 1);
1283
+ }
1284
+ }
1285
+ function resolveAtFileRef(value) {
1286
+ if (typeof value !== "string" || !value.startsWith("@")) return value;
1287
+ const filePath = value.slice(1);
1288
+ try {
1289
+ return readFileSync2(filePath, "utf8");
1290
+ } catch (e) {
1291
+ throw new PncliError(`Cannot read referenced file "${filePath}": ${e.message}`, 1);
1292
+ }
1293
+ }
1294
+ function mergeWithOverrides(fromJson, fromFlags) {
1295
+ const merged = { ...fromJson };
1296
+ const overrides = [];
1297
+ for (const [key, value] of Object.entries(fromFlags)) {
1298
+ if (value === void 0) continue;
1299
+ if (Object.prototype.hasOwnProperty.call(fromJson, key)) overrides.push(key);
1300
+ merged[key] = value;
1301
+ }
1302
+ return { merged, overrides };
1303
+ }
1304
+
1210
1305
  // src/services/jira/commands.ts
1306
+ var BUILTIN_ISSUE_FIELDS = /* @__PURE__ */ new Set(["summary", "description", "priority", "assignee", "labels", "parent"]);
1307
+ function splitFieldsDictionary(fields, fieldMap) {
1308
+ const builtin = {};
1309
+ const custom = {};
1310
+ for (const [rawKey, rawValue] of Object.entries(fields)) {
1311
+ const value = resolveAtFileRef(rawValue);
1312
+ const key = rawKey.toLowerCase();
1313
+ if (BUILTIN_ISSUE_FIELDS.has(key)) {
1314
+ builtin[key] = value;
1315
+ continue;
1316
+ }
1317
+ const def = fieldMap.byName.get(key) ?? fieldMap.byId.get(rawKey);
1318
+ if (def) {
1319
+ custom[def.id] = value;
1320
+ } else if (!/\s/.test(rawKey)) {
1321
+ custom[rawKey] = value;
1322
+ } else {
1323
+ throw new PncliError(
1324
+ `Unknown custom field: "${rawKey}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1325
+ 1
1326
+ );
1327
+ }
1328
+ }
1329
+ return { builtin, custom };
1330
+ }
1331
+ function normalizeLabels(value) {
1332
+ if (value === void 0) return void 0;
1333
+ if (Array.isArray(value)) return value.map((v) => String(v).trim());
1334
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
1335
+ }
1211
1336
  function getClient(program2) {
1212
1337
  const opts = program2.optsWithGlobals();
1213
1338
  const config = loadConfig({ configPath: opts.config });
@@ -1247,7 +1372,7 @@ function registerJiraCommands(program2) {
1247
1372
  fail(err, "jira", "get-issue", start);
1248
1373
  }
1249
1374
  });
1250
- jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", "Parent issue key \u2014 sets fields.parent in the create payload").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
1375
+ jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").option("--summary <text>", "Issue summary (required, unless supplied via --input-file)").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", "Parent issue key \u2014 sets fields.parent in the create payload").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").option("--input-file <path>", "JSON file describing the whole issue ({ project, issueType, fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli jira schema").action(async (opts) => {
1251
1376
  const start = Date.now();
1252
1377
  let fieldMap = buildFieldMap([]);
1253
1378
  try {
@@ -1255,35 +1380,87 @@ function registerJiraCommands(program2) {
1255
1380
  const client = resolved.client;
1256
1381
  fieldMap = resolved.fieldMap;
1257
1382
  const defaults = getDefaults(program2);
1258
- const project = opts.project ?? defaults.project;
1259
- const issueType = opts.type ?? defaults.issueType ?? "Task";
1260
- const priority = opts.priority ?? defaults.priority;
1261
- if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
1262
- const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
1383
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
1384
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitFieldsDictionary(jsonInput.fields, fieldMap) : { builtin: {}, custom: {} };
1385
+ const project = opts.project ?? jsonInput?.project ?? defaults.project;
1386
+ const issueType = opts.type ?? jsonInput?.issueType ?? defaults.issueType ?? "Task";
1387
+ if (!project) throw new PncliError('--project required (or set defaults.jira.project in config, or "project" in --input-file)', 1);
1388
+ const flagBuiltin = {
1389
+ ...opts.summary !== void 0 ? { summary: opts.summary } : {},
1390
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1391
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {},
1392
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
1393
+ ...opts.labels !== void 0 ? { labels: opts.labels } : {},
1394
+ ...opts.parent !== void 0 ? { parent: opts.parent } : {}
1395
+ };
1396
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
1397
+ const summary = builtinFields.summary;
1398
+ if (!summary) throw new PncliError('Must specify --summary or "fields.summary" in --input-file', 1);
1399
+ const priority = builtinFields.priority ?? defaults.priority;
1263
1400
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1264
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1265
- const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, parent: opts.parent, customFieldValues });
1266
- success(data, "jira", "create-issue", start);
1401
+ const flagCustom = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1402
+ const { merged: customFieldValues, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
1403
+ const overrides = [...builtinOverrides, ...customOverrides];
1404
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
1405
+ const data = await client.createIssue({
1406
+ project,
1407
+ issueType,
1408
+ summary,
1409
+ description: builtinFields.description,
1410
+ priority,
1411
+ assignee: builtinFields.assignee,
1412
+ labels: normalizeLabels(builtinFields.labels),
1413
+ parent: builtinFields.parent,
1414
+ customFieldValues
1415
+ });
1416
+ success(data, "jira", "create-issue", start, overrides);
1267
1417
  } catch (err) {
1268
1418
  fail(translateFieldErrors(err, fieldMap), "jira", "create-issue", start);
1269
1419
  }
1270
1420
  });
1271
- jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
1421
+ jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").option("--input-file <path>", "JSON file describing fields to update ({ fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli jira schema").action(async (opts) => {
1272
1422
  const start = Date.now();
1273
1423
  let fieldMap = buildFieldMap([]);
1274
1424
  try {
1275
1425
  const resolved = getClientAndFields(program2);
1276
1426
  const client = resolved.client;
1277
1427
  fieldMap = resolved.fieldMap;
1278
- const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
1428
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
1429
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitFieldsDictionary(jsonInput.fields, fieldMap) : { builtin: {}, custom: {} };
1430
+ const flagBuiltin = {
1431
+ ...opts.summary !== void 0 ? { summary: opts.summary } : {},
1432
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1433
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {},
1434
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
1435
+ ...opts.labels !== void 0 ? { labels: opts.labels } : {}
1436
+ };
1437
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
1279
1438
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1280
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1281
- await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
1282
- success({ updated: opts.key }, "jira", "update-issue", start);
1439
+ const flagCustom = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1440
+ const { merged: customFieldValues, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
1441
+ const overrides = [...builtinOverrides, ...customOverrides];
1442
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
1443
+ await client.updateIssue(opts.key, {
1444
+ summary: builtinFields.summary,
1445
+ description: builtinFields.description,
1446
+ priority: builtinFields.priority,
1447
+ assignee: builtinFields.assignee,
1448
+ labels: normalizeLabels(builtinFields.labels),
1449
+ customFieldValues
1450
+ });
1451
+ success({ updated: opts.key }, "jira", "update-issue", start, overrides);
1283
1452
  } catch (err) {
1284
1453
  fail(translateFieldErrors(err, fieldMap), "jira", "update-issue", start);
1285
1454
  }
1286
1455
  });
1456
+ jira.command("schema").description("Print the --input-file JSON schema and an example for create-issue/update-issue").option("--example-only", "Print only the runnable example JSON (no envelope) \u2014 pipeable straight into --input-file").action((opts) => {
1457
+ const start = Date.now();
1458
+ if (opts.exampleOnly) {
1459
+ writeRawOutput(JSON.stringify(JIRA_INPUT_FILE_EXAMPLE, null, 2) + "\n");
1460
+ return;
1461
+ }
1462
+ success({ schema: JIRA_INPUT_FILE_SCHEMA, example: JIRA_INPUT_FILE_EXAMPLE }, "jira", "schema", start);
1463
+ });
1287
1464
  jira.command("transition-issue").description("Transition a Jira issue to a new status").requiredOption("--key <issue-key>", "Issue key").requiredOption("--transition <name-or-id>", "Transition name or ID").action(async (opts) => {
1288
1465
  const start = Date.now();
1289
1466
  try {
@@ -1492,7 +1669,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1492
1669
  const filePath = value.slice(1);
1493
1670
  let raw;
1494
1671
  try {
1495
- raw = readFileSync2(filePath, "utf8").trim();
1672
+ raw = readFileSync3(filePath, "utf8").trim();
1496
1673
  } catch (e) {
1497
1674
  throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1498
1675
  }
@@ -1510,7 +1687,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1510
1687
  function parseFieldsFile(filePath, fieldMap) {
1511
1688
  let raw;
1512
1689
  try {
1513
- raw = readFileSync2(filePath, "utf8");
1690
+ raw = readFileSync3(filePath, "utf8");
1514
1691
  } catch (e) {
1515
1692
  throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
1516
1693
  }
@@ -1563,6 +1740,24 @@ function getClient2(program2, overrides) {
1563
1740
  }
1564
1741
  function registerBitbucketCommands(program2) {
1565
1742
  const bb = program2.command("bitbucket").description("Bitbucket Server operations").option("--project <key>", "Bitbucket project key").option("--repo <slug>", "Bitbucket repository slug");
1743
+ bb.command("create-repo").description("Create a new repository in a project").requiredOption("--name <name>", "Repository name (slug)").option("--description <desc>", "Repository description").option("--project <key>", "Bitbucket project key (overrides parent --project)").action(async (opts) => {
1744
+ const start = Date.now();
1745
+ try {
1746
+ const globalOpts = bb.optsWithGlobals();
1747
+ const config = loadConfig({ configPath: globalOpts.config });
1748
+ const http = createHttpClient(config, Boolean(globalOpts.dryRun));
1749
+ const client = new BitbucketClient(http);
1750
+ const ctx = getGitContext(config);
1751
+ const project = opts.project ?? globalOpts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
1752
+ if (!project) {
1753
+ throw new PncliError("Could not determine Bitbucket project. Pass --project, or run pncli config init.", 1);
1754
+ }
1755
+ const data = await client.createRepo({ project, name: opts.name, description: opts.description });
1756
+ success(data, "bitbucket", "create-repo", start);
1757
+ } catch (err) {
1758
+ fail(err, "bitbucket", "create-repo", start);
1759
+ }
1760
+ });
1566
1761
  bb.command("list-prs").description("List pull requests").option("--state <state>", "PR state: OPEN|MERGED|DECLINED|ALL", "OPEN").option("--author <username>", "Filter by author username").option("--reviewer <username>", "Filter by reviewer username").action(async (opts) => {
1567
1762
  const start = Date.now();
1568
1763
  try {
@@ -1965,6 +2160,32 @@ var GitHubClient = class {
1965
2160
  }
1966
2161
  return results;
1967
2162
  }
2163
+ async createIssue(opts) {
2164
+ return this.http.github(
2165
+ `/repos/${opts.owner}/${opts.repo}/issues`,
2166
+ {
2167
+ method: "POST",
2168
+ body: {
2169
+ title: opts.title,
2170
+ ...opts.body !== void 0 ? { body: opts.body } : {},
2171
+ ...opts.labels?.length ? { labels: opts.labels } : {},
2172
+ ...opts.assignees?.length ? { assignees: opts.assignees } : {}
2173
+ }
2174
+ }
2175
+ );
2176
+ }
2177
+ async createRepo(opts) {
2178
+ const path8 = opts.org ? `/orgs/${opts.org}/repos` : "/user/repos";
2179
+ return this.http.github(path8, {
2180
+ method: "POST",
2181
+ body: {
2182
+ name: opts.name,
2183
+ ...opts.description !== void 0 ? { description: opts.description } : {},
2184
+ ...opts.private !== void 0 ? { private: opts.private } : {},
2185
+ ...opts.autoInit !== void 0 ? { auto_init: opts.autoInit } : {}
2186
+ }
2187
+ });
2188
+ }
1968
2189
  };
1969
2190
 
1970
2191
  // src/services/github/commands.ts
@@ -1986,6 +2207,50 @@ function getClient3(program2, overrides) {
1986
2207
  }
1987
2208
  function registerGitHubCommands(program2) {
1988
2209
  const gh = program2.command("github").description("GitHub repository operations").option("--owner <owner>", "GitHub owner (user or org)").option("--repo <repo>", "GitHub repository name");
2210
+ gh.command("create-repo").description("Create a new repository (personal or org)").requiredOption("--name <name>", "Repository name").option("--description <desc>", "Repository description").option("--private", "Create as a private repository").option("--auto-init", "Initialize with a README").action(async (opts) => {
2211
+ const start = Date.now();
2212
+ try {
2213
+ const globalOpts = gh.optsWithGlobals();
2214
+ const config = loadConfig({ configPath: globalOpts.config });
2215
+ const http = createHttpClient(config, Boolean(globalOpts.dryRun));
2216
+ const client = new GitHubClient(http);
2217
+ const owner = globalOpts.owner ?? "";
2218
+ const data = await client.createRepo({
2219
+ org: owner || void 0,
2220
+ name: opts.name,
2221
+ description: opts.description,
2222
+ private: opts.private,
2223
+ autoInit: opts.autoInit
2224
+ });
2225
+ success(data, "github", "create-repo", start);
2226
+ } catch (err) {
2227
+ fail(err, "github", "create-repo", start);
2228
+ }
2229
+ });
2230
+ gh.command("create-issue").description("Create a GitHub issue").requiredOption("--title <title>", "Issue title").option("--body <body>", "Issue body (markdown)").option("--body-file <path>", "Read issue body from a file (use - for stdin); overridden by --body").option("--label <label>", "Label to apply (repeatable)", (v, acc) => {
2231
+ acc.push(v);
2232
+ return acc;
2233
+ }, []).option("--assignee <login>", "Assignee login (repeatable)", (v, acc) => {
2234
+ acc.push(v);
2235
+ return acc;
2236
+ }, []).action(async (opts) => {
2237
+ const start = Date.now();
2238
+ try {
2239
+ const { client, owner, repo } = getClient3(gh);
2240
+ const body = opts.body ?? resolveTextInput(void 0, opts.bodyFile, "body");
2241
+ const data = await client.createIssue({
2242
+ owner,
2243
+ repo,
2244
+ title: opts.title,
2245
+ body,
2246
+ labels: opts.label.length ? opts.label : void 0,
2247
+ assignees: opts.assignee.length ? opts.assignee : void 0
2248
+ });
2249
+ success(data, "github", "create-issue", start);
2250
+ } catch (err) {
2251
+ fail(err, "github", "create-issue", start);
2252
+ }
2253
+ });
1989
2254
  gh.command("list-prs").description("List pull requests").addOption(new Option("--state <state>", "PR state").choices(["open", "closed", "all"]).default("open")).option("--head <branch>", "Filter by head branch (user:branch)").option("--base <branch>", "Filter by base branch").action(async (opts) => {
1990
2255
  const start = Date.now();
1991
2256
  try {
@@ -2012,18 +2277,19 @@ function registerGitHubCommands(program2) {
2012
2277
  fail(err, "github", "get-pr", start);
2013
2278
  }
2014
2279
  });
2015
- gh.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--head <branch>", "Source branch (or user:branch for forks)").option("--base <branch>", "Target branch (defaults to config or main)").option("--body <body>", "PR description").option("--draft", "Create as draft PR").option("--owner <owner>", "GitHub owner (overrides parent --owner)").option("--repo <repo>", "GitHub repository name (overrides parent --repo)").action(async (opts) => {
2280
+ gh.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--head <branch>", "Source branch (or user:branch for forks)").option("--base <branch>", "Target branch (defaults to config or main)").option("--body <body>", "PR description").option("--body-file <path>", "Read PR description from a file (use - for stdin); overridden by --body").option("--draft", "Create as draft PR").option("--owner <owner>", "GitHub owner (overrides parent --owner)").option("--repo <repo>", "GitHub repository name (overrides parent --repo)").action(async (opts) => {
2016
2281
  const start = Date.now();
2017
2282
  try {
2018
2283
  const { client, owner, repo, config } = getClient3(gh, { owner: opts.owner, repo: opts.repo });
2019
2284
  const base = opts.base ?? config.defaults.github?.targetBranch ?? "main";
2285
+ const body = opts.body ?? resolveTextInput(void 0, opts.bodyFile, "body");
2020
2286
  const data = await client.createPR({
2021
2287
  owner,
2022
2288
  repo,
2023
2289
  title: opts.title,
2024
2290
  head: opts.head,
2025
2291
  base,
2026
- body: opts.body,
2292
+ body,
2027
2293
  draft: opts.draft
2028
2294
  });
2029
2295
  success(data, "github", "create-pr", start);
@@ -2248,7 +2514,12 @@ function registerGitHubCommands(program2) {
2248
2514
  });
2249
2515
  }
2250
2516
 
2517
+ // src/services/confluence/commands.ts
2518
+ import { existsSync as existsSync2 } from "fs";
2519
+
2251
2520
  // src/services/confluence/client.ts
2521
+ import { readFileSync as readFileSync4 } from "fs";
2522
+ import { basename as basename2 } from "path";
2252
2523
  var API3 = "/rest/api";
2253
2524
  var ConfluenceClient = class {
2254
2525
  constructor(http) {
@@ -2393,9 +2664,52 @@ var ConfluenceClient = class {
2393
2664
  });
2394
2665
  });
2395
2666
  }
2667
+ async uploadAttachment(pageId, filePath, comment) {
2668
+ const fileContent = readFileSync4(filePath);
2669
+ const fileName = basename2(filePath);
2670
+ const mimeType = guessMimeType(filePath);
2671
+ const formData = new FormData();
2672
+ formData.append("file", new Blob([fileContent], { type: mimeType }), fileName);
2673
+ if (comment) formData.append("comment", comment);
2674
+ const result = await this.http.confluenceUpload(
2675
+ `${API3}/content/${pageId}/child/attachment`,
2676
+ formData
2677
+ );
2678
+ return result.results;
2679
+ }
2680
+ async deleteAttachment(attachmentId) {
2681
+ return this.http.confluence(`${API3}/content/${attachmentId}`, { method: "DELETE" });
2682
+ }
2683
+ async getPageHistory(id) {
2684
+ return this.http.confluence(`${API3}/content/${id}/history`);
2685
+ }
2686
+ async convertToStorage(value, fromRepresentation = "markdown") {
2687
+ const result = await this.http.confluence(
2688
+ `${API3}/contentbody/convert/storage`,
2689
+ {
2690
+ method: "POST",
2691
+ body: { value, representation: fromRepresentation }
2692
+ }
2693
+ );
2694
+ return result.value;
2695
+ }
2396
2696
  };
2397
2697
 
2398
2698
  // src/services/confluence/commands.ts
2699
+ function xmlParseHint(err, bodyContent, convertedFromMarkdown = false) {
2700
+ if (!(err instanceof Error)) return null;
2701
+ const m = err.message.match(/at \[row,col\]=\{(\d+),(\d+)\}/);
2702
+ if (!m) return null;
2703
+ const row = parseInt(m[1], 10);
2704
+ const col = parseInt(m[2], 10);
2705
+ const lines = bodyContent.split("\n");
2706
+ const line = lines[row - 1];
2707
+ if (!line) return null;
2708
+ const pointer = " ".repeat(Math.max(0, col - 1)) + "^";
2709
+ const suffix = convertedFromMarkdown ? " (converted from Markdown)" : "";
2710
+ return `Offending line ${row}${suffix}: ${line}
2711
+ ${pointer}`;
2712
+ }
2399
2713
  function getClient4(program2) {
2400
2714
  const opts = program2.optsWithGlobals();
2401
2715
  const config = loadConfig({ configPath: opts.config });
@@ -2473,38 +2787,67 @@ function registerConfluenceCommands(program2) {
2473
2787
  fail(err, "confluence", "search", start);
2474
2788
  }
2475
2789
  });
2476
- confluence.command("create-page").description("Create a new Confluence page").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").requiredOption("--body <html>", "Page body (storage format HTML)").option("--parent-id <id>", "Parent page ID (to nest under a page)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2790
+ confluence.command("create-page").description("Create a new Confluence page").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").option("--body <html>", "Page body (storage format HTML or Markdown when --markdown is set)").option("--body-file <path>", "Path to a file containing the page body ('-' = stdin)").option("--markdown", "Convert body from Markdown to storage format via Confluence API").option("--parent-id <id>", "Parent page ID (to nest under a page)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2477
2791
  const start = Date.now();
2792
+ let bodyContent;
2478
2793
  try {
2479
2794
  const client = getClient4(program2);
2795
+ const rawBody = resolveTextInput(opts.body, opts.bodyFile, "body");
2796
+ if (rawBody === void 0) throw new PncliError("Must specify --body or --body-file", 1);
2797
+ bodyContent = opts.markdown ? await client.convertToStorage(rawBody) : rawBody;
2798
+ const representation = opts.markdown ? "storage" : opts.representation;
2480
2799
  const data = await client.createPage({
2481
2800
  spaceKey: opts.space,
2482
2801
  title: opts.title,
2483
- body: opts.body,
2802
+ body: bodyContent,
2484
2803
  parentId: opts.parentId,
2485
- representation: opts.representation
2804
+ representation
2486
2805
  });
2487
2806
  success(data, "confluence", "create-page", start);
2488
2807
  } catch (err) {
2489
- fail(err, "confluence", "create-page", start);
2808
+ const hint = bodyContent ? xmlParseHint(err, bodyContent, Boolean(opts.markdown)) : null;
2809
+ if (hint) {
2810
+ const msg = err instanceof Error ? err.message : String(err);
2811
+ const status = err instanceof PncliError ? err.status : 1;
2812
+ const url = err instanceof PncliError ? err.url : void 0;
2813
+ fail(new PncliError(`${msg}
2814
+
2815
+ ${hint}`, status, url), "confluence", "create-page", start);
2816
+ } else {
2817
+ fail(err, "confluence", "create-page", start);
2818
+ }
2490
2819
  }
2491
2820
  });
2492
- confluence.command("update-page").description("Update a Confluence page (fetches current version automatically)").requiredOption("--id <page-id>", "Page ID").option("--title <title>", "New page title").option("--body <html>", "New page body (storage format HTML)").option("--status <status>", "Page status: current (default) or draft", "current").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2821
+ confluence.command("update-page").description("Update a Confluence page (fetches current version automatically)").requiredOption("--id <page-id>", "Page ID").option("--title <title>", "New page title").option("--body <html>", "New page body (storage format HTML or Markdown when --markdown is set)").option("--body-file <path>", "Path to a file containing the new page body ('-' = stdin)").option("--markdown", "Convert body from Markdown to storage format via Confluence API").option("--status <status>", "Page status: current (default) or draft", "current").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2493
2822
  const start = Date.now();
2823
+ let bodyContent;
2494
2824
  try {
2495
2825
  const client = getClient4(program2);
2826
+ const rawBody = resolveTextInput(opts.body, opts.bodyFile, "body");
2827
+ bodyContent = rawBody !== void 0 && opts.markdown ? await client.convertToStorage(rawBody) : rawBody;
2828
+ const representation = bodyContent !== void 0 && opts.markdown ? "storage" : opts.representation;
2496
2829
  const current = await client.getPage(opts.id, "version");
2497
2830
  const nextVersion = current.version.number + 1;
2498
2831
  const data = await client.updatePage(opts.id, {
2499
2832
  version: nextVersion,
2500
2833
  title: opts.title ?? current.title,
2501
- body: opts.body,
2834
+ body: bodyContent,
2502
2835
  status: opts.status,
2503
- representation: opts.representation
2836
+ representation
2504
2837
  });
2505
2838
  success(data, "confluence", "update-page", start);
2506
2839
  } catch (err) {
2507
- fail(err, "confluence", "update-page", start);
2840
+ const hint = bodyContent ? xmlParseHint(err, bodyContent, Boolean(opts.markdown)) : null;
2841
+ if (hint) {
2842
+ const msg = err instanceof Error ? err.message : String(err);
2843
+ const status = err instanceof PncliError ? err.status : 1;
2844
+ const url = err instanceof PncliError ? err.url : void 0;
2845
+ fail(new PncliError(`${msg}
2846
+
2847
+ ${hint}`, status, url), "confluence", "update-page", start);
2848
+ } else {
2849
+ fail(err, "confluence", "update-page", start);
2850
+ }
2508
2851
  }
2509
2852
  });
2510
2853
  confluence.command("delete-page").description("Delete a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
@@ -2591,6 +2934,37 @@ function registerConfluenceCommands(program2) {
2591
2934
  fail(err, "confluence", "list-attachments", start);
2592
2935
  }
2593
2936
  });
2937
+ confluence.command("upload-attachment").description("Upload a file as an attachment to a Confluence page").requiredOption("--id <page-id>", "Page ID").requiredOption("--file <path>", "Path to the file to upload").option("--comment <text>", "Optional comment to attach to the file version").action(async (opts) => {
2938
+ const start = Date.now();
2939
+ try {
2940
+ if (!existsSync2(opts.file)) throw new PncliError(`File not found: ${opts.file}`, 1);
2941
+ const client = getClient4(program2);
2942
+ const data = await client.uploadAttachment(opts.id, opts.file, opts.comment);
2943
+ success(data, "confluence", "upload-attachment", start);
2944
+ } catch (err) {
2945
+ fail(err, "confluence", "upload-attachment", start);
2946
+ }
2947
+ });
2948
+ confluence.command("delete-attachment").description("Delete an attachment from Confluence by its content ID (moves it to the trash; does not permanently purge it)").requiredOption("--id <attachment-id>", "Attachment content ID").action(async (opts) => {
2949
+ const start = Date.now();
2950
+ try {
2951
+ const client = getClient4(program2);
2952
+ await client.deleteAttachment(opts.id);
2953
+ success({ deleted: opts.id }, "confluence", "delete-attachment", start);
2954
+ } catch (err) {
2955
+ fail(err, "confluence", "delete-attachment", start);
2956
+ }
2957
+ });
2958
+ confluence.command("get-page-history").description("Get the version history metadata for a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
2959
+ const start = Date.now();
2960
+ try {
2961
+ const client = getClient4(program2);
2962
+ const data = await client.getPageHistory(opts.id);
2963
+ success(data, "confluence", "get-page-history", start);
2964
+ } catch (err) {
2965
+ fail(err, "confluence", "get-page-history", start);
2966
+ }
2967
+ });
2594
2968
  }
2595
2969
 
2596
2970
  // src/services/sonar/client.ts
@@ -4909,8 +5283,8 @@ import password from "@inquirer/password";
4909
5283
  import confirm from "@inquirer/confirm";
4910
5284
 
4911
5285
  // src/services/ado/client/work.ts
4912
- import { readFileSync as readFileSync3 } from "fs";
4913
- import { basename as basename2 } from "path";
5286
+ import { readFileSync as readFileSync5 } from "fs";
5287
+ import { basename as basename3 } from "path";
4914
5288
  var API6 = "7.1";
4915
5289
  var API_PREVIEW = "7.1-preview.4";
4916
5290
  var AdoWorkClient = class {
@@ -5025,8 +5399,8 @@ var AdoWorkClient = class {
5025
5399
  );
5026
5400
  }
5027
5401
  async uploadAttachment(collection, workItemId, filePath, comment) {
5028
- const fileContent = readFileSync3(filePath);
5029
- const fileName = basename2(filePath);
5402
+ const fileContent = readFileSync5(filePath);
5403
+ const fileName = basename3(filePath);
5030
5404
  const attachment = await this.http.adoUpload(
5031
5405
  `/${encodeURIComponent(collection)}/_apis/wit/attachments?fileName=${encodeURIComponent(fileName)}&api-version=${API6}`,
5032
5406
  fileContent,
@@ -5077,32 +5451,32 @@ async function discoverTypes(client, collection, project) {
5077
5451
  }
5078
5452
  return result;
5079
5453
  }
5454
+ var ADO_FIELD_ALIAS_CANDIDATES = [
5455
+ ["title", "System.Title"],
5456
+ ["state", "System.State"],
5457
+ ["assignedto", "System.AssignedTo"],
5458
+ ["assigned-to", "System.AssignedTo"],
5459
+ ["description", "System.Description"],
5460
+ ["areapath", "System.AreaPath"],
5461
+ ["area-path", "System.AreaPath"],
5462
+ ["iterationpath", "System.IterationPath"],
5463
+ ["iteration", "System.IterationPath"],
5464
+ ["tags", "System.Tags"],
5465
+ ["priority", "Microsoft.VSTS.Common.Priority"],
5466
+ ["severity", "Microsoft.VSTS.Common.Severity"],
5467
+ ["effort", "Microsoft.VSTS.Scheduling.Effort"],
5468
+ ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
5469
+ ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
5470
+ ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
5471
+ ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
5472
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
5473
+ ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
5474
+ ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
5475
+ ];
5080
5476
  function buildDefaultAliases(fields) {
5081
5477
  const refNames = new Set(fields.map((f) => f.referenceName));
5082
- const candidates = [
5083
- ["title", "System.Title"],
5084
- ["state", "System.State"],
5085
- ["assignedto", "System.AssignedTo"],
5086
- ["assigned-to", "System.AssignedTo"],
5087
- ["description", "System.Description"],
5088
- ["areapath", "System.AreaPath"],
5089
- ["area-path", "System.AreaPath"],
5090
- ["iterationpath", "System.IterationPath"],
5091
- ["iteration", "System.IterationPath"],
5092
- ["tags", "System.Tags"],
5093
- ["priority", "Microsoft.VSTS.Common.Priority"],
5094
- ["severity", "Microsoft.VSTS.Common.Severity"],
5095
- ["effort", "Microsoft.VSTS.Scheduling.Effort"],
5096
- ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
5097
- ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
5098
- ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
5099
- ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
5100
- ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
5101
- ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
5102
- ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
5103
- ];
5104
5478
  const aliases = {};
5105
- for (const [alias, ref] of candidates) {
5479
+ for (const [alias, ref] of ADO_FIELD_ALIAS_CANDIDATES) {
5106
5480
  if (refNames.has(ref)) {
5107
5481
  aliases[alias] = ref;
5108
5482
  }
@@ -5265,9 +5639,9 @@ function registerConfigCommands(program2) {
5265
5639
  } else {
5266
5640
  results.udeploy = { ok: null, message: "not configured" };
5267
5641
  }
5268
- if (cfg.checkmarx.baseUrl && cfg.checkmarx.username && cfg.checkmarx.password) {
5642
+ if (cfg.checkmarx.baseUrl && cfg.checkmarx.tenantName && (cfg.checkmarx.apiKey || cfg.checkmarx.clientId && cfg.checkmarx.clientSecret)) {
5269
5643
  try {
5270
- await http.checkmarx("/cxrestapi/projects");
5644
+ await http.checkmarx("/api/projects", { params: { limit: 1 } });
5271
5645
  results.checkmarx = { ok: true, message: "connected" };
5272
5646
  } catch (err) {
5273
5647
  results.checkmarx = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -5316,6 +5690,37 @@ function registerConfigCommands(program2) {
5316
5690
  } else {
5317
5691
  results.openshift = { ok: null, message: "not configured" };
5318
5692
  }
5693
+ if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {
5694
+ try {
5695
+ await http.dynatrace("/api/v2/entities", {
5696
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
5697
+ });
5698
+ results.dynatrace = { ok: true, message: "connected" };
5699
+ } catch (err) {
5700
+ results.dynatrace = { ok: false, message: err instanceof Error ? err.message : String(err) };
5701
+ }
5702
+ } else {
5703
+ results.dynatrace = { ok: null, message: "not configured" };
5704
+ }
5705
+ if (cfg.dynatrace.platformUrl && cfg.dynatrace.platformToken) {
5706
+ try {
5707
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5708
+ method: "POST",
5709
+ body: {
5710
+ query: "fetch spans | limit 1",
5711
+ requestTimeoutMilliseconds: 5e3
5712
+ },
5713
+ timeoutMs: 1e4
5714
+ });
5715
+ results.dynatrace_platform = { ok: true, message: "connected" };
5716
+ } catch (err) {
5717
+ results.dynatrace_platform = { ok: false, message: err instanceof Error ? err.message : String(err) };
5718
+ }
5719
+ } else if (cfg.dynatrace.platformUrl || cfg.dynatrace.platformToken) {
5720
+ results.dynatrace_platform = { ok: false, message: "platformUrl and platformToken must both be configured" };
5721
+ } else {
5722
+ results.dynatrace_platform = { ok: null, message: "not configured" };
5723
+ }
5319
5724
  success(results, "config", "test", start);
5320
5725
  } catch (err) {
5321
5726
  fail(err, "config", "test", start);
@@ -5462,13 +5867,13 @@ function registerConfigCommands(program2) {
5462
5867
  } else {
5463
5868
  results.artifactory = { status: "valid", message: "ok" };
5464
5869
  }
5465
- if (!cfg.checkmarx.username || !cfg.checkmarx.password) {
5870
+ if (!cfg.checkmarx.tenantName || !cfg.checkmarx.apiKey && (!cfg.checkmarx.clientId || !cfg.checkmarx.clientSecret)) {
5466
5871
  results.checkmarx = { status: "blank", message: "not configured" };
5467
5872
  } else if (!cfg.checkmarx.baseUrl) {
5468
5873
  results.checkmarx = { status: "error", message: "baseUrl not configured" };
5469
5874
  } else {
5470
5875
  try {
5471
- await http.checkmarx("/cxrestapi/projects", { timeoutMs: 1e4 });
5876
+ await http.checkmarx("/api/projects", { params: { limit: 1 }, timeoutMs: 1e4 });
5472
5877
  results.checkmarx = { status: "valid", message: "ok" };
5473
5878
  } catch (err) {
5474
5879
  results.checkmarx = categorize2(err);
@@ -5523,11 +5928,48 @@ function registerConfigCommands(program2) {
5523
5928
  results.openshift = categorize2(err);
5524
5929
  }
5525
5930
  }
5931
+ if (!cfg.dynatrace.apiToken) {
5932
+ results.dynatrace = { status: "blank", message: "not configured" };
5933
+ } else if (!cfg.dynatrace.baseUrl) {
5934
+ results.dynatrace = { status: "error", message: "baseUrl not configured" };
5935
+ } else {
5936
+ try {
5937
+ await http.dynatrace("/api/v2/entities", {
5938
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 },
5939
+ timeoutMs: 1e4
5940
+ });
5941
+ results.dynatrace = { status: "valid", message: "ok" };
5942
+ } catch (err) {
5943
+ results.dynatrace = categorize2(err);
5944
+ }
5945
+ }
5946
+ if (!cfg.dynatrace.platformUrl && !cfg.dynatrace.platformToken) {
5947
+ results.dynatrace_platform = { status: "blank", message: "not configured" };
5948
+ } else if (!cfg.dynatrace.platformUrl || !cfg.dynatrace.platformToken) {
5949
+ results.dynatrace_platform = {
5950
+ status: "error",
5951
+ message: "platformUrl and platformToken must both be configured"
5952
+ };
5953
+ } else {
5954
+ try {
5955
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5956
+ method: "POST",
5957
+ body: {
5958
+ query: "fetch spans | limit 1",
5959
+ requestTimeoutMilliseconds: 5e3
5960
+ },
5961
+ timeoutMs: 1e4
5962
+ });
5963
+ results.dynatrace_platform = { status: "valid", message: "ok" };
5964
+ } catch (err) {
5965
+ results.dynatrace_platform = categorize2(err);
5966
+ }
5967
+ }
5526
5968
  const statuses = Object.values(results).map((r) => r.status);
5527
5969
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
5528
5970
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
5529
5971
  if (cmdOpts.output === "table") {
5530
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
5972
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5531
5973
  const labelWidth = 14;
5532
5974
  const statusWidth = 9;
5533
5975
  for (const svc of services) {
@@ -5541,7 +5983,7 @@ function registerConfigCommands(program2) {
5541
5983
  process.stdout.write("\n");
5542
5984
  } else {
5543
5985
  if (opts.pretty) {
5544
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
5986
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5545
5987
  const labelWidth = 14;
5546
5988
  const statusWidth = 9;
5547
5989
  for (const svc of services) {
@@ -5874,38 +6316,51 @@ async function initGlobalConfig(start) {
5874
6316
  }
5875
6317
  process.stderr.write("\n\u2500\u2500 Checkmarx \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
5876
6318
  const useCheckmarx = await confirm({
5877
- message: "Configure Checkmarx CxSAST for vulnerability scanning?",
6319
+ message: "Configure Checkmarx One for vulnerability scanning?",
5878
6320
  default: false
5879
6321
  });
5880
6322
  let checkmarxBaseUrl = "";
5881
- let checkmarxUsername = "";
5882
- let checkmarxPassword = "";
6323
+ let checkmarxTenantName = "";
6324
+ let checkmarxApiKey = "";
6325
+ let checkmarxClientId = "";
6326
+ let checkmarxClientSecret = "";
5883
6327
  if (useCheckmarx) {
5884
6328
  checkmarxBaseUrl = await input({
5885
- message: "Checkmarx base URL (e.g. cx.company.com):",
6329
+ message: "Checkmarx One API base URL (e.g. https://ast.checkmarx.net):",
5886
6330
  default: ""
5887
6331
  });
5888
- checkmarxUsername = await input({
5889
- message: "Checkmarx username:",
6332
+ checkmarxTenantName = await input({
6333
+ message: "Checkmarx One tenant name (IAM realm, e.g. mycompany):",
5890
6334
  default: ""
5891
6335
  });
5892
- checkmarxPassword = await password({
5893
- message: "Checkmarx password:",
5894
- validate: (v) => v.length > 0 || "Password cannot be blank"
6336
+ checkmarxApiKey = await password({
6337
+ message: "Checkmarx One API key (leave blank to use an OAuth client):"
5895
6338
  });
5896
- if (checkmarxBaseUrl && checkmarxUsername && checkmarxPassword) {
6339
+ if (!checkmarxApiKey) {
6340
+ checkmarxClientId = await input({
6341
+ message: "Checkmarx One OAuth client ID:",
6342
+ default: ""
6343
+ });
6344
+ checkmarxClientSecret = await password({
6345
+ message: "Checkmarx One OAuth client secret:",
6346
+ validate: (v) => v.length > 0 || "Client secret cannot be blank"
6347
+ });
6348
+ }
6349
+ if (checkmarxBaseUrl && checkmarxTenantName && (checkmarxApiKey || checkmarxClientId && checkmarxClientSecret)) {
5897
6350
  process.stderr.write("\n Verifying connection...\n");
5898
6351
  try {
5899
6352
  const tempConfig = {
5900
6353
  ...loadConfig(),
5901
6354
  checkmarx: {
5902
6355
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
5903
- username: checkmarxUsername,
5904
- password: checkmarxPassword
6356
+ tenantName: checkmarxTenantName,
6357
+ apiKey: checkmarxApiKey || void 0,
6358
+ clientId: checkmarxClientId,
6359
+ clientSecret: checkmarxClientSecret
5905
6360
  }
5906
6361
  };
5907
6362
  const tempHttp = createHttpClient(tempConfig);
5908
- await tempHttp.checkmarx("/cxrestapi/projects");
6363
+ await tempHttp.checkmarx("/api/projects", { params: { limit: 1 } });
5909
6364
  process.stderr.write(" Connected.\n");
5910
6365
  } catch (err) {
5911
6366
  warn(`Could not connect to Checkmarx: ${err instanceof Error ? err.message : String(err)}`);
@@ -6098,6 +6553,59 @@ async function initGlobalConfig(start) {
6098
6553
  }
6099
6554
  }
6100
6555
  }
6556
+ process.stderr.write("\n\u2500\u2500 Dynatrace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
6557
+ const useDynatrace = await confirm({
6558
+ message: "Configure Dynatrace for services, problems, workloads, and traces?",
6559
+ default: false
6560
+ });
6561
+ let dynatraceBaseUrl = "";
6562
+ let dynatraceApiToken = "";
6563
+ let dynatracePlatformUrl = "";
6564
+ let dynatracePlatformToken = "";
6565
+ if (useDynatrace) {
6566
+ dynatraceBaseUrl = await input({
6567
+ message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
6568
+ default: ""
6569
+ });
6570
+ dynatraceApiToken = await password({
6571
+ message: "Environment API token (entities.read and problems.read):"
6572
+ });
6573
+ const configureTraces = await confirm({
6574
+ message: "Configure the latest Dynatrace platform for Grail trace queries?",
6575
+ default: false
6576
+ });
6577
+ if (configureTraces) {
6578
+ dynatracePlatformUrl = await input({
6579
+ message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
6580
+ default: ""
6581
+ });
6582
+ dynatracePlatformToken = await password({
6583
+ message: "Platform token with Grail span read permissions:"
6584
+ });
6585
+ }
6586
+ if (dynatraceBaseUrl && dynatraceApiToken) {
6587
+ process.stderr.write("\n Verifying connection...\n");
6588
+ try {
6589
+ const tempConfig = {
6590
+ ...loadConfig(),
6591
+ dynatrace: {
6592
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6593
+ apiToken: dynatraceApiToken,
6594
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6595
+ platformToken: dynatracePlatformToken || void 0
6596
+ }
6597
+ };
6598
+ const tempHttp = createHttpClient(tempConfig);
6599
+ await tempHttp.dynatrace("/api/v2/entities", {
6600
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
6601
+ });
6602
+ process.stderr.write(" Connected.\n");
6603
+ } catch (err) {
6604
+ warn(`Could not connect to Dynatrace: ${err instanceof Error ? err.message : String(err)}`);
6605
+ warn("Config will be saved anyway. Check your URLs and tokens and re-run pncli config test.");
6606
+ }
6607
+ }
6608
+ }
6101
6609
  process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
6102
6610
  const jiraProject = await input({
6103
6611
  message: "Default Jira project key (optional):",
@@ -6193,8 +6701,10 @@ async function initGlobalConfig(start) {
6193
6701
  ...useCheckmarx && checkmarxBaseUrl ? {
6194
6702
  checkmarx: {
6195
6703
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
6196
- username: checkmarxUsername || void 0,
6197
- password: checkmarxPassword || void 0
6704
+ tenantName: checkmarxTenantName || void 0,
6705
+ apiKey: checkmarxApiKey || void 0,
6706
+ clientId: checkmarxClientId || void 0,
6707
+ clientSecret: checkmarxClientSecret || void 0
6198
6708
  }
6199
6709
  } : {},
6200
6710
  ...useServiceNow && servicenowBaseUrl ? {
@@ -6227,6 +6737,14 @@ async function initGlobalConfig(start) {
6227
6737
  token: openShiftToken || void 0
6228
6738
  }
6229
6739
  } : {},
6740
+ ...useDynatrace && dynatraceBaseUrl ? {
6741
+ dynatrace: {
6742
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6743
+ apiToken: dynatraceApiToken || void 0,
6744
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6745
+ platformToken: dynatracePlatformToken || void 0
6746
+ }
6747
+ } : {},
6230
6748
  defaults: {
6231
6749
  jira: {
6232
6750
  project: jiraProject || void 0
@@ -6350,6 +6868,12 @@ var AdoGitClient = class {
6350
6868
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}?api-version=${API7}`
6351
6869
  );
6352
6870
  }
6871
+ async createRepo(collection, project, name) {
6872
+ return this.http.ado(
6873
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories?api-version=${API7}`,
6874
+ { method: "POST", body: { name } }
6875
+ );
6876
+ }
6353
6877
  // ── Pull Requests ─────────────────────────────────────────────────
6354
6878
  async listPRs(collection, project, repo, opts = {}) {
6355
6879
  return this.http.adoPaginate(async (token) => {
@@ -6604,14 +7128,34 @@ function parseFieldArgs2(fields) {
6604
7128
  }
6605
7129
  return result;
6606
7130
  }
7131
+ var stripSeparators = (s) => s.toLowerCase().replace(/[\s-]+/g, "");
7132
+ var DEFAULT_ADO_FIELD_ALIASES = Object.fromEntries(ADO_FIELD_ALIAS_CANDIDATES);
6607
7133
  function buildFieldPatch(fields, fieldAliases) {
7134
+ const aliases = { ...DEFAULT_ADO_FIELD_ALIASES, ...fieldAliases };
6608
7135
  return Object.entries(fields).map(([key, value]) => {
6609
- const refName = fieldAliases[key.toLowerCase()] ?? fieldAliases[key] ?? key;
6610
- return { op: "add", path: `/fields/${refName}`, value };
6611
- });
6612
- }
6613
- function fieldArgsToPatch(fieldArgs, fieldAliases) {
6614
- return buildFieldPatch(parseFieldArgs2(fieldArgs), fieldAliases);
7136
+ let refName = aliases[key.toLowerCase()] ?? aliases[key];
7137
+ if (!refName) {
7138
+ const target = stripSeparators(key);
7139
+ const match = Object.entries(aliases).find(([aliasKey]) => stripSeparators(aliasKey) === target);
7140
+ refName = match?.[1];
7141
+ }
7142
+ return { op: "add", path: `/fields/${refName ?? key}`, value };
7143
+ });
7144
+ }
7145
+ var BUILTIN_WORK_FIELD_KEYS = /* @__PURE__ */ new Set(["title", "description", "assignee", "assignedto", "priority"]);
7146
+ function splitWorkFieldsDictionary(fields) {
7147
+ const builtin = {};
7148
+ const custom = {};
7149
+ for (const [rawKey, rawValue] of Object.entries(fields)) {
7150
+ const value = resolveAtFileRef(rawValue);
7151
+ const key = rawKey.toLowerCase().replace(/\s+/g, "");
7152
+ if (BUILTIN_WORK_FIELD_KEYS.has(key)) {
7153
+ builtin[key === "assignedto" ? "assignee" : key] = value;
7154
+ } else {
7155
+ custom[rawKey] = value;
7156
+ }
7157
+ }
7158
+ return { builtin, custom };
6615
7159
  }
6616
7160
  var PR_VOTE = {
6617
7161
  APPROVE: 10,
@@ -6679,6 +7223,35 @@ function registerAdoProjectCommands(ado) {
6679
7223
  // src/services/ado/commands/work.ts
6680
7224
  import fs3 from "fs";
6681
7225
  import path6 from "path";
7226
+
7227
+ // src/services/ado/input-schema.ts
7228
+ var ADO_WORK_INPUT_FILE_SCHEMA = {
7229
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7230
+ title: "pncli Azure DevOps work item input file",
7231
+ description: "Shape accepted by --input-file on `pncli ado work create` / `work update`. CLI flags (--title, --description, --priority, --field, ...) override matching keys here; overridden keys are reported in meta.overrides.",
7232
+ type: "object",
7233
+ properties: {
7234
+ type: { type: "string", description: "Work item type, e.g. Bug, Task, User Story (create only)" },
7235
+ fields: {
7236
+ type: "object",
7237
+ description: 'Field dictionary. Built-in keys (case-insensitive): title, description, assignee, priority. Any other key (e.g. "Acceptance Criteria", or a raw reference name like "Microsoft.VSTS.Common.AcceptanceCriteria" or a custom "MyOrg.SomeField") resolves through the fieldAliases saved by `pncli ado work fields --save`, falling back to the key itself \u2014 custom fields never need to be pre-registered to be usable here. Any string value may be "@path/to/file" to read that field\'s content from a file instead of inlining it (useful for large rich-text fields like Description or Acceptance Criteria).',
7238
+ additionalProperties: true
7239
+ }
7240
+ },
7241
+ additionalProperties: false
7242
+ };
7243
+ var ADO_WORK_INPUT_FILE_EXAMPLE = {
7244
+ type: "Bug",
7245
+ fields: {
7246
+ Title: "Login broken on Safari",
7247
+ Description: "@desc.html",
7248
+ "Acceptance Criteria": "@ac.md",
7249
+ Priority: 2,
7250
+ "MyOrg.SomeCustomField": "x"
7251
+ }
7252
+ };
7253
+
7254
+ // src/services/ado/commands/work.ts
6682
7255
  function registerAdoWorkCommands(ado) {
6683
7256
  const work = ado.command("work").description("Azure DevOps work item operations");
6684
7257
  work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
@@ -6691,28 +7264,45 @@ function registerAdoWorkCommands(ado) {
6691
7264
  fail(err, "ado", "work-get", start);
6692
7265
  }
6693
7266
  });
6694
- work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--parent <id>", "Parent work item ID \u2014 creates a parent link after creation").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
7267
+ work.command("create").description("Create a work item").option("--type <type>", "Work item type (e.g. Bug, Task, User Story) (required, unless supplied via --input-file)").option("--title <title>", "Work item title (required, unless supplied via --input-file)").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--parent <id>", "Parent work item ID \u2014 creates a parent link after creation").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
6695
7268
  acc.push(v);
6696
7269
  return acc;
6697
- }, []).action(async (opts) => {
7270
+ }, []).option("--input-file <path>", "JSON file describing the work item ({ type, fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli ado work schema").action(async (opts) => {
6698
7271
  const start = Date.now();
6699
7272
  try {
6700
7273
  const globalOpts = ado.optsWithGlobals();
6701
7274
  const config = loadConfig({ configPath: globalOpts.config });
6702
7275
  const { collection, project, workClient } = getAdoContext(ado);
7276
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
7277
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitWorkFieldsDictionary(jsonInput.fields) : { builtin: {}, custom: {} };
7278
+ const type = opts.type ?? jsonInput?.type;
7279
+ if (!type) throw new PncliError('--type required (or "type" in --input-file)', 1);
7280
+ const flagBuiltin = {
7281
+ ...opts.title !== void 0 ? { title: opts.title } : {},
7282
+ ...opts.description !== void 0 ? { description: opts.description } : {},
7283
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
7284
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {}
7285
+ };
7286
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
7287
+ if (!builtinFields.title) throw new PncliError('--title required (or "fields.Title" in --input-file)', 1);
7288
+ const flagCustom = parseFieldArgs2(opts.field);
7289
+ const { merged: customFields, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
7290
+ const overrides = [...builtinOverrides, ...customOverrides];
7291
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
7292
+ const hasContent = (v) => v !== void 0 && v !== "";
6703
7293
  const builtIn = {
6704
- "System.Title": opts.title,
6705
- ...opts.description ? { "System.Description": opts.description } : {},
6706
- ...opts.assignee ? { "System.AssignedTo": opts.assignee } : {},
6707
- ...opts.priority ? { "Microsoft.VSTS.Common.Priority": opts.priority } : {}
7294
+ "System.Title": builtinFields.title,
7295
+ ...hasContent(builtinFields.description) ? { "System.Description": builtinFields.description } : {},
7296
+ ...hasContent(builtinFields.assignee) ? { "System.AssignedTo": builtinFields.assignee } : {},
7297
+ ...hasContent(builtinFields.priority) ? { "Microsoft.VSTS.Common.Priority": builtinFields.priority } : {}
6708
7298
  };
6709
- const extra = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
7299
+ const extra = buildFieldPatch(customFields, config.ado.fieldAliases);
6710
7300
  const patch = [
6711
7301
  ...Object.entries(builtIn).map(([k, v]) => ({ op: "add", path: `/fields/${k}`, value: v })),
6712
7302
  ...extra
6713
7303
  ];
6714
- const data = await workClient.createWorkItem(collection, project, opts.type, patch);
6715
- success(data, "ado", "work-create", start);
7304
+ const data = await workClient.createWorkItem(collection, project, type, patch);
7305
+ success(data, "ado", "work-create", start, overrides);
6716
7306
  if (opts.parent) {
6717
7307
  try {
6718
7308
  const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
@@ -6731,15 +7321,20 @@ function registerAdoWorkCommands(ado) {
6731
7321
  work.command("update").description("Update a work item field").requiredOption("--id <n>", "Work item ID").option("--field <name=value>", "Field to update (repeatable)", (v, acc) => {
6732
7322
  acc.push(v);
6733
7323
  return acc;
6734
- }, []).action(async (opts) => {
7324
+ }, []).option("--input-file <path>", "JSON file describing fields to update ({ fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli ado work schema").action(async (opts) => {
6735
7325
  const start = Date.now();
6736
7326
  try {
6737
7327
  const globalOpts = ado.optsWithGlobals();
6738
7328
  const config = loadConfig({ configPath: globalOpts.config });
6739
7329
  const { collection, workClient } = getAdoContext(ado);
6740
- const patch = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
7330
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
7331
+ const jsonFields = jsonInput?.fields ? Object.fromEntries(Object.entries(jsonInput.fields).map(([k, v]) => [k, resolveAtFileRef(v)])) : {};
7332
+ const flagFields = parseFieldArgs2(opts.field);
7333
+ const { merged, overrides } = mergeWithOverrides(jsonFields, flagFields);
7334
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
7335
+ const patch = buildFieldPatch(merged, config.ado.fieldAliases);
6741
7336
  const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
6742
- success(data, "ado", "work-update", start);
7337
+ success(data, "ado", "work-update", start, overrides);
6743
7338
  } catch (err) {
6744
7339
  fail(err, "ado", "work-update", start);
6745
7340
  }
@@ -6950,6 +7545,14 @@ function registerAdoWorkCommands(ado) {
6950
7545
  fail(err, "ado", "work-fields", start);
6951
7546
  }
6952
7547
  });
7548
+ work.command("schema").description("Print the --input-file JSON schema and an example for work create/update").option("--example-only", "Print only the runnable example JSON (no envelope) \u2014 pipeable straight into --input-file").action((opts) => {
7549
+ const start = Date.now();
7550
+ if (opts.exampleOnly) {
7551
+ writeRawOutput(JSON.stringify(ADO_WORK_INPUT_FILE_EXAMPLE, null, 2) + "\n");
7552
+ return;
7553
+ }
7554
+ success({ schema: ADO_WORK_INPUT_FILE_SCHEMA, example: ADO_WORK_INPUT_FILE_EXAMPLE }, "ado", "work-schema", start);
7555
+ });
6953
7556
  }
6954
7557
 
6955
7558
  // src/services/ado/commands/repo.ts
@@ -6978,6 +7581,16 @@ function registerAdoRepoCommands(ado) {
6978
7581
  fail(err, "ado", "repo-list", start);
6979
7582
  }
6980
7583
  });
7584
+ repo.command("create").description("Create a new git repository in the project").requiredOption("--name <name>", "Repository name").action(async (opts) => {
7585
+ const start = Date.now();
7586
+ try {
7587
+ const { collection, project, gitClient } = getAdoContext(ado);
7588
+ const data = await gitClient.createRepo(collection, project, opts.name);
7589
+ success(data, "ado", "repo-create", start);
7590
+ } catch (err) {
7591
+ fail(err, "ado", "repo-create", start);
7592
+ }
7593
+ });
6981
7594
  repo.command("get").description("Get a specific repository").action(async () => {
6982
7595
  const start = Date.now();
6983
7596
  try {
@@ -7219,11 +7832,11 @@ function registerAdoRepoCommands(ado) {
7219
7832
  const start = Date.now();
7220
7833
  try {
7221
7834
  const globalOpts = ado.optsWithGlobals();
7222
- const { loadConfig: lc } = await import("./config-S56LADVF.js");
7835
+ const { loadConfig: lc } = await import("./config-CPRXY5SN.js");
7223
7836
  const config = lc({ configPath: globalOpts.config });
7224
7837
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
7225
7838
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
7226
- const { createHttpClient: createHttpClient2 } = await import("./http-5WRBWEM6.js");
7839
+ const { createHttpClient: createHttpClient2 } = await import("./http-FNRE7NQU.js");
7227
7840
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
7228
7841
  const coreClient = new AdoCoreClient2(http);
7229
7842
  const me = await coreClient.getConnectionData(collection);
@@ -8037,24 +8650,35 @@ var CheckmarxClient = class {
8037
8650
  }
8038
8651
  http;
8039
8652
  async listProjects() {
8040
- return this.http.checkmarx("/cxrestapi/projects");
8653
+ const res = await this.http.checkmarx("projects", { params: { limit: 100 } });
8654
+ const projects = res.projects ?? [];
8655
+ if (res.filteredTotalCount > projects.length) {
8656
+ process.stderr.write(`warning: ${res.filteredTotalCount} projects found; only showing first ${projects.length}
8657
+ `);
8658
+ }
8659
+ return projects;
8041
8660
  }
8042
8661
  async getProject(id) {
8043
- return this.http.checkmarx(`/cxrestapi/projects/${id}`);
8662
+ return this.http.checkmarx(`projects/${id}`);
8044
8663
  }
8045
8664
  async listScans(opts = {}) {
8046
- return this.http.checkmarx("/cxrestapi/sast/scans", {
8047
- params: {
8048
- projectId: opts.projectId,
8049
- last: opts.last
8050
- }
8051
- });
8665
+ const params = { limit: opts.last ?? 100 };
8666
+ if (opts.projectId) params["project-id"] = opts.projectId;
8667
+ const res = await this.http.checkmarx("scans", { params });
8668
+ const scans = res.scans ?? [];
8669
+ if (res.filteredTotalCount > scans.length) {
8670
+ process.stderr.write(`warning: ${res.filteredTotalCount} scans found; only showing first ${scans.length}
8671
+ `);
8672
+ }
8673
+ return scans;
8052
8674
  }
8053
8675
  async getScan(id) {
8054
- return this.http.checkmarx(`/cxrestapi/sast/scans/${id}`);
8676
+ return this.http.checkmarx(`scans/${id}`);
8055
8677
  }
8056
8678
  async getScanResultsStatistics(scanId) {
8057
- return this.http.checkmarx(`/cxrestapi/sast/scans/${scanId}/resultsStatistics`);
8679
+ return this.http.checkmarx("results/summary", {
8680
+ params: { "scan-id": scanId }
8681
+ });
8058
8682
  }
8059
8683
  };
8060
8684
 
@@ -8067,7 +8691,7 @@ function getClient10(program2) {
8067
8691
  return new CheckmarxClient(http);
8068
8692
  }
8069
8693
  function registerCheckmarxCommands(program2) {
8070
- const cx = program2.command("checkmarx").description("Checkmarx CxSAST operations");
8694
+ const cx = program2.command("checkmarx").description("Checkmarx One operations");
8071
8695
  const project = cx.command("project").description("Project operations");
8072
8696
  const scan = cx.command("scan").description("Scan operations");
8073
8697
  project.command("list").description("List all projects").action(async () => {
@@ -8079,47 +8703,39 @@ function registerCheckmarxCommands(program2) {
8079
8703
  fail(err, "checkmarx", "project-list", start);
8080
8704
  }
8081
8705
  });
8082
- project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project ID").action(async (opts) => {
8706
+ project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project UUID").action(async (opts) => {
8083
8707
  const start = Date.now();
8084
8708
  try {
8085
- const id = parseInt(opts.id, 10);
8086
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8087
- const data = await getClient10(program2).getProject(id);
8709
+ const data = await getClient10(program2).getProject(opts.id);
8088
8710
  success(data, "checkmarx", "project-get", start);
8089
8711
  } catch (err) {
8090
8712
  fail(err, "checkmarx", "project-get", start);
8091
8713
  }
8092
8714
  });
8093
- scan.command("list").description("List scans").option("--project <id>", "Filter by project ID").option("--last <n>", "Return only the last N scans per project").action(async (opts) => {
8715
+ scan.command("list").description("List scans").option("--project <id>", "Filter by project UUID").option("--last <n>", "Return only the last N scans").action(async (opts) => {
8094
8716
  const start = Date.now();
8095
8717
  try {
8096
- const projectId = opts.project ? parseInt(opts.project, 10) : void 0;
8097
8718
  const last = opts.last ? parseInt(opts.last, 10) : void 0;
8098
- if (opts.project && isNaN(projectId)) throw new PncliError(`Invalid --project "${opts.project}". Expected an integer.`, 1);
8099
8719
  if (opts.last && isNaN(last)) throw new PncliError(`Invalid --last "${opts.last}". Expected an integer.`, 1);
8100
- const data = await getClient10(program2).listScans({ projectId, last });
8720
+ const data = await getClient10(program2).listScans({ projectId: opts.project, last });
8101
8721
  success(data, "checkmarx", "scan-list", start);
8102
8722
  } catch (err) {
8103
8723
  fail(err, "checkmarx", "scan-list", start);
8104
8724
  }
8105
8725
  });
8106
- scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8726
+ scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8107
8727
  const start = Date.now();
8108
8728
  try {
8109
- const id = parseInt(opts.id, 10);
8110
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8111
- const data = await getClient10(program2).getScan(id);
8729
+ const data = await getClient10(program2).getScan(opts.id);
8112
8730
  success(data, "checkmarx", "scan-get", start);
8113
8731
  } catch (err) {
8114
8732
  fail(err, "checkmarx", "scan-get", start);
8115
8733
  }
8116
8734
  });
8117
- scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8735
+ scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8118
8736
  const start = Date.now();
8119
8737
  try {
8120
- const id = parseInt(opts.id, 10);
8121
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8122
- const data = await getClient10(program2).getScanResultsStatistics(id);
8738
+ const data = await getClient10(program2).getScanResultsStatistics(opts.id);
8123
8739
  success(data, "checkmarx", "scan-stats", start);
8124
8740
  } catch (err) {
8125
8741
  fail(err, "checkmarx", "scan-stats", start);
@@ -8430,18 +9046,52 @@ function getBundledSkillsDir() {
8430
9046
  function getInstalledMetaPath(targetDir) {
8431
9047
  return path7.join(targetDir, ".pncli-installed.json");
8432
9048
  }
9049
+ var SKILL_ORIGIN_FILENAME = "pncli-origin.json";
8433
9050
  function readInstalledMeta(targetDir) {
8434
9051
  const metaPath = getInstalledMetaPath(targetDir);
8435
9052
  const raw = loadJsonFile(metaPath);
8436
9053
  if (raw && raw.version === 1 && raw.skills) return raw;
8437
9054
  return { version: 1, skills: {} };
8438
9055
  }
9056
+ function getSkillOriginPath(skillDir) {
9057
+ return path7.join(skillDir, SKILL_ORIGIN_FILENAME);
9058
+ }
9059
+ function readSkillOrigin(skillDir) {
9060
+ const originPath = getSkillOriginPath(skillDir);
9061
+ if (!fs4.existsSync(originPath)) return null;
9062
+ try {
9063
+ const raw = JSON.parse(fs4.readFileSync(originPath, "utf8"));
9064
+ if (raw && raw.version === 1 && raw.source) return raw;
9065
+ } catch {
9066
+ }
9067
+ return null;
9068
+ }
9069
+ function detectRepoBranch(repoPath) {
9070
+ try {
9071
+ const result = execFileSync3("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
9072
+ encoding: "utf8",
9073
+ stdio: ["pipe", "pipe", "pipe"]
9074
+ }).trim();
9075
+ return result && result !== "HEAD" ? result : void 0;
9076
+ } catch {
9077
+ return void 0;
9078
+ }
9079
+ }
8439
9080
  function recordInstalledSkills(targetDir, skillNames, record) {
8440
9081
  if (skillNames.length === 0) return;
8441
9082
  const meta = readInstalledMeta(targetDir);
8442
9083
  const now = (/* @__PURE__ */ new Date()).toISOString();
8443
9084
  for (const skillName of skillNames) {
8444
- meta.skills[skillName] = { ...record, installedAt: now };
9085
+ const fullRecord = { ...record, installedAt: now };
9086
+ meta.skills[skillName] = fullRecord;
9087
+ const skillDir = path7.join(targetDir, skillName);
9088
+ if (fs4.existsSync(skillDir)) {
9089
+ const origin = { version: 1, ...fullRecord };
9090
+ try {
9091
+ fs4.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
9092
+ } catch {
9093
+ }
9094
+ }
8445
9095
  }
8446
9096
  fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8447
9097
  }
@@ -8555,6 +9205,7 @@ function pullMarketplace(marketplacePath, repoUrl, token, marketplaceName) {
8555
9205
  function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl, pluginNames, targetDir) {
8556
9206
  const results = {};
8557
9207
  let totalInstalled = 0;
9208
+ const branch = detectRepoBranch(marketplacePath);
8558
9209
  for (const pluginName of pluginNames) {
8559
9210
  const skillsSrc = resolveSkillsSrc(marketplacePath, pluginName);
8560
9211
  if (!fs4.existsSync(skillsSrc)) {
@@ -8565,7 +9216,8 @@ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl,
8565
9216
  const { installed, failed } = copyPluginSkills(skillsSrc, targetDir, {
8566
9217
  marketplace: marketplaceName,
8567
9218
  plugin: pluginName,
8568
- installedFrom: repoUrl ?? ""
9219
+ installedFrom: repoUrl ?? "",
9220
+ branch
8569
9221
  });
8570
9222
  results[pluginName] = { installed, failed };
8571
9223
  totalInstalled += installed.length;
@@ -9003,6 +9655,121 @@ function registerSkillsCommands(program2) {
9003
9655
  fail(err, "skills", "marketplace-sync", start);
9004
9656
  }
9005
9657
  });
9658
+ 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) => {
9659
+ const start = Date.now();
9660
+ try {
9661
+ let targetDir;
9662
+ if (opts.target) {
9663
+ targetDir = path7.resolve(opts.target);
9664
+ } else {
9665
+ const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9666
+ const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
9667
+ targetDir = path7.resolve(scopePath);
9668
+ }
9669
+ const resolvedTarget = path7.resolve(targetDir);
9670
+ if (!fs4.existsSync(targetDir)) {
9671
+ success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
9672
+ return;
9673
+ }
9674
+ const meta = readInstalledMeta(targetDir);
9675
+ const removed = [];
9676
+ const skipped = [];
9677
+ const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
9678
+ if (name.startsWith(".")) return false;
9679
+ const p = path7.join(targetDir, name);
9680
+ try {
9681
+ return fs4.statSync(p).isDirectory();
9682
+ } catch {
9683
+ return false;
9684
+ }
9685
+ });
9686
+ for (const skillName of skillDirs) {
9687
+ const skillDir = path7.resolve(targetDir, skillName);
9688
+ if (!skillDir.startsWith(resolvedTarget + path7.sep)) continue;
9689
+ let record = meta.skills[skillName] ?? null;
9690
+ if (!record) {
9691
+ const perSkill = readSkillOrigin(skillDir);
9692
+ if (perSkill) {
9693
+ record = {
9694
+ source: perSkill.source,
9695
+ marketplace: perSkill.marketplace,
9696
+ plugin: perSkill.plugin,
9697
+ installedFrom: perSkill.installedFrom,
9698
+ branch: perSkill.branch,
9699
+ installedAt: perSkill.installedAt
9700
+ };
9701
+ }
9702
+ }
9703
+ if (!record || record.source !== "marketplace") {
9704
+ skipped.push(skillName);
9705
+ continue;
9706
+ }
9707
+ const matchesMarketplace = !opts.marketplace || record.marketplace === opts.marketplace || record.installedFrom === opts.marketplace;
9708
+ const matchesPlugin = plugin === "all" || record.plugin === plugin;
9709
+ if (!matchesMarketplace || !matchesPlugin) {
9710
+ skipped.push(skillName);
9711
+ continue;
9712
+ }
9713
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9714
+ delete meta.skills[skillName];
9715
+ removed.push(skillName);
9716
+ }
9717
+ if (removed.length > 0) {
9718
+ fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9719
+ }
9720
+ success({
9721
+ removed,
9722
+ skipped: skipped.length,
9723
+ total: removed.length,
9724
+ target: targetDir,
9725
+ plugin,
9726
+ ...opts.marketplace ? { marketplace: opts.marketplace } : {}
9727
+ }, "skills", "marketplace-purge-plugin", start);
9728
+ } catch (err) {
9729
+ fail(err, "skills", "marketplace-purge-plugin", start);
9730
+ }
9731
+ });
9732
+ 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) => {
9733
+ const start = Date.now();
9734
+ try {
9735
+ const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
9736
+ if (!fs4.existsSync(userDir)) {
9737
+ success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
9738
+ return;
9739
+ }
9740
+ const skillDirs = fs4.readdirSync(userDir).filter((name) => {
9741
+ if (name.startsWith(".")) return false;
9742
+ const p = path7.join(userDir, name);
9743
+ try {
9744
+ return fs4.statSync(p).isDirectory();
9745
+ } catch {
9746
+ return false;
9747
+ }
9748
+ });
9749
+ if (skillDirs.length === 0) {
9750
+ success({ removed: 0, target: userDir, message: "No skills found in user-level skills directory." }, "skills", "purge-user", start);
9751
+ return;
9752
+ }
9753
+ if (!opts.force) {
9754
+ throw new Error(
9755
+ `This will remove ${skillDirs.length} skill(s) from ${userDir}: ${skillDirs.join(", ")}.
9756
+ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\` to target a specific plugin.`
9757
+ );
9758
+ }
9759
+ for (const name of skillDirs) {
9760
+ const skillDir = path7.resolve(userDir, name);
9761
+ if (!skillDir.startsWith(path7.resolve(userDir) + path7.sep)) continue;
9762
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9763
+ }
9764
+ const metaPath = getInstalledMetaPath(userDir);
9765
+ if (fs4.existsSync(metaPath)) {
9766
+ fs4.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
9767
+ }
9768
+ success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
9769
+ } catch (err) {
9770
+ fail(err, "skills", "purge-user", start);
9771
+ }
9772
+ });
9006
9773
  }
9007
9774
  function resolvePluginChoices(marketplacePath) {
9008
9775
  const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
@@ -9052,7 +9819,8 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
9052
9819
  source: "marketplace",
9053
9820
  marketplace: meta.marketplace,
9054
9821
  plugin: meta.plugin,
9055
- installedFrom: meta.installedFrom
9822
+ installedFrom: meta.installedFrom,
9823
+ ...meta.branch ? { branch: meta.branch } : {}
9056
9824
  });
9057
9825
  }
9058
9826
  return { installed, failed };
@@ -9314,6 +10082,170 @@ function registerOpenShiftCommands(program2) {
9314
10082
  });
9315
10083
  }
9316
10084
 
10085
+ // src/services/dynatrace/commands.ts
10086
+ function getHttp3(program2) {
10087
+ const opts = program2.optsWithGlobals();
10088
+ return createHttpClient(
10089
+ loadConfig({ configPath: opts.config }),
10090
+ Boolean(opts.dryRun)
10091
+ );
10092
+ }
10093
+ async function allPages(fetchPage) {
10094
+ const items = [];
10095
+ let nextPageKey;
10096
+ do {
10097
+ const page = await fetchPage(nextPageKey);
10098
+ items.push(...page.items);
10099
+ nextPageKey = page.nextPageKey;
10100
+ } while (nextPageKey);
10101
+ return { items, totalCount: items.length };
10102
+ }
10103
+ async function listEntities(http, entitySelector, opts) {
10104
+ return allPages(async (nextPageKey) => {
10105
+ const page = await http.dynatrace("/api/v2/entities", {
10106
+ params: nextPageKey ? { nextPageKey } : {
10107
+ entitySelector,
10108
+ from: opts.from,
10109
+ to: opts.to,
10110
+ fields: opts.fields,
10111
+ pageSize: 500
10112
+ }
10113
+ });
10114
+ return {
10115
+ items: page.entities,
10116
+ nextPageKey: page.nextPageKey,
10117
+ totalCount: page.totalCount
10118
+ };
10119
+ });
10120
+ }
10121
+ async function runTraceQuery(http, traceId) {
10122
+ if (!/^[0-9a-f]{16,32}$/i.test(traceId)) {
10123
+ throw new PncliError("Trace ID must be a 16- or 32-character hexadecimal value");
10124
+ }
10125
+ const deadline = Date.now() + 6e4;
10126
+ let response = await http.dynatracePlatform(
10127
+ "/platform/storage/query/v1/query:execute",
10128
+ {
10129
+ method: "POST",
10130
+ body: {
10131
+ query: `fetch spans | filter trace.id == "${traceId.toLowerCase()}" | sort start_time asc`,
10132
+ requestTimeoutMilliseconds: 5e3
10133
+ },
10134
+ timeoutMs: 1e4
10135
+ }
10136
+ );
10137
+ while (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10138
+ if (!response.requestToken) throw new PncliError("Dynatrace did not return a query request token");
10139
+ const remainingMs = deadline - Date.now();
10140
+ if (remainingMs <= 0) throw new PncliError("Dynatrace trace query did not finish within 60 seconds");
10141
+ const pollStarted = Date.now();
10142
+ response = await http.dynatracePlatform(
10143
+ "/platform/storage/query/v1/query:poll",
10144
+ {
10145
+ params: {
10146
+ "request-token": response.requestToken,
10147
+ "request-timeout": Math.min(5e3, remainingMs)
10148
+ },
10149
+ timeoutMs: 1e4
10150
+ }
10151
+ );
10152
+ if (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10153
+ const delayMs = Math.min(1e3 - (Date.now() - pollStarted), deadline - Date.now());
10154
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
10155
+ }
10156
+ }
10157
+ if (response.state !== "SUCCEEDED") {
10158
+ throw new PncliError(response.error?.message ?? `Dynatrace trace query ${response.state.toLowerCase()}`);
10159
+ }
10160
+ return response.result;
10161
+ }
10162
+ function registerDynatraceCommands(program2) {
10163
+ const dynatrace = program2.command("dynatrace").description("Dynatrace observability operations");
10164
+ const entities = dynatrace.command("entities").description("Monitored entity operations");
10165
+ 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) => {
10166
+ const start = Date.now();
10167
+ try {
10168
+ const data = await listEntities(getHttp3(program2), opts.selector, opts);
10169
+ success({ totalCount: data.totalCount, entities: data.items }, "dynatrace", "entities list", start);
10170
+ } catch (err) {
10171
+ fail(err, "dynatrace", "entities list", start);
10172
+ }
10173
+ });
10174
+ 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) => {
10175
+ const start = Date.now();
10176
+ try {
10177
+ const data = await getHttp3(program2).dynatrace(
10178
+ `/api/v2/entities/${encodeURIComponent(opts.id)}`,
10179
+ { params: { from: opts.from, to: opts.to, fields: opts.fields } }
10180
+ );
10181
+ success(data, "dynatrace", "entities get", start);
10182
+ } catch (err) {
10183
+ fail(err, "dynatrace", "entities get", start);
10184
+ }
10185
+ });
10186
+ dynatrace.command("services").description("List monitored services").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10187
+ const start = Date.now();
10188
+ try {
10189
+ const data = await listEntities(getHttp3(program2), 'type("SERVICE")', opts);
10190
+ success({ totalCount: data.totalCount, services: data.items }, "dynatrace", "services", start);
10191
+ } catch (err) {
10192
+ fail(err, "dynatrace", "services", start);
10193
+ }
10194
+ });
10195
+ dynatrace.command("workloads").description("List monitored Kubernetes workloads").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10196
+ const start = Date.now();
10197
+ try {
10198
+ const selector = 'type("CLOUD_APPLICATION")';
10199
+ const data = await listEntities(getHttp3(program2), selector, opts);
10200
+ success({ totalCount: data.totalCount, workloads: data.items }, "dynatrace", "workloads", start);
10201
+ } catch (err) {
10202
+ fail(err, "dynatrace", "workloads", start);
10203
+ }
10204
+ });
10205
+ const problems = dynatrace.command("problems").description("Problem operations");
10206
+ 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) => {
10207
+ const start = Date.now();
10208
+ try {
10209
+ const data = await allPages(async (nextPageKey) => {
10210
+ const page = await getHttp3(program2).dynatrace("/api/v2/problems", {
10211
+ params: nextPageKey ? { nextPageKey } : {
10212
+ from: opts.from ?? "now-2h",
10213
+ to: opts.to ?? "now",
10214
+ problemSelector: opts.problemSelector,
10215
+ entitySelector: opts.entitySelector,
10216
+ pageSize: 500
10217
+ }
10218
+ });
10219
+ return { items: page.problems, nextPageKey: page.nextPageKey, totalCount: page.totalCount };
10220
+ });
10221
+ success({ totalCount: data.totalCount, problems: data.items }, "dynatrace", "problems list", start);
10222
+ } catch (err) {
10223
+ fail(err, "dynatrace", "problems list", start);
10224
+ }
10225
+ });
10226
+ problems.command("get").description("Get a problem by ID").requiredOption("--id <id>", "Problem ID").option("--fields <fields>", "Additional fields").action(async (opts) => {
10227
+ const start = Date.now();
10228
+ try {
10229
+ const data = await getHttp3(program2).dynatrace(
10230
+ `/api/v2/problems/${encodeURIComponent(opts.id)}`,
10231
+ { params: { fields: opts.fields } }
10232
+ );
10233
+ success(data, "dynatrace", "problems get", start);
10234
+ } catch (err) {
10235
+ fail(err, "dynatrace", "problems get", start);
10236
+ }
10237
+ });
10238
+ dynatrace.command("trace").description("Get distributed-trace spans from Grail").requiredOption("--id <trace-id>", "16- or 32-character hexadecimal trace ID").action(async (opts) => {
10239
+ const start = Date.now();
10240
+ try {
10241
+ const result = await runTraceQuery(getHttp3(program2), opts.id);
10242
+ success(result, "dynatrace", "trace", start);
10243
+ } catch (err) {
10244
+ fail(err, "dynatrace", "trace", start);
10245
+ }
10246
+ });
10247
+ }
10248
+
9317
10249
  // src/cli.ts
9318
10250
  if (!process.env.PNCLI_VERIFY_TLS) {
9319
10251
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -9357,6 +10289,7 @@ registerSonatypeIqCommands(program);
9357
10289
  registerSkillsCommands(program);
9358
10290
  registerJwtCommands(program);
9359
10291
  registerOpenShiftCommands(program);
10292
+ registerDynatraceCommands(program);
9360
10293
  program.addHelpText("after", `
9361
10294
  Services:
9362
10295
  git Local git operations (status, diff, log, branch)
@@ -9371,11 +10304,12 @@ Services:
9371
10304
  jenkins Jenkins Data Center (jobs, builds, logs)
9372
10305
  artifactory Artifactory (repos, artifact search, build info, properties)
9373
10306
  udeploy IBM UrbanCode Deploy (component versions, deployment processes)
9374
- checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
10307
+ checkmarx Checkmarx One (projects, scans, scan statistics)
9375
10308
  servicenow ServiceNow (change requests)
9376
10309
  contrast Contrast IAST (applications, vulnerability findings)
9377
10310
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
9378
10311
  openshift OpenShift / Kubernetes (pods, events, logs, metrics)
10312
+ dynatrace Dynatrace (services, entities, problems, traces, Kubernetes workloads)
9379
10313
  config Manage pncli configuration
9380
10314
  skills Download and manage Claude Code skills
9381
10315
  jwt JWT token utilities (decode header and payload)