@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/README.md +5 -3
- package/copilot-instructions.md +44 -5
- package/dist/{chunk-FFVQ4PA7.js → chunk-FXY262BR.js} +38 -8
- package/dist/chunk-FXY262BR.js.map +1 -0
- package/dist/{chunk-XPPEXJBP.js → chunk-WM5NYVEO.js} +112 -24
- package/dist/chunk-WM5NYVEO.js.map +1 -0
- package/dist/cli.js +1065 -131
- package/dist/cli.js.map +1 -1
- package/dist/{config-S56LADVF.js → config-CPRXY5SN.js} +2 -2
- package/dist/{http-5WRBWEM6.js → http-FNRE7NQU.js} +2 -2
- package/package.json +1 -1
- package/skills/pncli/SKILL.md +7 -2
- package/skills/pncli/ado.md +19 -1
- package/skills/pncli/bitbucket.md +11 -1
- package/skills/pncli/checkmarx.md +20 -13
- package/skills/pncli/confluence.md +5 -1
- package/skills/pncli/dynatrace.md +46 -0
- package/skills/pncli/github.md +11 -1
- package/skills/pncli/jira.md +11 -0
- package/dist/chunk-FFVQ4PA7.js.map +0 -1
- package/dist/chunk-XPPEXJBP.js.map +0 -1
- /package/dist/{config-S56LADVF.js.map → config-CPRXY5SN.js.map} +0 -0
- /package/dist/{http-5WRBWEM6.js.map → http-FNRE7NQU.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
setRepoConfigValue,
|
|
9
9
|
writeGlobalConfig,
|
|
10
10
|
writeRepoConfig
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, ...)").
|
|
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
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
|
1265
|
-
const
|
|
1266
|
-
|
|
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
|
|
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
|
|
1281
|
-
|
|
1282
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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").
|
|
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:
|
|
2802
|
+
body: bodyContent,
|
|
2484
2803
|
parentId: opts.parentId,
|
|
2485
|
-
representation
|
|
2804
|
+
representation
|
|
2486
2805
|
});
|
|
2487
2806
|
success(data, "confluence", "create-page", start);
|
|
2488
2807
|
} catch (err) {
|
|
2489
|
-
|
|
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:
|
|
2834
|
+
body: bodyContent,
|
|
2502
2835
|
status: opts.status,
|
|
2503
|
-
representation
|
|
2836
|
+
representation
|
|
2504
2837
|
});
|
|
2505
2838
|
success(data, "confluence", "update-page", start);
|
|
2506
2839
|
} catch (err) {
|
|
2507
|
-
|
|
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
|
|
4913
|
-
import { basename as
|
|
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 =
|
|
5029
|
-
const fileName =
|
|
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
|
|
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.
|
|
5642
|
+
if (cfg.checkmarx.baseUrl && cfg.checkmarx.tenantName && (cfg.checkmarx.apiKey || cfg.checkmarx.clientId && cfg.checkmarx.clientSecret)) {
|
|
5269
5643
|
try {
|
|
5270
|
-
await http.checkmarx("/
|
|
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.
|
|
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("/
|
|
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
|
|
6319
|
+
message: "Configure Checkmarx One for vulnerability scanning?",
|
|
5878
6320
|
default: false
|
|
5879
6321
|
});
|
|
5880
6322
|
let checkmarxBaseUrl = "";
|
|
5881
|
-
let
|
|
5882
|
-
let
|
|
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.
|
|
6329
|
+
message: "Checkmarx One API base URL (e.g. https://ast.checkmarx.net):",
|
|
5886
6330
|
default: ""
|
|
5887
6331
|
});
|
|
5888
|
-
|
|
5889
|
-
message: "Checkmarx
|
|
6332
|
+
checkmarxTenantName = await input({
|
|
6333
|
+
message: "Checkmarx One tenant name (IAM realm, e.g. mycompany):",
|
|
5890
6334
|
default: ""
|
|
5891
6335
|
});
|
|
5892
|
-
|
|
5893
|
-
message: "Checkmarx
|
|
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 (
|
|
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
|
-
|
|
5904
|
-
|
|
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("/
|
|
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
|
-
|
|
6197
|
-
|
|
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
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
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").
|
|
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":
|
|
6705
|
-
...
|
|
6706
|
-
...
|
|
6707
|
-
...
|
|
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 =
|
|
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,
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
8662
|
+
return this.http.checkmarx(`projects/${id}`);
|
|
8044
8663
|
}
|
|
8045
8664
|
async listScans(opts = {}) {
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
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(
|
|
8676
|
+
return this.http.checkmarx(`scans/${id}`);
|
|
8055
8677
|
}
|
|
8056
8678
|
async getScanResultsStatistics(scanId) {
|
|
8057
|
-
return this.http.checkmarx(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|