@kolatts/pncli 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  setRepoConfigValue,
9
9
  writeGlobalConfig,
10
10
  writeRepoConfig
11
- } from "./chunk-FFVQ4PA7.js";
11
+ } from "./chunk-IVUDXPDP.js";
12
12
  import {
13
13
  ExitCode,
14
14
  PncliError,
@@ -20,7 +20,7 @@ import {
20
20
  success,
21
21
  warn,
22
22
  writeRawOutput
23
- } from "./chunk-XPPEXJBP.js";
23
+ } from "./chunk-WX4ZRHIC.js";
24
24
  import {
25
25
  AdoCoreClient
26
26
  } from "./chunk-VDL32PSG.js";
@@ -734,6 +734,19 @@ var BitbucketClient = class {
734
734
  );
735
735
  return result.values ?? [];
736
736
  }
737
+ async createRepo(opts) {
738
+ return this.http.bitbucket(
739
+ `${API}/projects/${opts.project}/repos`,
740
+ {
741
+ method: "POST",
742
+ body: {
743
+ name: opts.name,
744
+ scmId: "git",
745
+ ...opts.description !== void 0 ? { description: opts.description } : {}
746
+ }
747
+ }
748
+ );
749
+ }
737
750
  };
738
751
 
739
752
  // src/services/git/commands.ts
@@ -837,7 +850,7 @@ function registerGitCommands(program2) {
837
850
  }
838
851
 
839
852
  // src/services/jira/commands.ts
840
- import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
853
+ import { readFileSync as readFileSync3, existsSync, writeFileSync, mkdirSync } from "fs";
841
854
  import path from "path";
842
855
 
843
856
  // src/services/jira/client.ts
@@ -911,7 +924,7 @@ var JiraClient = class {
911
924
  if (opts.description) fields.description = opts.description;
912
925
  if (opts.priority) fields.priority = { name: opts.priority };
913
926
  if (opts.assignee) fields.assignee = { name: opts.assignee };
914
- if (opts.labels) fields.labels = opts.labels;
927
+ if (opts.labels?.length) fields.labels = opts.labels;
915
928
  Object.assign(fields, opts.customFieldValues ?? {});
916
929
  await this.http.jira(`${API2}/issue/${key}`, {
917
930
  method: "PUT",
@@ -1207,7 +1220,119 @@ function formatFieldValue(value, type) {
1207
1220
  }
1208
1221
  }
1209
1222
 
1223
+ // src/services/jira/input-schema.ts
1224
+ var JIRA_INPUT_FILE_SCHEMA = {
1225
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1226
+ title: "pncli Jira issue input file",
1227
+ description: "Shape accepted by --input-file on `pncli jira create-issue` / `update-issue`. CLI flags (--summary, --description, --priority, --field, ...) override matching keys here; overridden keys are reported in meta.overrides.",
1228
+ type: "object",
1229
+ properties: {
1230
+ project: { type: "string", description: "Project key (create-issue only)" },
1231
+ issueType: { type: "string", description: "Issue type name, e.g. Bug, Story, Task (create-issue only)" },
1232
+ fields: {
1233
+ type: "object",
1234
+ description: 'Field dictionary. Built-in keys: summary, description, priority, assignee, labels, parent. Any other key resolves as a custom field: a friendly name registered via `pncli jira fields`, or a raw field id (e.g. customfield_10032) passed through untouched \u2014 custom fields never need to be pre-registered in config. Any string value may be "@path/to/file" to read that field\'s content from a file instead of inlining it (useful for large rich-text fields).',
1235
+ additionalProperties: true
1236
+ }
1237
+ },
1238
+ additionalProperties: false
1239
+ };
1240
+ var JIRA_INPUT_FILE_EXAMPLE = {
1241
+ project: "PROJ",
1242
+ issueType: "Bug",
1243
+ fields: {
1244
+ summary: "Login broken on Safari",
1245
+ description: "@desc.html",
1246
+ priority: "High",
1247
+ labels: ["frontend", "urgent"],
1248
+ // Raw field id — passed through untouched, no pre-registration needed. A friendly
1249
+ // custom-field name (e.g. "Story Points") also works, but only once it's registered
1250
+ // via `pncli config set jira.customFields` / discovered with `pncli jira fields`.
1251
+ customfield_10032: "@ac.md"
1252
+ }
1253
+ };
1254
+
1255
+ // src/lib/input.ts
1256
+ import { readFileSync as readFileSync2 } from "fs";
1257
+ function resolveTextInput(inline, file, optionName) {
1258
+ if (inline !== void 0 && file !== void 0) {
1259
+ throw new PncliError(`Cannot specify both --${optionName} and --${optionName}-file`, 1);
1260
+ }
1261
+ if (file !== void 0) {
1262
+ try {
1263
+ return readFileSync2(file === "-" ? 0 : file, "utf8");
1264
+ } catch (e) {
1265
+ const source = file === "-" ? "stdin" : `file "${file}"`;
1266
+ throw new PncliError(`Cannot read ${optionName} ${source}: ${e.message}`, 1);
1267
+ }
1268
+ }
1269
+ return inline;
1270
+ }
1271
+ function readJsonInputFile(filePath, optionName = "input-file") {
1272
+ let raw;
1273
+ try {
1274
+ raw = readFileSync2(filePath === "-" ? 0 : filePath, "utf8");
1275
+ } catch (e) {
1276
+ const source = filePath === "-" ? "stdin" : `file "${filePath}"`;
1277
+ throw new PncliError(`Cannot read --${optionName} ${source}: ${e.message}`, 1);
1278
+ }
1279
+ try {
1280
+ return JSON.parse(raw);
1281
+ } catch (e) {
1282
+ throw new PncliError(`--${optionName} must contain valid JSON: ${e.message}`, 1);
1283
+ }
1284
+ }
1285
+ function resolveAtFileRef(value) {
1286
+ if (typeof value !== "string" || !value.startsWith("@")) return value;
1287
+ const filePath = value.slice(1);
1288
+ try {
1289
+ return readFileSync2(filePath, "utf8");
1290
+ } catch (e) {
1291
+ throw new PncliError(`Cannot read referenced file "${filePath}": ${e.message}`, 1);
1292
+ }
1293
+ }
1294
+ function mergeWithOverrides(fromJson, fromFlags) {
1295
+ const merged = { ...fromJson };
1296
+ const overrides = [];
1297
+ for (const [key, value] of Object.entries(fromFlags)) {
1298
+ if (value === void 0) continue;
1299
+ if (Object.prototype.hasOwnProperty.call(fromJson, key)) overrides.push(key);
1300
+ merged[key] = value;
1301
+ }
1302
+ return { merged, overrides };
1303
+ }
1304
+
1210
1305
  // src/services/jira/commands.ts
1306
+ var BUILTIN_ISSUE_FIELDS = /* @__PURE__ */ new Set(["summary", "description", "priority", "assignee", "labels", "parent"]);
1307
+ function splitFieldsDictionary(fields, fieldMap) {
1308
+ const builtin = {};
1309
+ const custom = {};
1310
+ for (const [rawKey, rawValue] of Object.entries(fields)) {
1311
+ const value = resolveAtFileRef(rawValue);
1312
+ const key = rawKey.toLowerCase();
1313
+ if (BUILTIN_ISSUE_FIELDS.has(key)) {
1314
+ builtin[key] = value;
1315
+ continue;
1316
+ }
1317
+ const def = fieldMap.byName.get(key) ?? fieldMap.byId.get(rawKey);
1318
+ if (def) {
1319
+ custom[def.id] = value;
1320
+ } else if (!/\s/.test(rawKey)) {
1321
+ custom[rawKey] = value;
1322
+ } else {
1323
+ throw new PncliError(
1324
+ `Unknown custom field: "${rawKey}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1325
+ 1
1326
+ );
1327
+ }
1328
+ }
1329
+ return { builtin, custom };
1330
+ }
1331
+ function normalizeLabels(value) {
1332
+ if (value === void 0) return void 0;
1333
+ if (Array.isArray(value)) return value.map((v) => String(v).trim());
1334
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
1335
+ }
1211
1336
  function getClient(program2) {
1212
1337
  const opts = program2.optsWithGlobals();
1213
1338
  const config = loadConfig({ configPath: opts.config });
@@ -1247,7 +1372,7 @@ function registerJiraCommands(program2) {
1247
1372
  fail(err, "jira", "get-issue", start);
1248
1373
  }
1249
1374
  });
1250
- jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", "Parent issue key \u2014 sets fields.parent in the create payload").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
1375
+ jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").option("--summary <text>", "Issue summary (required, unless supplied via --input-file)").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", "Parent issue key \u2014 sets fields.parent in the create payload").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").option("--input-file <path>", "JSON file describing the whole issue ({ project, issueType, fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli jira schema").action(async (opts) => {
1251
1376
  const start = Date.now();
1252
1377
  let fieldMap = buildFieldMap([]);
1253
1378
  try {
@@ -1255,35 +1380,87 @@ function registerJiraCommands(program2) {
1255
1380
  const client = resolved.client;
1256
1381
  fieldMap = resolved.fieldMap;
1257
1382
  const defaults = getDefaults(program2);
1258
- const project = opts.project ?? defaults.project;
1259
- const issueType = opts.type ?? defaults.issueType ?? "Task";
1260
- const priority = opts.priority ?? defaults.priority;
1261
- if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
1262
- const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
1383
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
1384
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitFieldsDictionary(jsonInput.fields, fieldMap) : { builtin: {}, custom: {} };
1385
+ const project = opts.project ?? jsonInput?.project ?? defaults.project;
1386
+ const issueType = opts.type ?? jsonInput?.issueType ?? defaults.issueType ?? "Task";
1387
+ if (!project) throw new PncliError('--project required (or set defaults.jira.project in config, or "project" in --input-file)', 1);
1388
+ const flagBuiltin = {
1389
+ ...opts.summary !== void 0 ? { summary: opts.summary } : {},
1390
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1391
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {},
1392
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
1393
+ ...opts.labels !== void 0 ? { labels: opts.labels } : {},
1394
+ ...opts.parent !== void 0 ? { parent: opts.parent } : {}
1395
+ };
1396
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
1397
+ const summary = builtinFields.summary;
1398
+ if (!summary) throw new PncliError('Must specify --summary or "fields.summary" in --input-file', 1);
1399
+ const priority = builtinFields.priority ?? defaults.priority;
1263
1400
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1264
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1265
- const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, parent: opts.parent, customFieldValues });
1266
- success(data, "jira", "create-issue", start);
1401
+ const flagCustom = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1402
+ const { merged: customFieldValues, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
1403
+ const overrides = [...builtinOverrides, ...customOverrides];
1404
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
1405
+ const data = await client.createIssue({
1406
+ project,
1407
+ issueType,
1408
+ summary,
1409
+ description: builtinFields.description,
1410
+ priority,
1411
+ assignee: builtinFields.assignee,
1412
+ labels: normalizeLabels(builtinFields.labels),
1413
+ parent: builtinFields.parent,
1414
+ customFieldValues
1415
+ });
1416
+ success(data, "jira", "create-issue", start, overrides);
1267
1417
  } catch (err) {
1268
1418
  fail(translateFieldErrors(err, fieldMap), "jira", "create-issue", start);
1269
1419
  }
1270
1420
  });
1271
- jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
1421
+ jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").option("--input-file <path>", "JSON file describing fields to update ({ fields: {...} }); '-' = stdin. CLI flags override matching keys. See: pncli jira schema").action(async (opts) => {
1272
1422
  const start = Date.now();
1273
1423
  let fieldMap = buildFieldMap([]);
1274
1424
  try {
1275
1425
  const resolved = getClientAndFields(program2);
1276
1426
  const client = resolved.client;
1277
1427
  fieldMap = resolved.fieldMap;
1278
- const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
1428
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
1429
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitFieldsDictionary(jsonInput.fields, fieldMap) : { builtin: {}, custom: {} };
1430
+ const flagBuiltin = {
1431
+ ...opts.summary !== void 0 ? { summary: opts.summary } : {},
1432
+ ...opts.description !== void 0 ? { description: opts.description } : {},
1433
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {},
1434
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
1435
+ ...opts.labels !== void 0 ? { labels: opts.labels } : {}
1436
+ };
1437
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
1279
1438
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1280
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1281
- await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
1282
- success({ updated: opts.key }, "jira", "update-issue", start);
1439
+ const flagCustom = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1440
+ const { merged: customFieldValues, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
1441
+ const overrides = [...builtinOverrides, ...customOverrides];
1442
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
1443
+ await client.updateIssue(opts.key, {
1444
+ summary: builtinFields.summary,
1445
+ description: builtinFields.description,
1446
+ priority: builtinFields.priority,
1447
+ assignee: builtinFields.assignee,
1448
+ labels: normalizeLabels(builtinFields.labels),
1449
+ customFieldValues
1450
+ });
1451
+ success({ updated: opts.key }, "jira", "update-issue", start, overrides);
1283
1452
  } catch (err) {
1284
1453
  fail(translateFieldErrors(err, fieldMap), "jira", "update-issue", start);
1285
1454
  }
1286
1455
  });
1456
+ jira.command("schema").description("Print the --input-file JSON schema and an example for create-issue/update-issue").option("--example-only", "Print only the runnable example JSON (no envelope) \u2014 pipeable straight into --input-file").action((opts) => {
1457
+ const start = Date.now();
1458
+ if (opts.exampleOnly) {
1459
+ writeRawOutput(JSON.stringify(JIRA_INPUT_FILE_EXAMPLE, null, 2) + "\n");
1460
+ return;
1461
+ }
1462
+ success({ schema: JIRA_INPUT_FILE_SCHEMA, example: JIRA_INPUT_FILE_EXAMPLE }, "jira", "schema", start);
1463
+ });
1287
1464
  jira.command("transition-issue").description("Transition a Jira issue to a new status").requiredOption("--key <issue-key>", "Issue key").requiredOption("--transition <name-or-id>", "Transition name or ID").action(async (opts) => {
1288
1465
  const start = Date.now();
1289
1466
  try {
@@ -1492,7 +1669,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1492
1669
  const filePath = value.slice(1);
1493
1670
  let raw;
1494
1671
  try {
1495
- raw = readFileSync2(filePath, "utf8").trim();
1672
+ raw = readFileSync3(filePath, "utf8").trim();
1496
1673
  } catch (e) {
1497
1674
  throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1498
1675
  }
@@ -1510,7 +1687,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1510
1687
  function parseFieldsFile(filePath, fieldMap) {
1511
1688
  let raw;
1512
1689
  try {
1513
- raw = readFileSync2(filePath, "utf8");
1690
+ raw = readFileSync3(filePath, "utf8");
1514
1691
  } catch (e) {
1515
1692
  throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
1516
1693
  }
@@ -1563,6 +1740,24 @@ function getClient2(program2, overrides) {
1563
1740
  }
1564
1741
  function registerBitbucketCommands(program2) {
1565
1742
  const bb = program2.command("bitbucket").description("Bitbucket Server operations").option("--project <key>", "Bitbucket project key").option("--repo <slug>", "Bitbucket repository slug");
1743
+ bb.command("create-repo").description("Create a new repository in a project").requiredOption("--name <name>", "Repository name (slug)").option("--description <desc>", "Repository description").option("--project <key>", "Bitbucket project key (overrides parent --project)").action(async (opts) => {
1744
+ const start = Date.now();
1745
+ try {
1746
+ const globalOpts = bb.optsWithGlobals();
1747
+ const config = loadConfig({ configPath: globalOpts.config });
1748
+ const http = createHttpClient(config, Boolean(globalOpts.dryRun));
1749
+ const client = new BitbucketClient(http);
1750
+ const ctx = getGitContext(config);
1751
+ const project = opts.project ?? globalOpts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
1752
+ if (!project) {
1753
+ throw new PncliError("Could not determine Bitbucket project. Pass --project, or run pncli config init.", 1);
1754
+ }
1755
+ const data = await client.createRepo({ project, name: opts.name, description: opts.description });
1756
+ success(data, "bitbucket", "create-repo", start);
1757
+ } catch (err) {
1758
+ fail(err, "bitbucket", "create-repo", start);
1759
+ }
1760
+ });
1566
1761
  bb.command("list-prs").description("List pull requests").option("--state <state>", "PR state: OPEN|MERGED|DECLINED|ALL", "OPEN").option("--author <username>", "Filter by author username").option("--reviewer <username>", "Filter by reviewer username").action(async (opts) => {
1567
1762
  const start = Date.now();
1568
1763
  try {
@@ -1965,6 +2160,32 @@ var GitHubClient = class {
1965
2160
  }
1966
2161
  return results;
1967
2162
  }
2163
+ async createIssue(opts) {
2164
+ return this.http.github(
2165
+ `/repos/${opts.owner}/${opts.repo}/issues`,
2166
+ {
2167
+ method: "POST",
2168
+ body: {
2169
+ title: opts.title,
2170
+ ...opts.body !== void 0 ? { body: opts.body } : {},
2171
+ ...opts.labels?.length ? { labels: opts.labels } : {},
2172
+ ...opts.assignees?.length ? { assignees: opts.assignees } : {}
2173
+ }
2174
+ }
2175
+ );
2176
+ }
2177
+ async createRepo(opts) {
2178
+ const path8 = opts.org ? `/orgs/${opts.org}/repos` : "/user/repos";
2179
+ return this.http.github(path8, {
2180
+ method: "POST",
2181
+ body: {
2182
+ name: opts.name,
2183
+ ...opts.description !== void 0 ? { description: opts.description } : {},
2184
+ ...opts.private !== void 0 ? { private: opts.private } : {},
2185
+ ...opts.autoInit !== void 0 ? { auto_init: opts.autoInit } : {}
2186
+ }
2187
+ });
2188
+ }
1968
2189
  };
1969
2190
 
1970
2191
  // src/services/github/commands.ts
@@ -1986,6 +2207,50 @@ function getClient3(program2, overrides) {
1986
2207
  }
1987
2208
  function registerGitHubCommands(program2) {
1988
2209
  const gh = program2.command("github").description("GitHub repository operations").option("--owner <owner>", "GitHub owner (user or org)").option("--repo <repo>", "GitHub repository name");
2210
+ gh.command("create-repo").description("Create a new repository (personal or org)").requiredOption("--name <name>", "Repository name").option("--description <desc>", "Repository description").option("--private", "Create as a private repository").option("--auto-init", "Initialize with a README").action(async (opts) => {
2211
+ const start = Date.now();
2212
+ try {
2213
+ const globalOpts = gh.optsWithGlobals();
2214
+ const config = loadConfig({ configPath: globalOpts.config });
2215
+ const http = createHttpClient(config, Boolean(globalOpts.dryRun));
2216
+ const client = new GitHubClient(http);
2217
+ const owner = globalOpts.owner ?? "";
2218
+ const data = await client.createRepo({
2219
+ org: owner || void 0,
2220
+ name: opts.name,
2221
+ description: opts.description,
2222
+ private: opts.private,
2223
+ autoInit: opts.autoInit
2224
+ });
2225
+ success(data, "github", "create-repo", start);
2226
+ } catch (err) {
2227
+ fail(err, "github", "create-repo", start);
2228
+ }
2229
+ });
2230
+ gh.command("create-issue").description("Create a GitHub issue").requiredOption("--title <title>", "Issue title").option("--body <body>", "Issue body (markdown)").option("--body-file <path>", "Read issue body from a file (use - for stdin); overridden by --body").option("--label <label>", "Label to apply (repeatable)", (v, acc) => {
2231
+ acc.push(v);
2232
+ return acc;
2233
+ }, []).option("--assignee <login>", "Assignee login (repeatable)", (v, acc) => {
2234
+ acc.push(v);
2235
+ return acc;
2236
+ }, []).action(async (opts) => {
2237
+ const start = Date.now();
2238
+ try {
2239
+ const { client, owner, repo } = getClient3(gh);
2240
+ const body = opts.body ?? resolveTextInput(void 0, opts.bodyFile, "body");
2241
+ const data = await client.createIssue({
2242
+ owner,
2243
+ repo,
2244
+ title: opts.title,
2245
+ body,
2246
+ labels: opts.label.length ? opts.label : void 0,
2247
+ assignees: opts.assignee.length ? opts.assignee : void 0
2248
+ });
2249
+ success(data, "github", "create-issue", start);
2250
+ } catch (err) {
2251
+ fail(err, "github", "create-issue", start);
2252
+ }
2253
+ });
1989
2254
  gh.command("list-prs").description("List pull requests").addOption(new Option("--state <state>", "PR state").choices(["open", "closed", "all"]).default("open")).option("--head <branch>", "Filter by head branch (user:branch)").option("--base <branch>", "Filter by base branch").action(async (opts) => {
1990
2255
  const start = Date.now();
1991
2256
  try {
@@ -2012,18 +2277,19 @@ function registerGitHubCommands(program2) {
2012
2277
  fail(err, "github", "get-pr", start);
2013
2278
  }
2014
2279
  });
2015
- gh.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--head <branch>", "Source branch (or user:branch for forks)").option("--base <branch>", "Target branch (defaults to config or main)").option("--body <body>", "PR description").option("--draft", "Create as draft PR").option("--owner <owner>", "GitHub owner (overrides parent --owner)").option("--repo <repo>", "GitHub repository name (overrides parent --repo)").action(async (opts) => {
2280
+ gh.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--head <branch>", "Source branch (or user:branch for forks)").option("--base <branch>", "Target branch (defaults to config or main)").option("--body <body>", "PR description").option("--body-file <path>", "Read PR description from a file (use - for stdin); overridden by --body").option("--draft", "Create as draft PR").option("--owner <owner>", "GitHub owner (overrides parent --owner)").option("--repo <repo>", "GitHub repository name (overrides parent --repo)").action(async (opts) => {
2016
2281
  const start = Date.now();
2017
2282
  try {
2018
2283
  const { client, owner, repo, config } = getClient3(gh, { owner: opts.owner, repo: opts.repo });
2019
2284
  const base = opts.base ?? config.defaults.github?.targetBranch ?? "main";
2285
+ const body = opts.body ?? resolveTextInput(void 0, opts.bodyFile, "body");
2020
2286
  const data = await client.createPR({
2021
2287
  owner,
2022
2288
  repo,
2023
2289
  title: opts.title,
2024
2290
  head: opts.head,
2025
2291
  base,
2026
- body: opts.body,
2292
+ body,
2027
2293
  draft: opts.draft
2028
2294
  });
2029
2295
  success(data, "github", "create-pr", start);
@@ -2248,7 +2514,12 @@ function registerGitHubCommands(program2) {
2248
2514
  });
2249
2515
  }
2250
2516
 
2517
+ // src/services/confluence/commands.ts
2518
+ import { existsSync as existsSync2 } from "fs";
2519
+
2251
2520
  // src/services/confluence/client.ts
2521
+ import { readFileSync as readFileSync4 } from "fs";
2522
+ import { basename as basename2 } from "path";
2252
2523
  var API3 = "/rest/api";
2253
2524
  var ConfluenceClient = class {
2254
2525
  constructor(http) {
@@ -2393,9 +2664,52 @@ var ConfluenceClient = class {
2393
2664
  });
2394
2665
  });
2395
2666
  }
2667
+ async uploadAttachment(pageId, filePath, comment) {
2668
+ const fileContent = readFileSync4(filePath);
2669
+ const fileName = basename2(filePath);
2670
+ const mimeType = guessMimeType(filePath);
2671
+ const formData = new FormData();
2672
+ formData.append("file", new Blob([fileContent], { type: mimeType }), fileName);
2673
+ if (comment) formData.append("comment", comment);
2674
+ const result = await this.http.confluenceUpload(
2675
+ `${API3}/content/${pageId}/child/attachment`,
2676
+ formData
2677
+ );
2678
+ return result.results;
2679
+ }
2680
+ async deleteAttachment(attachmentId) {
2681
+ return this.http.confluence(`${API3}/content/${attachmentId}`, { method: "DELETE" });
2682
+ }
2683
+ async getPageHistory(id) {
2684
+ return this.http.confluence(`${API3}/content/${id}/history`);
2685
+ }
2686
+ async convertToStorage(value, fromRepresentation = "markdown") {
2687
+ const result = await this.http.confluence(
2688
+ `${API3}/contentbody/convert/storage`,
2689
+ {
2690
+ method: "POST",
2691
+ body: { value, representation: fromRepresentation }
2692
+ }
2693
+ );
2694
+ return result.value;
2695
+ }
2396
2696
  };
2397
2697
 
2398
2698
  // src/services/confluence/commands.ts
2699
+ function xmlParseHint(err, bodyContent, convertedFromMarkdown = false) {
2700
+ if (!(err instanceof Error)) return null;
2701
+ const m = err.message.match(/at \[row,col\]=\{(\d+),(\d+)\}/);
2702
+ if (!m) return null;
2703
+ const row = parseInt(m[1], 10);
2704
+ const col = parseInt(m[2], 10);
2705
+ const lines = bodyContent.split("\n");
2706
+ const line = lines[row - 1];
2707
+ if (!line) return null;
2708
+ const pointer = " ".repeat(Math.max(0, col - 1)) + "^";
2709
+ const suffix = convertedFromMarkdown ? " (converted from Markdown)" : "";
2710
+ return `Offending line ${row}${suffix}: ${line}
2711
+ ${pointer}`;
2712
+ }
2399
2713
  function getClient4(program2) {
2400
2714
  const opts = program2.optsWithGlobals();
2401
2715
  const config = loadConfig({ configPath: opts.config });
@@ -2473,38 +2787,67 @@ function registerConfluenceCommands(program2) {
2473
2787
  fail(err, "confluence", "search", start);
2474
2788
  }
2475
2789
  });
2476
- confluence.command("create-page").description("Create a new Confluence page").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").requiredOption("--body <html>", "Page body (storage format HTML)").option("--parent-id <id>", "Parent page ID (to nest under a page)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2790
+ confluence.command("create-page").description("Create a new Confluence page").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").option("--body <html>", "Page body (storage format HTML or Markdown when --markdown is set)").option("--body-file <path>", "Path to a file containing the page body ('-' = stdin)").option("--markdown", "Convert body from Markdown to storage format via Confluence API").option("--parent-id <id>", "Parent page ID (to nest under a page)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2477
2791
  const start = Date.now();
2792
+ let bodyContent;
2478
2793
  try {
2479
2794
  const client = getClient4(program2);
2795
+ const rawBody = resolveTextInput(opts.body, opts.bodyFile, "body");
2796
+ if (rawBody === void 0) throw new PncliError("Must specify --body or --body-file", 1);
2797
+ bodyContent = opts.markdown ? await client.convertToStorage(rawBody) : rawBody;
2798
+ const representation = opts.markdown ? "storage" : opts.representation;
2480
2799
  const data = await client.createPage({
2481
2800
  spaceKey: opts.space,
2482
2801
  title: opts.title,
2483
- body: opts.body,
2802
+ body: bodyContent,
2484
2803
  parentId: opts.parentId,
2485
- representation: opts.representation
2804
+ representation
2486
2805
  });
2487
2806
  success(data, "confluence", "create-page", start);
2488
2807
  } catch (err) {
2489
- fail(err, "confluence", "create-page", start);
2808
+ const hint = bodyContent ? xmlParseHint(err, bodyContent, Boolean(opts.markdown)) : null;
2809
+ if (hint) {
2810
+ const msg = err instanceof Error ? err.message : String(err);
2811
+ const status = err instanceof PncliError ? err.status : 1;
2812
+ const url = err instanceof PncliError ? err.url : void 0;
2813
+ fail(new PncliError(`${msg}
2814
+
2815
+ ${hint}`, status, url), "confluence", "create-page", start);
2816
+ } else {
2817
+ fail(err, "confluence", "create-page", start);
2818
+ }
2490
2819
  }
2491
2820
  });
2492
- confluence.command("update-page").description("Update a Confluence page (fetches current version automatically)").requiredOption("--id <page-id>", "Page ID").option("--title <title>", "New page title").option("--body <html>", "New page body (storage format HTML)").option("--status <status>", "Page status: current (default) or draft", "current").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2821
+ confluence.command("update-page").description("Update a Confluence page (fetches current version automatically)").requiredOption("--id <page-id>", "Page ID").option("--title <title>", "New page title").option("--body <html>", "New page body (storage format HTML or Markdown when --markdown is set)").option("--body-file <path>", "Path to a file containing the new page body ('-' = stdin)").option("--markdown", "Convert body from Markdown to storage format via Confluence API").option("--status <status>", "Page status: current (default) or draft", "current").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
2493
2822
  const start = Date.now();
2823
+ let bodyContent;
2494
2824
  try {
2495
2825
  const client = getClient4(program2);
2826
+ const rawBody = resolveTextInput(opts.body, opts.bodyFile, "body");
2827
+ bodyContent = rawBody !== void 0 && opts.markdown ? await client.convertToStorage(rawBody) : rawBody;
2828
+ const representation = bodyContent !== void 0 && opts.markdown ? "storage" : opts.representation;
2496
2829
  const current = await client.getPage(opts.id, "version");
2497
2830
  const nextVersion = current.version.number + 1;
2498
2831
  const data = await client.updatePage(opts.id, {
2499
2832
  version: nextVersion,
2500
2833
  title: opts.title ?? current.title,
2501
- body: opts.body,
2834
+ body: bodyContent,
2502
2835
  status: opts.status,
2503
- representation: opts.representation
2836
+ representation
2504
2837
  });
2505
2838
  success(data, "confluence", "update-page", start);
2506
2839
  } catch (err) {
2507
- fail(err, "confluence", "update-page", start);
2840
+ const hint = bodyContent ? xmlParseHint(err, bodyContent, Boolean(opts.markdown)) : null;
2841
+ if (hint) {
2842
+ const msg = err instanceof Error ? err.message : String(err);
2843
+ const status = err instanceof PncliError ? err.status : 1;
2844
+ const url = err instanceof PncliError ? err.url : void 0;
2845
+ fail(new PncliError(`${msg}
2846
+
2847
+ ${hint}`, status, url), "confluence", "update-page", start);
2848
+ } else {
2849
+ fail(err, "confluence", "update-page", start);
2850
+ }
2508
2851
  }
2509
2852
  });
2510
2853
  confluence.command("delete-page").description("Delete a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
@@ -2591,6 +2934,37 @@ function registerConfluenceCommands(program2) {
2591
2934
  fail(err, "confluence", "list-attachments", start);
2592
2935
  }
2593
2936
  });
2937
+ confluence.command("upload-attachment").description("Upload a file as an attachment to a Confluence page").requiredOption("--id <page-id>", "Page ID").requiredOption("--file <path>", "Path to the file to upload").option("--comment <text>", "Optional comment to attach to the file version").action(async (opts) => {
2938
+ const start = Date.now();
2939
+ try {
2940
+ if (!existsSync2(opts.file)) throw new PncliError(`File not found: ${opts.file}`, 1);
2941
+ const client = getClient4(program2);
2942
+ const data = await client.uploadAttachment(opts.id, opts.file, opts.comment);
2943
+ success(data, "confluence", "upload-attachment", start);
2944
+ } catch (err) {
2945
+ fail(err, "confluence", "upload-attachment", start);
2946
+ }
2947
+ });
2948
+ confluence.command("delete-attachment").description("Delete an attachment from Confluence by its content ID (moves it to the trash; does not permanently purge it)").requiredOption("--id <attachment-id>", "Attachment content ID").action(async (opts) => {
2949
+ const start = Date.now();
2950
+ try {
2951
+ const client = getClient4(program2);
2952
+ await client.deleteAttachment(opts.id);
2953
+ success({ deleted: opts.id }, "confluence", "delete-attachment", start);
2954
+ } catch (err) {
2955
+ fail(err, "confluence", "delete-attachment", start);
2956
+ }
2957
+ });
2958
+ confluence.command("get-page-history").description("Get the version history metadata for a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
2959
+ const start = Date.now();
2960
+ try {
2961
+ const client = getClient4(program2);
2962
+ const data = await client.getPageHistory(opts.id);
2963
+ success(data, "confluence", "get-page-history", start);
2964
+ } catch (err) {
2965
+ fail(err, "confluence", "get-page-history", start);
2966
+ }
2967
+ });
2594
2968
  }
2595
2969
 
2596
2970
  // src/services/sonar/client.ts
@@ -4909,8 +5283,8 @@ import password from "@inquirer/password";
4909
5283
  import confirm from "@inquirer/confirm";
4910
5284
 
4911
5285
  // src/services/ado/client/work.ts
4912
- import { readFileSync as readFileSync3 } from "fs";
4913
- import { basename as basename2 } from "path";
5286
+ import { readFileSync as readFileSync5 } from "fs";
5287
+ import { basename as basename3 } from "path";
4914
5288
  var API6 = "7.1";
4915
5289
  var API_PREVIEW = "7.1-preview.4";
4916
5290
  var AdoWorkClient = class {
@@ -5025,8 +5399,8 @@ var AdoWorkClient = class {
5025
5399
  );
5026
5400
  }
5027
5401
  async uploadAttachment(collection, workItemId, filePath, comment) {
5028
- const fileContent = readFileSync3(filePath);
5029
- const fileName = basename2(filePath);
5402
+ const fileContent = readFileSync5(filePath);
5403
+ const fileName = basename3(filePath);
5030
5404
  const attachment = await this.http.adoUpload(
5031
5405
  `/${encodeURIComponent(collection)}/_apis/wit/attachments?fileName=${encodeURIComponent(fileName)}&api-version=${API6}`,
5032
5406
  fileContent,
@@ -5077,32 +5451,32 @@ async function discoverTypes(client, collection, project) {
5077
5451
  }
5078
5452
  return result;
5079
5453
  }
5454
+ var ADO_FIELD_ALIAS_CANDIDATES = [
5455
+ ["title", "System.Title"],
5456
+ ["state", "System.State"],
5457
+ ["assignedto", "System.AssignedTo"],
5458
+ ["assigned-to", "System.AssignedTo"],
5459
+ ["description", "System.Description"],
5460
+ ["areapath", "System.AreaPath"],
5461
+ ["area-path", "System.AreaPath"],
5462
+ ["iterationpath", "System.IterationPath"],
5463
+ ["iteration", "System.IterationPath"],
5464
+ ["tags", "System.Tags"],
5465
+ ["priority", "Microsoft.VSTS.Common.Priority"],
5466
+ ["severity", "Microsoft.VSTS.Common.Severity"],
5467
+ ["effort", "Microsoft.VSTS.Scheduling.Effort"],
5468
+ ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
5469
+ ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
5470
+ ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
5471
+ ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
5472
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
5473
+ ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
5474
+ ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
5475
+ ];
5080
5476
  function buildDefaultAliases(fields) {
5081
5477
  const refNames = new Set(fields.map((f) => f.referenceName));
5082
- const candidates = [
5083
- ["title", "System.Title"],
5084
- ["state", "System.State"],
5085
- ["assignedto", "System.AssignedTo"],
5086
- ["assigned-to", "System.AssignedTo"],
5087
- ["description", "System.Description"],
5088
- ["areapath", "System.AreaPath"],
5089
- ["area-path", "System.AreaPath"],
5090
- ["iterationpath", "System.IterationPath"],
5091
- ["iteration", "System.IterationPath"],
5092
- ["tags", "System.Tags"],
5093
- ["priority", "Microsoft.VSTS.Common.Priority"],
5094
- ["severity", "Microsoft.VSTS.Common.Severity"],
5095
- ["effort", "Microsoft.VSTS.Scheduling.Effort"],
5096
- ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
5097
- ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
5098
- ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
5099
- ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
5100
- ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
5101
- ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
5102
- ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
5103
- ];
5104
5478
  const aliases = {};
5105
- for (const [alias, ref] of candidates) {
5479
+ for (const [alias, ref] of ADO_FIELD_ALIAS_CANDIDATES) {
5106
5480
  if (refNames.has(ref)) {
5107
5481
  aliases[alias] = ref;
5108
5482
  }
@@ -6350,6 +6724,12 @@ var AdoGitClient = class {
6350
6724
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}?api-version=${API7}`
6351
6725
  );
6352
6726
  }
6727
+ async createRepo(collection, project, name) {
6728
+ return this.http.ado(
6729
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories?api-version=${API7}`,
6730
+ { method: "POST", body: { name } }
6731
+ );
6732
+ }
6353
6733
  // ── Pull Requests ─────────────────────────────────────────────────
6354
6734
  async listPRs(collection, project, repo, opts = {}) {
6355
6735
  return this.http.adoPaginate(async (token) => {
@@ -6604,14 +6984,34 @@ function parseFieldArgs2(fields) {
6604
6984
  }
6605
6985
  return result;
6606
6986
  }
6987
+ var stripSeparators = (s) => s.toLowerCase().replace(/[\s-]+/g, "");
6988
+ var DEFAULT_ADO_FIELD_ALIASES = Object.fromEntries(ADO_FIELD_ALIAS_CANDIDATES);
6607
6989
  function buildFieldPatch(fields, fieldAliases) {
6990
+ const aliases = { ...DEFAULT_ADO_FIELD_ALIASES, ...fieldAliases };
6608
6991
  return Object.entries(fields).map(([key, value]) => {
6609
- const refName = fieldAliases[key.toLowerCase()] ?? fieldAliases[key] ?? key;
6610
- return { op: "add", path: `/fields/${refName}`, value };
6611
- });
6612
- }
6613
- function fieldArgsToPatch(fieldArgs, fieldAliases) {
6614
- return buildFieldPatch(parseFieldArgs2(fieldArgs), fieldAliases);
6992
+ let refName = aliases[key.toLowerCase()] ?? aliases[key];
6993
+ if (!refName) {
6994
+ const target = stripSeparators(key);
6995
+ const match = Object.entries(aliases).find(([aliasKey]) => stripSeparators(aliasKey) === target);
6996
+ refName = match?.[1];
6997
+ }
6998
+ return { op: "add", path: `/fields/${refName ?? key}`, value };
6999
+ });
7000
+ }
7001
+ var BUILTIN_WORK_FIELD_KEYS = /* @__PURE__ */ new Set(["title", "description", "assignee", "assignedto", "priority"]);
7002
+ function splitWorkFieldsDictionary(fields) {
7003
+ const builtin = {};
7004
+ const custom = {};
7005
+ for (const [rawKey, rawValue] of Object.entries(fields)) {
7006
+ const value = resolveAtFileRef(rawValue);
7007
+ const key = rawKey.toLowerCase().replace(/\s+/g, "");
7008
+ if (BUILTIN_WORK_FIELD_KEYS.has(key)) {
7009
+ builtin[key === "assignedto" ? "assignee" : key] = value;
7010
+ } else {
7011
+ custom[rawKey] = value;
7012
+ }
7013
+ }
7014
+ return { builtin, custom };
6615
7015
  }
6616
7016
  var PR_VOTE = {
6617
7017
  APPROVE: 10,
@@ -6679,6 +7079,35 @@ function registerAdoProjectCommands(ado) {
6679
7079
  // src/services/ado/commands/work.ts
6680
7080
  import fs3 from "fs";
6681
7081
  import path6 from "path";
7082
+
7083
+ // src/services/ado/input-schema.ts
7084
+ var ADO_WORK_INPUT_FILE_SCHEMA = {
7085
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7086
+ title: "pncli Azure DevOps work item input file",
7087
+ 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.",
7088
+ type: "object",
7089
+ properties: {
7090
+ type: { type: "string", description: "Work item type, e.g. Bug, Task, User Story (create only)" },
7091
+ fields: {
7092
+ type: "object",
7093
+ 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).',
7094
+ additionalProperties: true
7095
+ }
7096
+ },
7097
+ additionalProperties: false
7098
+ };
7099
+ var ADO_WORK_INPUT_FILE_EXAMPLE = {
7100
+ type: "Bug",
7101
+ fields: {
7102
+ Title: "Login broken on Safari",
7103
+ Description: "@desc.html",
7104
+ "Acceptance Criteria": "@ac.md",
7105
+ Priority: 2,
7106
+ "MyOrg.SomeCustomField": "x"
7107
+ }
7108
+ };
7109
+
7110
+ // src/services/ado/commands/work.ts
6682
7111
  function registerAdoWorkCommands(ado) {
6683
7112
  const work = ado.command("work").description("Azure DevOps work item operations");
6684
7113
  work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
@@ -6691,28 +7120,45 @@ function registerAdoWorkCommands(ado) {
6691
7120
  fail(err, "ado", "work-get", start);
6692
7121
  }
6693
7122
  });
6694
- work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--parent <id>", "Parent work item ID \u2014 creates a parent link after creation").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
7123
+ 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
7124
  acc.push(v);
6696
7125
  return acc;
6697
- }, []).action(async (opts) => {
7126
+ }, []).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
7127
  const start = Date.now();
6699
7128
  try {
6700
7129
  const globalOpts = ado.optsWithGlobals();
6701
7130
  const config = loadConfig({ configPath: globalOpts.config });
6702
7131
  const { collection, project, workClient } = getAdoContext(ado);
7132
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
7133
+ const { builtin: jsonBuiltin, custom: jsonCustom } = jsonInput?.fields ? splitWorkFieldsDictionary(jsonInput.fields) : { builtin: {}, custom: {} };
7134
+ const type = opts.type ?? jsonInput?.type;
7135
+ if (!type) throw new PncliError('--type required (or "type" in --input-file)', 1);
7136
+ const flagBuiltin = {
7137
+ ...opts.title !== void 0 ? { title: opts.title } : {},
7138
+ ...opts.description !== void 0 ? { description: opts.description } : {},
7139
+ ...opts.assignee !== void 0 ? { assignee: opts.assignee } : {},
7140
+ ...opts.priority !== void 0 ? { priority: opts.priority } : {}
7141
+ };
7142
+ const { merged: builtinFields, overrides: builtinOverrides } = mergeWithOverrides(jsonBuiltin, flagBuiltin);
7143
+ if (!builtinFields.title) throw new PncliError('--title required (or "fields.Title" in --input-file)', 1);
7144
+ const flagCustom = parseFieldArgs2(opts.field);
7145
+ const { merged: customFields, overrides: customOverrides } = mergeWithOverrides(jsonCustom, flagCustom);
7146
+ const overrides = [...builtinOverrides, ...customOverrides];
7147
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
7148
+ const hasContent = (v) => v !== void 0 && v !== "";
6703
7149
  const builtIn = {
6704
- "System.Title": opts.title,
6705
- ...opts.description ? { "System.Description": opts.description } : {},
6706
- ...opts.assignee ? { "System.AssignedTo": opts.assignee } : {},
6707
- ...opts.priority ? { "Microsoft.VSTS.Common.Priority": opts.priority } : {}
7150
+ "System.Title": builtinFields.title,
7151
+ ...hasContent(builtinFields.description) ? { "System.Description": builtinFields.description } : {},
7152
+ ...hasContent(builtinFields.assignee) ? { "System.AssignedTo": builtinFields.assignee } : {},
7153
+ ...hasContent(builtinFields.priority) ? { "Microsoft.VSTS.Common.Priority": builtinFields.priority } : {}
6708
7154
  };
6709
- const extra = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
7155
+ const extra = buildFieldPatch(customFields, config.ado.fieldAliases);
6710
7156
  const patch = [
6711
7157
  ...Object.entries(builtIn).map(([k, v]) => ({ op: "add", path: `/fields/${k}`, value: v })),
6712
7158
  ...extra
6713
7159
  ];
6714
- const data = await workClient.createWorkItem(collection, project, opts.type, patch);
6715
- success(data, "ado", "work-create", start);
7160
+ const data = await workClient.createWorkItem(collection, project, type, patch);
7161
+ success(data, "ado", "work-create", start, overrides);
6716
7162
  if (opts.parent) {
6717
7163
  try {
6718
7164
  const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
@@ -6731,15 +7177,20 @@ function registerAdoWorkCommands(ado) {
6731
7177
  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
7178
  acc.push(v);
6733
7179
  return acc;
6734
- }, []).action(async (opts) => {
7180
+ }, []).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
7181
  const start = Date.now();
6736
7182
  try {
6737
7183
  const globalOpts = ado.optsWithGlobals();
6738
7184
  const config = loadConfig({ configPath: globalOpts.config });
6739
7185
  const { collection, workClient } = getAdoContext(ado);
6740
- const patch = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
7186
+ const jsonInput = opts.inputFile ? readJsonInputFile(opts.inputFile) : void 0;
7187
+ const jsonFields = jsonInput?.fields ? Object.fromEntries(Object.entries(jsonInput.fields).map(([k, v]) => [k, resolveAtFileRef(v)])) : {};
7188
+ const flagFields = parseFieldArgs2(opts.field);
7189
+ const { merged, overrides } = mergeWithOverrides(jsonFields, flagFields);
7190
+ if (overrides.length) warn(`--input-file value(s) overridden by CLI flags: ${overrides.join(", ")}`);
7191
+ const patch = buildFieldPatch(merged, config.ado.fieldAliases);
6741
7192
  const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
6742
- success(data, "ado", "work-update", start);
7193
+ success(data, "ado", "work-update", start, overrides);
6743
7194
  } catch (err) {
6744
7195
  fail(err, "ado", "work-update", start);
6745
7196
  }
@@ -6950,6 +7401,14 @@ function registerAdoWorkCommands(ado) {
6950
7401
  fail(err, "ado", "work-fields", start);
6951
7402
  }
6952
7403
  });
7404
+ 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) => {
7405
+ const start = Date.now();
7406
+ if (opts.exampleOnly) {
7407
+ writeRawOutput(JSON.stringify(ADO_WORK_INPUT_FILE_EXAMPLE, null, 2) + "\n");
7408
+ return;
7409
+ }
7410
+ success({ schema: ADO_WORK_INPUT_FILE_SCHEMA, example: ADO_WORK_INPUT_FILE_EXAMPLE }, "ado", "work-schema", start);
7411
+ });
6953
7412
  }
6954
7413
 
6955
7414
  // src/services/ado/commands/repo.ts
@@ -6978,6 +7437,16 @@ function registerAdoRepoCommands(ado) {
6978
7437
  fail(err, "ado", "repo-list", start);
6979
7438
  }
6980
7439
  });
7440
+ repo.command("create").description("Create a new git repository in the project").requiredOption("--name <name>", "Repository name").action(async (opts) => {
7441
+ const start = Date.now();
7442
+ try {
7443
+ const { collection, project, gitClient } = getAdoContext(ado);
7444
+ const data = await gitClient.createRepo(collection, project, opts.name);
7445
+ success(data, "ado", "repo-create", start);
7446
+ } catch (err) {
7447
+ fail(err, "ado", "repo-create", start);
7448
+ }
7449
+ });
6981
7450
  repo.command("get").description("Get a specific repository").action(async () => {
6982
7451
  const start = Date.now();
6983
7452
  try {
@@ -7219,11 +7688,11 @@ function registerAdoRepoCommands(ado) {
7219
7688
  const start = Date.now();
7220
7689
  try {
7221
7690
  const globalOpts = ado.optsWithGlobals();
7222
- const { loadConfig: lc } = await import("./config-S56LADVF.js");
7691
+ const { loadConfig: lc } = await import("./config-QN7KDHE4.js");
7223
7692
  const config = lc({ configPath: globalOpts.config });
7224
7693
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
7225
7694
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
7226
- const { createHttpClient: createHttpClient2 } = await import("./http-5WRBWEM6.js");
7695
+ const { createHttpClient: createHttpClient2 } = await import("./http-DDY5DLF6.js");
7227
7696
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
7228
7697
  const coreClient = new AdoCoreClient2(http);
7229
7698
  const me = await coreClient.getConnectionData(collection);