@kolatts/pncli 1.16.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-P5NWDQ2B.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
@@ -876,6 +889,7 @@ function guessMimeType(filePath) {
876
889
 
877
890
  // src/services/jira/client.ts
878
891
  var API2 = "/rest/api/2";
892
+ var AGILE_API = "/rest/agile/1.0";
879
893
  var JiraClient = class {
880
894
  constructor(http) {
881
895
  this.http = http;
@@ -910,7 +924,7 @@ var JiraClient = class {
910
924
  if (opts.description) fields.description = opts.description;
911
925
  if (opts.priority) fields.priority = { name: opts.priority };
912
926
  if (opts.assignee) fields.assignee = { name: opts.assignee };
913
- if (opts.labels) fields.labels = opts.labels;
927
+ if (opts.labels?.length) fields.labels = opts.labels;
914
928
  Object.assign(fields, opts.customFieldValues ?? {});
915
929
  await this.http.jira(`${API2}/issue/${key}`, {
916
930
  method: "PUT",
@@ -1071,6 +1085,42 @@ var JiraClient = class {
1071
1085
  }
1072
1086
  });
1073
1087
  }
1088
+ async listBoards(project) {
1089
+ return this.http.jiraPaginate(async (startAt, maxResults) => {
1090
+ return this.http.jira(
1091
+ `${AGILE_API}/board`,
1092
+ { params: { projectKeyOrId: project, startAt, maxResults } }
1093
+ );
1094
+ });
1095
+ }
1096
+ async listSprintsForBoard(boardId, states) {
1097
+ return this.http.jiraPaginate(async (startAt, maxResults) => {
1098
+ return this.http.jira(
1099
+ `${AGILE_API}/board/${boardId}/sprint`,
1100
+ { params: { startAt, maxResults, ...states?.length ? { state: states.join(",") } : {} } }
1101
+ );
1102
+ });
1103
+ }
1104
+ async listSprintsForProject(project, states) {
1105
+ const boards = await this.listBoards(project);
1106
+ const sprints = [];
1107
+ const seen = /* @__PURE__ */ new Set();
1108
+ for (const board of boards) {
1109
+ if (board.type !== "scrum") continue;
1110
+ for (const sprint of await this.listSprintsForBoard(board.id, states)) {
1111
+ if (seen.has(sprint.id)) continue;
1112
+ seen.add(sprint.id);
1113
+ sprints.push(sprint);
1114
+ }
1115
+ }
1116
+ return sprints;
1117
+ }
1118
+ async setSprint(sprintId, issueKeys) {
1119
+ await this.http.jira(`${AGILE_API}/sprint/${sprintId}/issue`, {
1120
+ method: "POST",
1121
+ body: { issues: issueKeys }
1122
+ });
1123
+ }
1074
1124
  };
1075
1125
  function schemaToPncliType(schema) {
1076
1126
  if (!schema) return void 0;
@@ -1170,7 +1220,119 @@ function formatFieldValue(value, type) {
1170
1220
  }
1171
1221
  }
1172
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
+
1173
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
+ }
1174
1336
  function getClient(program2) {
1175
1337
  const opts = program2.optsWithGlobals();
1176
1338
  const config = loadConfig({ configPath: opts.config });
@@ -1210,7 +1372,7 @@ function registerJiraCommands(program2) {
1210
1372
  fail(err, "jira", "get-issue", start);
1211
1373
  }
1212
1374
  });
1213
- 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) => {
1214
1376
  const start = Date.now();
1215
1377
  let fieldMap = buildFieldMap([]);
1216
1378
  try {
@@ -1218,35 +1380,87 @@ function registerJiraCommands(program2) {
1218
1380
  const client = resolved.client;
1219
1381
  fieldMap = resolved.fieldMap;
1220
1382
  const defaults = getDefaults(program2);
1221
- const project = opts.project ?? defaults.project;
1222
- const issueType = opts.type ?? defaults.issueType ?? "Task";
1223
- const priority = opts.priority ?? defaults.priority;
1224
- if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
1225
- 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;
1226
1400
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1227
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1228
- const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, parent: opts.parent, customFieldValues });
1229
- 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);
1230
1417
  } catch (err) {
1231
1418
  fail(translateFieldErrors(err, fieldMap), "jira", "create-issue", start);
1232
1419
  }
1233
1420
  });
1234
- 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) => {
1235
1422
  const start = Date.now();
1236
1423
  let fieldMap = buildFieldMap([]);
1237
1424
  try {
1238
1425
  const resolved = getClientAndFields(program2);
1239
1426
  const client = resolved.client;
1240
1427
  fieldMap = resolved.fieldMap;
1241
- 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);
1242
1438
  const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1243
- const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1244
- await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
1245
- 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);
1246
1452
  } catch (err) {
1247
1453
  fail(translateFieldErrors(err, fieldMap), "jira", "update-issue", start);
1248
1454
  }
1249
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
+ });
1250
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) => {
1251
1465
  const start = Date.now();
1252
1466
  try {
@@ -1406,6 +1620,38 @@ function registerJiraCommands(program2) {
1406
1620
  fail(err, "jira", "fields", start);
1407
1621
  }
1408
1622
  });
1623
+ jira.command("list-boards").description("List Agile boards for a project").requiredOption("--project <key>", "Project key").action(async (opts) => {
1624
+ const start = Date.now();
1625
+ try {
1626
+ const client = getClient(program2);
1627
+ const data = await client.listBoards(opts.project);
1628
+ success(data, "jira", "list-boards", start);
1629
+ } catch (err) {
1630
+ fail(err, "jira", "list-boards", start);
1631
+ }
1632
+ });
1633
+ jira.command("list-sprints").description("List sprints for a board or project, including start/end dates").option("--board <id>", "Board ID").option("--project <key>", "Project key \u2014 resolves to that project's board(s)").option("--state <states>", "Comma-separated states to filter (active,future,closed)").action(async (opts) => {
1634
+ const start = Date.now();
1635
+ try {
1636
+ if (!opts.board && !opts.project) throw new PncliError("Either --board or --project is required", 1);
1637
+ const client = getClient(program2);
1638
+ const states = opts.state ? opts.state.split(",").map((s) => s.trim()) : void 0;
1639
+ const data = opts.board ? await client.listSprintsForBoard(parseInt(opts.board, 10), states) : await client.listSprintsForProject(opts.project, states);
1640
+ success(data, "jira", "list-sprints", start);
1641
+ } catch (err) {
1642
+ fail(err, "jira", "list-sprints", start);
1643
+ }
1644
+ });
1645
+ jira.command("set-sprint").description("Move a Jira issue into a sprint").requiredOption("--key <issue-key>", "Issue key").requiredOption("--sprint <id>", "Sprint ID (from list-sprints)").action(async (opts) => {
1646
+ const start = Date.now();
1647
+ try {
1648
+ const client = getClient(program2);
1649
+ await client.setSprint(parseInt(opts.sprint, 10), [opts.key]);
1650
+ success({ updated: opts.key, sprint: opts.sprint }, "jira", "set-sprint", start);
1651
+ } catch (err) {
1652
+ fail(err, "jira", "set-sprint", start);
1653
+ }
1654
+ });
1409
1655
  }
1410
1656
  function parseFieldArgs(fieldArgs, fieldMap) {
1411
1657
  const result = {};
@@ -1423,7 +1669,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1423
1669
  const filePath = value.slice(1);
1424
1670
  let raw;
1425
1671
  try {
1426
- raw = readFileSync2(filePath, "utf8").trim();
1672
+ raw = readFileSync3(filePath, "utf8").trim();
1427
1673
  } catch (e) {
1428
1674
  throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1429
1675
  }
@@ -1441,7 +1687,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1441
1687
  function parseFieldsFile(filePath, fieldMap) {
1442
1688
  let raw;
1443
1689
  try {
1444
- raw = readFileSync2(filePath, "utf8");
1690
+ raw = readFileSync3(filePath, "utf8");
1445
1691
  } catch (e) {
1446
1692
  throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
1447
1693
  }
@@ -1494,6 +1740,24 @@ function getClient2(program2, overrides) {
1494
1740
  }
1495
1741
  function registerBitbucketCommands(program2) {
1496
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
+ });
1497
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) => {
1498
1762
  const start = Date.now();
1499
1763
  try {
@@ -1896,6 +2160,32 @@ var GitHubClient = class {
1896
2160
  }
1897
2161
  return results;
1898
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
+ }
1899
2189
  };
1900
2190
 
1901
2191
  // src/services/github/commands.ts
@@ -1917,6 +2207,50 @@ function getClient3(program2, overrides) {
1917
2207
  }
1918
2208
  function registerGitHubCommands(program2) {
1919
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
+ });
1920
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) => {
1921
2255
  const start = Date.now();
1922
2256
  try {
@@ -1943,18 +2277,19 @@ function registerGitHubCommands(program2) {
1943
2277
  fail(err, "github", "get-pr", start);
1944
2278
  }
1945
2279
  });
1946
- 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) => {
1947
2281
  const start = Date.now();
1948
2282
  try {
1949
2283
  const { client, owner, repo, config } = getClient3(gh, { owner: opts.owner, repo: opts.repo });
1950
2284
  const base = opts.base ?? config.defaults.github?.targetBranch ?? "main";
2285
+ const body = opts.body ?? resolveTextInput(void 0, opts.bodyFile, "body");
1951
2286
  const data = await client.createPR({
1952
2287
  owner,
1953
2288
  repo,
1954
2289
  title: opts.title,
1955
2290
  head: opts.head,
1956
2291
  base,
1957
- body: opts.body,
2292
+ body,
1958
2293
  draft: opts.draft
1959
2294
  });
1960
2295
  success(data, "github", "create-pr", start);
@@ -2179,7 +2514,12 @@ function registerGitHubCommands(program2) {
2179
2514
  });
2180
2515
  }
2181
2516
 
2517
+ // src/services/confluence/commands.ts
2518
+ import { existsSync as existsSync2 } from "fs";
2519
+
2182
2520
  // src/services/confluence/client.ts
2521
+ import { readFileSync as readFileSync4 } from "fs";
2522
+ import { basename as basename2 } from "path";
2183
2523
  var API3 = "/rest/api";
2184
2524
  var ConfluenceClient = class {
2185
2525
  constructor(http) {
@@ -2324,9 +2664,52 @@ var ConfluenceClient = class {
2324
2664
  });
2325
2665
  });
2326
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
+ }
2327
2696
  };
2328
2697
 
2329
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
+ }
2330
2713
  function getClient4(program2) {
2331
2714
  const opts = program2.optsWithGlobals();
2332
2715
  const config = loadConfig({ configPath: opts.config });
@@ -2404,38 +2787,67 @@ function registerConfluenceCommands(program2) {
2404
2787
  fail(err, "confluence", "search", start);
2405
2788
  }
2406
2789
  });
2407
- 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) => {
2408
2791
  const start = Date.now();
2792
+ let bodyContent;
2409
2793
  try {
2410
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;
2411
2799
  const data = await client.createPage({
2412
2800
  spaceKey: opts.space,
2413
2801
  title: opts.title,
2414
- body: opts.body,
2802
+ body: bodyContent,
2415
2803
  parentId: opts.parentId,
2416
- representation: opts.representation
2804
+ representation
2417
2805
  });
2418
2806
  success(data, "confluence", "create-page", start);
2419
2807
  } catch (err) {
2420
- 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
+ }
2421
2819
  }
2422
2820
  });
2423
- 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) => {
2424
2822
  const start = Date.now();
2823
+ let bodyContent;
2425
2824
  try {
2426
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;
2427
2829
  const current = await client.getPage(opts.id, "version");
2428
2830
  const nextVersion = current.version.number + 1;
2429
2831
  const data = await client.updatePage(opts.id, {
2430
2832
  version: nextVersion,
2431
2833
  title: opts.title ?? current.title,
2432
- body: opts.body,
2834
+ body: bodyContent,
2433
2835
  status: opts.status,
2434
- representation: opts.representation
2836
+ representation
2435
2837
  });
2436
2838
  success(data, "confluence", "update-page", start);
2437
2839
  } catch (err) {
2438
- 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
+ }
2439
2851
  }
2440
2852
  });
2441
2853
  confluence.command("delete-page").description("Delete a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
@@ -2522,6 +2934,37 @@ function registerConfluenceCommands(program2) {
2522
2934
  fail(err, "confluence", "list-attachments", start);
2523
2935
  }
2524
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
+ });
2525
2968
  }
2526
2969
 
2527
2970
  // src/services/sonar/client.ts
@@ -4840,8 +5283,8 @@ import password from "@inquirer/password";
4840
5283
  import confirm from "@inquirer/confirm";
4841
5284
 
4842
5285
  // src/services/ado/client/work.ts
4843
- import { readFileSync as readFileSync3 } from "fs";
4844
- import { basename as basename2 } from "path";
5286
+ import { readFileSync as readFileSync5 } from "fs";
5287
+ import { basename as basename3 } from "path";
4845
5288
  var API6 = "7.1";
4846
5289
  var API_PREVIEW = "7.1-preview.4";
4847
5290
  var AdoWorkClient = class {
@@ -4945,9 +5388,19 @@ var AdoWorkClient = class {
4945
5388
  async downloadAttachment(absoluteUrl) {
4946
5389
  return this.http.adoBuffer(absoluteUrl);
4947
5390
  }
5391
+ async listAreas(collection, project, depth = 10) {
5392
+ return this.http.ado(
5393
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/classificationnodes/areas?$depth=${depth}&api-version=${API6}`
5394
+ );
5395
+ }
5396
+ async listIterations(collection, project, depth = 10) {
5397
+ return this.http.ado(
5398
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/classificationnodes/iterations?$depth=${depth}&api-version=${API6}`
5399
+ );
5400
+ }
4948
5401
  async uploadAttachment(collection, workItemId, filePath, comment) {
4949
- const fileContent = readFileSync3(filePath);
4950
- const fileName = basename2(filePath);
5402
+ const fileContent = readFileSync5(filePath);
5403
+ const fileName = basename3(filePath);
4951
5404
  const attachment = await this.http.adoUpload(
4952
5405
  `/${encodeURIComponent(collection)}/_apis/wit/attachments?fileName=${encodeURIComponent(fileName)}&api-version=${API6}`,
4953
5406
  fileContent,
@@ -4998,32 +5451,32 @@ async function discoverTypes(client, collection, project) {
4998
5451
  }
4999
5452
  return result;
5000
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
+ ];
5001
5476
  function buildDefaultAliases(fields) {
5002
5477
  const refNames = new Set(fields.map((f) => f.referenceName));
5003
- const candidates = [
5004
- ["title", "System.Title"],
5005
- ["state", "System.State"],
5006
- ["assignedto", "System.AssignedTo"],
5007
- ["assigned-to", "System.AssignedTo"],
5008
- ["description", "System.Description"],
5009
- ["areapath", "System.AreaPath"],
5010
- ["area-path", "System.AreaPath"],
5011
- ["iterationpath", "System.IterationPath"],
5012
- ["iteration", "System.IterationPath"],
5013
- ["tags", "System.Tags"],
5014
- ["priority", "Microsoft.VSTS.Common.Priority"],
5015
- ["severity", "Microsoft.VSTS.Common.Severity"],
5016
- ["effort", "Microsoft.VSTS.Scheduling.Effort"],
5017
- ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
5018
- ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
5019
- ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
5020
- ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
5021
- ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
5022
- ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
5023
- ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
5024
- ];
5025
5478
  const aliases = {};
5026
- for (const [alias, ref] of candidates) {
5479
+ for (const [alias, ref] of ADO_FIELD_ALIAS_CANDIDATES) {
5027
5480
  if (refNames.has(ref)) {
5028
5481
  aliases[alias] = ref;
5029
5482
  }
@@ -6271,6 +6724,12 @@ var AdoGitClient = class {
6271
6724
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}?api-version=${API7}`
6272
6725
  );
6273
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
+ }
6274
6733
  // ── Pull Requests ─────────────────────────────────────────────────
6275
6734
  async listPRs(collection, project, repo, opts = {}) {
6276
6735
  return this.http.adoPaginate(async (token) => {
@@ -6525,14 +6984,34 @@ function parseFieldArgs2(fields) {
6525
6984
  }
6526
6985
  return result;
6527
6986
  }
6987
+ var stripSeparators = (s) => s.toLowerCase().replace(/[\s-]+/g, "");
6988
+ var DEFAULT_ADO_FIELD_ALIASES = Object.fromEntries(ADO_FIELD_ALIAS_CANDIDATES);
6528
6989
  function buildFieldPatch(fields, fieldAliases) {
6990
+ const aliases = { ...DEFAULT_ADO_FIELD_ALIASES, ...fieldAliases };
6529
6991
  return Object.entries(fields).map(([key, value]) => {
6530
- const refName = fieldAliases[key.toLowerCase()] ?? fieldAliases[key] ?? key;
6531
- return { op: "add", path: `/fields/${refName}`, value };
6532
- });
6533
- }
6534
- function fieldArgsToPatch(fieldArgs, fieldAliases) {
6535
- 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 };
6536
7015
  }
6537
7016
  var PR_VOTE = {
6538
7017
  APPROVE: 10,
@@ -6600,6 +7079,35 @@ function registerAdoProjectCommands(ado) {
6600
7079
  // src/services/ado/commands/work.ts
6601
7080
  import fs3 from "fs";
6602
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
6603
7111
  function registerAdoWorkCommands(ado) {
6604
7112
  const work = ado.command("work").description("Azure DevOps work item operations");
6605
7113
  work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
@@ -6612,28 +7120,45 @@ function registerAdoWorkCommands(ado) {
6612
7120
  fail(err, "ado", "work-get", start);
6613
7121
  }
6614
7122
  });
6615
- 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) => {
6616
7124
  acc.push(v);
6617
7125
  return acc;
6618
- }, []).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) => {
6619
7127
  const start = Date.now();
6620
7128
  try {
6621
7129
  const globalOpts = ado.optsWithGlobals();
6622
7130
  const config = loadConfig({ configPath: globalOpts.config });
6623
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 !== "";
6624
7149
  const builtIn = {
6625
- "System.Title": opts.title,
6626
- ...opts.description ? { "System.Description": opts.description } : {},
6627
- ...opts.assignee ? { "System.AssignedTo": opts.assignee } : {},
6628
- ...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 } : {}
6629
7154
  };
6630
- const extra = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
7155
+ const extra = buildFieldPatch(customFields, config.ado.fieldAliases);
6631
7156
  const patch = [
6632
7157
  ...Object.entries(builtIn).map(([k, v]) => ({ op: "add", path: `/fields/${k}`, value: v })),
6633
7158
  ...extra
6634
7159
  ];
6635
- const data = await workClient.createWorkItem(collection, project, opts.type, patch);
6636
- success(data, "ado", "work-create", start);
7160
+ const data = await workClient.createWorkItem(collection, project, type, patch);
7161
+ success(data, "ado", "work-create", start, overrides);
6637
7162
  if (opts.parent) {
6638
7163
  try {
6639
7164
  const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
@@ -6652,15 +7177,20 @@ function registerAdoWorkCommands(ado) {
6652
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) => {
6653
7178
  acc.push(v);
6654
7179
  return acc;
6655
- }, []).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) => {
6656
7181
  const start = Date.now();
6657
7182
  try {
6658
7183
  const globalOpts = ado.optsWithGlobals();
6659
7184
  const config = loadConfig({ configPath: globalOpts.config });
6660
7185
  const { collection, workClient } = getAdoContext(ado);
6661
- 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);
6662
7192
  const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
6663
- success(data, "ado", "work-update", start);
7193
+ success(data, "ado", "work-update", start, overrides);
6664
7194
  } catch (err) {
6665
7195
  fail(err, "ado", "work-update", start);
6666
7196
  }
@@ -6799,6 +7329,26 @@ function registerAdoWorkCommands(ado) {
6799
7329
  fail(err, "ado", "work-download-attachment", start);
6800
7330
  }
6801
7331
  });
7332
+ work.command("list-areas").description("List classification areas for the team project (tree)").option("--depth <n>", "Depth of the area tree to retrieve", "10").action(async (opts) => {
7333
+ const start = Date.now();
7334
+ try {
7335
+ const { collection, project, workClient } = getAdoContext(ado);
7336
+ const data = await workClient.listAreas(collection, project, parseInt(opts.depth, 10));
7337
+ success(data, "ado", "work-list-areas", start);
7338
+ } catch (err) {
7339
+ fail(err, "ado", "work-list-areas", start);
7340
+ }
7341
+ });
7342
+ work.command("list-iterations").description("List classification iterations for the team project (tree), including start/finish dates").option("--depth <n>", "Depth of the iteration tree to retrieve", "10").action(async (opts) => {
7343
+ const start = Date.now();
7344
+ try {
7345
+ const { collection, project, workClient } = getAdoContext(ado);
7346
+ const data = await workClient.listIterations(collection, project, parseInt(opts.depth, 10));
7347
+ success(data, "ado", "work-list-iterations", start);
7348
+ } catch (err) {
7349
+ fail(err, "ado", "work-list-iterations", start);
7350
+ }
7351
+ });
6802
7352
  work.command("types").description("List work item types in the project").option("--discover", "Fetch from server (always true for this command)").option("--save", "Save discovered types to ~/.pncli/config.json").action(async (opts) => {
6803
7353
  const start = Date.now();
6804
7354
  try {
@@ -6851,6 +7401,14 @@ function registerAdoWorkCommands(ado) {
6851
7401
  fail(err, "ado", "work-fields", start);
6852
7402
  }
6853
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
+ });
6854
7412
  }
6855
7413
 
6856
7414
  // src/services/ado/commands/repo.ts
@@ -6879,6 +7437,16 @@ function registerAdoRepoCommands(ado) {
6879
7437
  fail(err, "ado", "repo-list", start);
6880
7438
  }
6881
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
+ });
6882
7450
  repo.command("get").description("Get a specific repository").action(async () => {
6883
7451
  const start = Date.now();
6884
7452
  try {
@@ -7120,11 +7688,11 @@ function registerAdoRepoCommands(ado) {
7120
7688
  const start = Date.now();
7121
7689
  try {
7122
7690
  const globalOpts = ado.optsWithGlobals();
7123
- const { loadConfig: lc } = await import("./config-3QD5Y2O5.js");
7691
+ const { loadConfig: lc } = await import("./config-QN7KDHE4.js");
7124
7692
  const config = lc({ configPath: globalOpts.config });
7125
7693
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
7126
7694
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
7127
- const { createHttpClient: createHttpClient2 } = await import("./http-5WRBWEM6.js");
7695
+ const { createHttpClient: createHttpClient2 } = await import("./http-DDY5DLF6.js");
7128
7696
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
7129
7697
  const coreClient = new AdoCoreClient2(http);
7130
7698
  const me = await coreClient.getConnectionData(collection);
@@ -8270,6 +8838,8 @@ import os from "os";
8270
8838
  import { fileURLToPath } from "url";
8271
8839
  import { execFileSync as execFileSync3 } from "child_process";
8272
8840
  import select from "@inquirer/select";
8841
+ var BACK = "__back__";
8842
+ var ALL_MARKETPLACES = "__all_marketplaces__";
8273
8843
  function injectTokenIntoUrl(url, token) {
8274
8844
  let parsed;
8275
8845
  try {
@@ -8293,10 +8863,31 @@ function repoNameFromUrl(url) {
8293
8863
  function defaultMarketplacePath(url) {
8294
8864
  return path7.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
8295
8865
  }
8866
+ function marketplaceLabel(m) {
8867
+ return m.name ?? repoNameFromUrl(m.repoUrl ?? "");
8868
+ }
8869
+ function scrubToken(msg) {
8870
+ return msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@");
8871
+ }
8872
+ function assertInteractive(hint) {
8873
+ if (!process.stdin.isTTY) {
8874
+ throw new Error(`This selection requires an interactive terminal. ${hint}`);
8875
+ }
8876
+ }
8296
8877
  var AGENT_PATHS = {
8297
8878
  "github-copilot": { project: ".agents/skills", user: path7.join(os.homedir(), ".copilot/skills") },
8298
8879
  "claude-code": { project: ".claude/skills", user: path7.join(os.homedir(), ".claude/skills") }
8299
8880
  };
8881
+ function resolveAgentName(opts) {
8882
+ return opts.claude ? "claude-code" : opts.agent ?? "github-copilot";
8883
+ }
8884
+ function resolveAgentPaths(agentName) {
8885
+ const agentConfig = AGENT_PATHS[agentName];
8886
+ if (!agentConfig) {
8887
+ throw new Error(`Unknown agent: "${agentName}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
8888
+ }
8889
+ return agentConfig;
8890
+ }
8300
8891
  function getBundledSkillsDir() {
8301
8892
  try {
8302
8893
  const thisFile = fileURLToPath(import.meta.url);
@@ -8305,6 +8896,233 @@ function getBundledSkillsDir() {
8305
8896
  return "";
8306
8897
  }
8307
8898
  }
8899
+ function getInstalledMetaPath(targetDir) {
8900
+ return path7.join(targetDir, ".pncli-installed.json");
8901
+ }
8902
+ function readInstalledMeta(targetDir) {
8903
+ const metaPath = getInstalledMetaPath(targetDir);
8904
+ const raw = loadJsonFile(metaPath);
8905
+ if (raw && raw.version === 1 && raw.skills) return raw;
8906
+ return { version: 1, skills: {} };
8907
+ }
8908
+ function recordInstalledSkills(targetDir, skillNames, record) {
8909
+ if (skillNames.length === 0) return;
8910
+ const meta = readInstalledMeta(targetDir);
8911
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8912
+ for (const skillName of skillNames) {
8913
+ meta.skills[skillName] = { ...record, installedAt: now };
8914
+ }
8915
+ fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8916
+ }
8917
+ function getAllMarketplaces(globalConfig) {
8918
+ const result = [];
8919
+ if (Array.isArray(globalConfig.marketplaces)) {
8920
+ result.push(...globalConfig.marketplaces);
8921
+ }
8922
+ if (globalConfig.marketplace?.repoUrl) {
8923
+ const legacyUrl = globalConfig.marketplace.repoUrl;
8924
+ const alreadyPresent = result.some((m) => m.repoUrl === legacyUrl);
8925
+ if (!alreadyPresent) {
8926
+ const existingNames = new Set(result.map((m) => m.name).filter((n) => !!n));
8927
+ let legacyName = globalConfig.marketplace.name ?? repoNameFromUrl(legacyUrl);
8928
+ if (existingNames.has(legacyName)) {
8929
+ let suffix = 2;
8930
+ while (existingNames.has(`${legacyName}-${suffix}`)) suffix++;
8931
+ legacyName = `${legacyName}-${suffix}`;
8932
+ }
8933
+ result.push({
8934
+ name: legacyName,
8935
+ repoUrl: globalConfig.marketplace.repoUrl,
8936
+ localPath: globalConfig.marketplace.localPath,
8937
+ token: globalConfig.marketplace.token
8938
+ });
8939
+ }
8940
+ }
8941
+ return result;
8942
+ }
8943
+ function saveMarketplaces(configPath, existing, marketplaces) {
8944
+ const updated = { ...existing, marketplaces };
8945
+ delete updated.marketplace;
8946
+ writeGlobalConfig(updated, configPath);
8947
+ }
8948
+ function loadMarketplacesConfig(configPath) {
8949
+ const existing = loadJsonFile(configPath) ?? {};
8950
+ const all = getAllMarketplaces(existing);
8951
+ if (existing.marketplace?.repoUrl) {
8952
+ saveMarketplaces(configPath, existing, all);
8953
+ warn("Migrated legacy single-marketplace config to the multi-marketplace format.");
8954
+ delete existing.marketplace;
8955
+ }
8956
+ return { existing, all };
8957
+ }
8958
+ function loadMarketplaces(configPath) {
8959
+ return loadMarketplacesConfig(configPath).all;
8960
+ }
8961
+ function upsertMarketplace(all, entry) {
8962
+ const idxByUrl = all.findIndex((m) => m.repoUrl === entry.repoUrl);
8963
+ const idxByName = all.findIndex((m) => m.name === entry.name);
8964
+ if (idxByUrl !== -1) {
8965
+ if (idxByName !== -1 && idxByName !== idxByUrl) {
8966
+ throw new Error(`Marketplace name "${entry.name}" is already used by a different marketplace (${all[idxByName].repoUrl}). Choose a different --name.`);
8967
+ }
8968
+ all[idxByUrl] = { ...entry, token: entry.token ?? all[idxByUrl].token };
8969
+ } else if (idxByName !== -1) {
8970
+ throw new Error(`Marketplace name "${entry.name}" is already registered for a different repo (${all[idxByName].repoUrl}). Choose a different --name, or remove the existing marketplace first.`);
8971
+ } else {
8972
+ all.push(entry);
8973
+ }
8974
+ }
8975
+ function cloneOrReuseMarketplace(url, resolvedPath, opts) {
8976
+ const hasGit = fs4.existsSync(path7.join(resolvedPath, ".git"));
8977
+ if (fs4.existsSync(resolvedPath) && !hasGit && fs4.readdirSync(resolvedPath).length > 0) {
8978
+ throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
8979
+ }
8980
+ if (hasGit) {
8981
+ warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config and re-installing plugins.`);
8982
+ return;
8983
+ }
8984
+ const branchLabel = opts.branch ?? "remote default";
8985
+ warn(`Cloning ${url} (branch: ${branchLabel}) \u2192 ${resolvedPath}...`);
8986
+ const cloneUrl = opts.token ? injectTokenIntoUrl(url, opts.token) : url;
8987
+ const cloneArgs = ["clone"];
8988
+ if (opts.branch) cloneArgs.push("--branch", opts.branch);
8989
+ cloneArgs.push(cloneUrl, resolvedPath);
8990
+ try {
8991
+ execFileSync3("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"] });
8992
+ } catch (e) {
8993
+ let cloneActuallySucceeded = false;
8994
+ try {
8995
+ execFileSync3("git", ["-C", resolvedPath, "rev-parse", "HEAD"], { stdio: "pipe" });
8996
+ cloneActuallySucceeded = true;
8997
+ } catch {
8998
+ }
8999
+ if (!cloneActuallySucceeded) {
9000
+ const msg = e instanceof Error ? e.message : String(e);
9001
+ throw new Error(scrubToken(msg));
9002
+ }
9003
+ }
9004
+ }
9005
+ function pullMarketplace(marketplacePath, repoUrl, token, marketplaceName) {
9006
+ warn(`Pulling latest content for "${marketplaceName}"...`);
9007
+ const gitArgs = ["-C", marketplacePath];
9008
+ if (repoUrl && token) {
9009
+ gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, token)}`);
9010
+ }
9011
+ gitArgs.push("pull");
9012
+ let pullOutput;
9013
+ try {
9014
+ pullOutput = execFileSync3("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } });
9015
+ } catch (e) {
9016
+ const msg = e instanceof Error ? e.message : String(e);
9017
+ throw new Error(scrubToken(msg));
9018
+ }
9019
+ const updated = !pullOutput.includes("Already up to date");
9020
+ if (pullOutput.trim() && updated) warn(pullOutput.trim());
9021
+ warn(updated ? `"${marketplaceName}" updated.` : `"${marketplaceName}" already up to date \u2014 no changes to sync.`);
9022
+ return { updated };
9023
+ }
9024
+ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl, pluginNames, targetDir) {
9025
+ const results = {};
9026
+ let totalInstalled = 0;
9027
+ for (const pluginName of pluginNames) {
9028
+ const skillsSrc = resolveSkillsSrc(marketplacePath, pluginName);
9029
+ if (!fs4.existsSync(skillsSrc)) {
9030
+ results[pluginName] = { installed: [], failed: [] };
9031
+ warn(`No skills directory found for plugin "${pluginName}" in "${marketplaceName}" \u2014 skipping.`);
9032
+ continue;
9033
+ }
9034
+ const { installed, failed } = copyPluginSkills(skillsSrc, targetDir, {
9035
+ marketplace: marketplaceName,
9036
+ plugin: pluginName,
9037
+ installedFrom: repoUrl ?? ""
9038
+ });
9039
+ results[pluginName] = { installed, failed };
9040
+ totalInstalled += installed.length;
9041
+ for (const skill of installed) {
9042
+ warn(` ${skill}: ${path7.join(skillsSrc, skill)} \u2192 ${path7.join(targetDir, skill)}`);
9043
+ }
9044
+ if (failed.length > 0) {
9045
+ warn(`Skipped ${failed.length} skill(s) with invalid names in "${pluginName}": ${failed.join(", ")}`);
9046
+ }
9047
+ }
9048
+ return { results, totalInstalled };
9049
+ }
9050
+ function installAllPlugins(resolvedPath, marketplaceName, url, targetDir) {
9051
+ const pluginChoices = resolvePluginChoices(resolvedPath);
9052
+ if (pluginChoices.length === 0) {
9053
+ warn("No plugins found in marketplace. Check the marketplace repository structure.");
9054
+ return { pluginResults: {}, totalInstalled: 0 };
9055
+ }
9056
+ warn(`Installing ${pluginChoices.length} plugin(s) to ${targetDir}...`);
9057
+ const { results, totalInstalled } = installPluginsForMarketplace(resolvedPath, marketplaceName, url, pluginChoices.map((p) => p.name), targetDir);
9058
+ return { pluginResults: results, totalInstalled };
9059
+ }
9060
+ function syncMarketplacePlugins(m, targetDir, force, pluginFilter) {
9061
+ const marketplaceName = marketplaceLabel(m);
9062
+ try {
9063
+ const marketplacePath = m.localPath;
9064
+ if (!marketplacePath || !fs4.existsSync(marketplacePath)) {
9065
+ warn(`Marketplace "${marketplaceName}" local path not found \u2014 skipping.`);
9066
+ return { marketplace: marketplaceName, skipped: true, message: "Local path not found." };
9067
+ }
9068
+ const { updated } = pullMarketplace(marketplacePath, m.repoUrl, m.token, marketplaceName);
9069
+ if (!updated && !force) {
9070
+ return { marketplace: marketplaceName, marketplaceUpdated: false, skipped: true, message: "No changes detected \u2014 skipping install. Use --force to reinstall anyway." };
9071
+ }
9072
+ const pluginChoices = resolvePluginChoices(marketplacePath);
9073
+ if (pluginChoices.length === 0) {
9074
+ warn(`No plugins found in marketplace "${marketplaceName}" \u2014 skipping.`);
9075
+ return { marketplace: marketplaceName, skipped: true, message: "No plugins found." };
9076
+ }
9077
+ let pluginNames;
9078
+ if (pluginFilter === "all") {
9079
+ pluginNames = pluginChoices.map((p) => p.name);
9080
+ } else if (pluginChoices.some((p) => p.name === pluginFilter)) {
9081
+ pluginNames = [pluginFilter];
9082
+ } else {
9083
+ warn(`Plugin "${pluginFilter}" not found in "${marketplaceName}" \u2014 skipping.`);
9084
+ return { marketplace: marketplaceName, skipped: true, message: `Plugin "${pluginFilter}" not found.` };
9085
+ }
9086
+ const { results, totalInstalled } = installPluginsForMarketplace(marketplacePath, marketplaceName, m.repoUrl, pluginNames, targetDir);
9087
+ return { marketplace: marketplaceName, plugins: results, total: totalInstalled, marketplaceUpdated: updated };
9088
+ } catch (err) {
9089
+ const message = err instanceof Error ? err.message : String(err);
9090
+ warn(`Marketplace "${marketplaceName}" failed \u2014 skipping. ${message}`);
9091
+ return { marketplace: marketplaceName, skipped: true, error: message };
9092
+ }
9093
+ }
9094
+ async function marketplaceAddAction(url, localPath, opts, commandName) {
9095
+ const start = Date.now();
9096
+ try {
9097
+ const resolvedPath = path7.resolve(localPath ?? defaultMarketplacePath(url));
9098
+ const marketplaceName = opts.name ?? repoNameFromUrl(url);
9099
+ cloneOrReuseMarketplace(url, resolvedPath, opts);
9100
+ const configPath = getGlobalConfigPath();
9101
+ const { existing, all } = loadMarketplacesConfig(configPath);
9102
+ const entry = {
9103
+ name: marketplaceName,
9104
+ repoUrl: url,
9105
+ localPath: resolvedPath,
9106
+ ...opts.token ? { token: opts.token } : {}
9107
+ };
9108
+ upsertMarketplace(all, entry);
9109
+ saveMarketplaces(configPath, existing, all);
9110
+ const targetDir = resolveAgentPaths(resolveAgentName(opts)).user;
9111
+ const { pluginResults, totalInstalled } = installAllPlugins(resolvedPath, marketplaceName, url, targetDir);
9112
+ success({
9113
+ name: marketplaceName,
9114
+ repoUrl: url,
9115
+ localPath: resolvedPath,
9116
+ branch: opts.branch ?? null,
9117
+ tokenConfigured: !!opts.token,
9118
+ plugins: pluginResults,
9119
+ total: totalInstalled,
9120
+ target: targetDir
9121
+ }, "skills", commandName, start);
9122
+ } catch (err) {
9123
+ fail(err, "skills", commandName, start);
9124
+ }
9125
+ }
8308
9126
  function registerSkillsCommands(program2) {
8309
9127
  const skills = program2.command("skills").description("Manage pncli Claude Code skills");
8310
9128
  skills.command("install").description("Install pncli skills into the current repo").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts) => {
@@ -8314,10 +9132,7 @@ function registerSkillsCommands(program2) {
8314
9132
  if (opts.target) {
8315
9133
  targetDir = path7.resolve(opts.target);
8316
9134
  } else {
8317
- const agentConfig = AGENT_PATHS[opts.agent];
8318
- if (!agentConfig) {
8319
- throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
8320
- }
9135
+ const agentConfig = resolveAgentPaths(opts.agent);
8321
9136
  const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
8322
9137
  targetDir = path7.resolve(scopePath);
8323
9138
  }
@@ -8367,6 +9182,7 @@ function registerSkillsCommands(program2) {
8367
9182
  if (failed.length > 0) {
8368
9183
  warn(`Failed to install: ${failed.join(", ")}`);
8369
9184
  }
9185
+ recordInstalledSkills(targetDir, installed, { source: "bundled" });
8370
9186
  success({
8371
9187
  installed,
8372
9188
  failed,
@@ -8387,10 +9203,7 @@ function registerSkillsCommands(program2) {
8387
9203
  if (opts.target) {
8388
9204
  targetDir = path7.resolve(opts.target);
8389
9205
  } else {
8390
- const agentConfig = AGENT_PATHS[opts.agent];
8391
- if (!agentConfig) {
8392
- throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
8393
- }
9206
+ const agentConfig = resolveAgentPaths(opts.agent);
8394
9207
  const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
8395
9208
  targetDir = path7.resolve(scopePath);
8396
9209
  }
@@ -8398,7 +9211,9 @@ function registerSkillsCommands(program2) {
8398
9211
  success({ skills: [], message: `No skills directory found at ${targetDir}. Run: pncli skills install` }, "skills", "list", start);
8399
9212
  return;
8400
9213
  }
9214
+ const meta = readInstalledMeta(targetDir);
8401
9215
  const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
9216
+ if (name.startsWith(".")) return false;
8402
9217
  const skillPath = path7.join(targetDir, name, "SKILL.md");
8403
9218
  return fs4.existsSync(skillPath);
8404
9219
  });
@@ -8433,7 +9248,8 @@ function registerSkillsCommands(program2) {
8433
9248
  category: metadata.category || data.category || "other",
8434
9249
  services: metadata.services || data.services || "",
8435
9250
  providers: metadata.providers || data.providers || "none",
8436
- userInvocable: data["user-invocable"] === "true"
9251
+ userInvocable: data["user-invocable"] === "true",
9252
+ installed: meta.skills[name] ?? null
8437
9253
  };
8438
9254
  });
8439
9255
  success({ skills: skillsList, total: skillsList.length }, "skills", "list", start);
@@ -8441,193 +9257,217 @@ function registerSkillsCommands(program2) {
8441
9257
  fail(err, "skills", "list", start);
8442
9258
  }
8443
9259
  });
8444
- const marketplace = skills.command("marketplace").description("Manage a git-hosted skills marketplace");
8445
- marketplace.command("setup").description("Clone a skills marketplace repo, register it in global config, and install all plugins").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", "Target agent host for plugin install: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").action(async (url, localPath, opts) => {
9260
+ skills.command("uninstall").description("Uninstall a skill (defaults to user scope, matching the marketplace install target; pass --scope project for skills installed via `skills install`)").argument("<name>", "Skill name to uninstall (the directory name under your skills folder)").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope to uninstall from: project | user", "user").option("--claude", "Shorthand for --agent claude-code").option("--target <dir>", "Override skills directory").action((name, opts) => {
8446
9261
  const start = Date.now();
8447
9262
  try {
8448
- const resolvedPath = path7.resolve(localPath ?? defaultMarketplacePath(url));
8449
- const hasGit = fs4.existsSync(path7.join(resolvedPath, ".git"));
8450
- if (fs4.existsSync(resolvedPath) && !hasGit && fs4.readdirSync(resolvedPath).length > 0) {
8451
- throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
8452
- }
8453
- if (hasGit) {
8454
- warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config and re-installing plugins.`);
9263
+ let targetDir;
9264
+ if (opts.target) {
9265
+ targetDir = path7.resolve(opts.target);
8455
9266
  } else {
8456
- const branchLabel = opts.branch ?? "remote default";
8457
- warn(`Cloning ${url} (branch: ${branchLabel}) \u2192 ${resolvedPath}...`);
8458
- const cloneUrl = opts.token ? injectTokenIntoUrl(url, opts.token) : url;
8459
- const cloneArgs = ["clone"];
8460
- if (opts.branch) cloneArgs.push("--branch", opts.branch);
8461
- cloneArgs.push(cloneUrl, resolvedPath);
8462
- try {
8463
- execFileSync3("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"] });
8464
- } catch (e) {
8465
- const msg = e instanceof Error ? e.message : String(e);
8466
- throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
8467
- }
9267
+ const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9268
+ const scopePath = opts.scope === "project" ? agentConfig.project : agentConfig.user;
9269
+ targetDir = path7.resolve(scopePath);
8468
9270
  }
9271
+ const resolvedTarget = path7.resolve(targetDir);
9272
+ const skillDir = path7.resolve(targetDir, name);
9273
+ if (!skillDir.startsWith(resolvedTarget + path7.sep)) {
9274
+ throw new Error(`Invalid skill name: "${name}"`);
9275
+ }
9276
+ if (!fs4.existsSync(skillDir)) {
9277
+ throw new Error(`Skill "${name}" not found at ${skillDir}`);
9278
+ }
9279
+ const meta = readInstalledMeta(targetDir);
9280
+ const record = meta.skills[name];
9281
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9282
+ delete meta.skills[name];
9283
+ fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9284
+ success({
9285
+ uninstalled: name,
9286
+ target: targetDir,
9287
+ wasTracked: !!record,
9288
+ ...record ? { installedFrom: record } : {}
9289
+ }, "skills", "uninstall", start);
9290
+ } catch (err) {
9291
+ fail(err, "skills", "uninstall", start);
9292
+ }
9293
+ });
9294
+ const marketplace = skills.command("marketplace").description("Manage git-hosted skills marketplaces");
9295
+ function withMarketplaceAddOptions(cmd) {
9296
+ return cmd.argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", "Target agent host for plugin install: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code");
9297
+ }
9298
+ withMarketplaceAddOptions(
9299
+ marketplace.command("add").description("Register a new marketplace, clone it, and install all its plugins")
9300
+ ).action((url, localPath, opts) => marketplaceAddAction(url, localPath, opts, "marketplace-add"));
9301
+ withMarketplaceAddOptions(
9302
+ marketplace.command("setup").description("Alias for `marketplace add` \u2014 clone a marketplace and install all its plugins")
9303
+ ).action((url, localPath, opts) => marketplaceAddAction(url, localPath, opts, "marketplace-setup"));
9304
+ marketplace.command("list").description("List all registered marketplaces").action(() => {
9305
+ const start = Date.now();
9306
+ try {
8469
9307
  const configPath = getGlobalConfigPath();
8470
- const existing = loadJsonFile(configPath) ?? {};
8471
- existing.marketplace = { repoUrl: url, localPath: resolvedPath, ...opts.token ? { token: opts.token } : {} };
8472
- writeGlobalConfig(existing, configPath);
8473
- const agentName = opts.claude ? "claude-code" : opts.agent ?? "github-copilot";
8474
- const agentConfig = AGENT_PATHS[agentName];
8475
- if (!agentConfig) {
8476
- throw new Error(`Unknown agent: "${agentName}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
8477
- }
8478
- const targetDir = agentConfig.user;
8479
- const pluginChoices = resolvePluginChoices(resolvedPath);
8480
- const pluginResults = {};
8481
- let totalInstalled = 0;
8482
- if (pluginChoices.length === 0) {
8483
- warn("No plugins found in marketplace. Check the marketplace repository structure.");
8484
- } else {
8485
- warn(`Installing ${pluginChoices.length} plugin(s) to ${targetDir}...`);
8486
- for (const pluginChoice of pluginChoices) {
8487
- const skillsSrc = resolveSkillsSrc(resolvedPath, pluginChoice.name);
8488
- if (!fs4.existsSync(skillsSrc)) {
8489
- pluginResults[pluginChoice.name] = { installed: [], failed: [] };
8490
- warn(`No skills directory found for plugin "${pluginChoice.name}" \u2014 skipping.`);
8491
- continue;
8492
- }
8493
- const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
8494
- pluginResults[pluginChoice.name] = { installed, failed };
8495
- totalInstalled += installed.length;
8496
- for (const skill of installed) {
8497
- warn(` ${skill}: ${path7.join(skillsSrc, skill)} \u2192 ${path7.join(targetDir, skill)}`);
8498
- }
8499
- if (failed.length > 0) {
8500
- warn(`Skipped ${failed.length} skill(s) with invalid names in "${pluginChoice.name}": ${failed.join(", ")}`);
8501
- }
8502
- }
9308
+ const all = loadMarketplaces(configPath);
9309
+ success({
9310
+ marketplaces: all.map((m) => ({
9311
+ name: marketplaceLabel(m),
9312
+ repoUrl: m.repoUrl,
9313
+ localPath: m.localPath,
9314
+ tokenConfigured: !!m.token
9315
+ })),
9316
+ total: all.length
9317
+ }, "skills", "marketplace-list", start);
9318
+ } catch (err) {
9319
+ fail(err, "skills", "marketplace-list", start);
9320
+ }
9321
+ });
9322
+ marketplace.command("plugins").description("List the plugins available in a registered marketplace").argument("<name>", "Marketplace name (or repo URL) to inspect").action((name) => {
9323
+ const start = Date.now();
9324
+ try {
9325
+ const configPath = getGlobalConfigPath();
9326
+ const all = loadMarketplaces(configPath);
9327
+ const found = all.find((m) => m.name === name || m.repoUrl === name);
9328
+ if (!found) {
9329
+ throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
8503
9330
  }
9331
+ if (!found.localPath || !fs4.existsSync(found.localPath)) {
9332
+ throw new Error(`Marketplace "${marketplaceLabel(found)}" local path not found at ${found.localPath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
9333
+ }
9334
+ const plugins = resolvePluginChoices(found.localPath);
8504
9335
  success({
8505
- repoUrl: url,
8506
- localPath: resolvedPath,
8507
- branch: opts.branch ?? null,
8508
- tokenConfigured: !!opts.token,
8509
- plugins: pluginResults,
8510
- total: totalInstalled,
8511
- target: targetDir
8512
- }, "skills", "marketplace-setup", start);
9336
+ marketplace: marketplaceLabel(found),
9337
+ plugins,
9338
+ total: plugins.length
9339
+ }, "skills", "marketplace-plugins", start);
8513
9340
  } catch (err) {
8514
- fail(err, "skills", "marketplace-setup", start);
9341
+ fail(err, "skills", "marketplace-plugins", start);
8515
9342
  }
8516
9343
  });
8517
- marketplace.command("sync").description("Pull latest marketplace content and install a plugin's skills").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--force", "Force reinstall even if the marketplace repo has no new changes").action(async (plugin, opts) => {
9344
+ marketplace.command("remove").description("Remove a registered marketplace from the config (does not delete the local clone)").argument("<name>", "Name of the marketplace to remove").action((name) => {
8518
9345
  const start = Date.now();
8519
9346
  try {
8520
9347
  const configPath = getGlobalConfigPath();
8521
- const globalConfig = loadJsonFile(configPath) ?? {};
8522
- const marketplacePath = globalConfig.marketplace?.localPath;
8523
- if (!marketplacePath || !fs4.existsSync(marketplacePath)) {
8524
- throw new Error("Marketplace not configured. Run: pncli skills marketplace setup <url> <localPath>");
8525
- }
8526
- warn("Pulling latest marketplace content...");
8527
- const repoUrl = globalConfig.marketplace?.repoUrl;
8528
- const token = globalConfig.marketplace?.token;
8529
- const gitArgs = ["-C", marketplacePath];
8530
- if (repoUrl && token) {
8531
- gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, token)}`);
8532
- }
8533
- gitArgs.push("pull");
8534
- let pullOutput;
8535
- try {
8536
- pullOutput = execFileSync3("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } });
8537
- } catch (e) {
8538
- const msg = e instanceof Error ? e.message : String(e);
8539
- throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
8540
- }
8541
- const marketplaceUpdated = !pullOutput.includes("Already up to date");
8542
- if (pullOutput.trim() && marketplaceUpdated) {
8543
- warn(pullOutput.trim());
8544
- }
8545
- warn(marketplaceUpdated ? "Marketplace updated." : "Marketplace already up to date \u2014 no changes to sync.");
8546
- const pluginChoices = resolvePluginChoices(marketplacePath);
8547
- if (pluginChoices.length === 0) {
8548
- throw new Error("No plugins found in marketplace. Check the marketplace repository structure.");
8549
- }
8550
- const agentName = opts.claude ? "claude-code" : opts.agent ?? "github-copilot";
8551
- const agentConfig = AGENT_PATHS[agentName];
8552
- if (!agentConfig) {
8553
- throw new Error(`Unknown agent: "${agentName}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
8554
- }
8555
- const targetDir = agentConfig.user;
8556
- let selectedPlugin;
8557
- if (plugin) {
8558
- if (plugin !== "all" && !pluginChoices.some((p) => p.name === plugin)) {
8559
- throw new Error(`Plugin "${plugin}" not found. Available: ${pluginChoices.map((p) => p.name).join(", ")}`);
9348
+ const { existing, all } = loadMarketplacesConfig(configPath);
9349
+ const idx = all.findIndex((m) => m.name === name || m.repoUrl === name);
9350
+ if (idx === -1) {
9351
+ throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
9352
+ }
9353
+ const removed = all.splice(idx, 1)[0];
9354
+ saveMarketplaces(configPath, existing, all);
9355
+ success({
9356
+ removed: {
9357
+ name: marketplaceLabel(removed),
9358
+ repoUrl: removed.repoUrl,
9359
+ localPath: removed.localPath
9360
+ },
9361
+ remaining: all.length
9362
+ }, "skills", "marketplace-remove", start);
9363
+ } catch (err) {
9364
+ fail(err, "skills", "marketplace-remove", start);
9365
+ }
9366
+ });
9367
+ marketplace.command("sync").description("Pull latest marketplace content and install plugin skills").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').action(async (plugin, opts) => {
9368
+ const start = Date.now();
9369
+ try {
9370
+ const configPath = getGlobalConfigPath();
9371
+ const allMarketplaces = loadMarketplaces(configPath);
9372
+ if (allMarketplaces.length === 0) {
9373
+ throw new Error("No marketplaces configured. Run: pncli skills marketplace add <url>");
9374
+ }
9375
+ const targetDir = resolveAgentPaths(resolveAgentName(opts)).user;
9376
+ const force = opts.force ?? false;
9377
+ if (opts.marketplace === "all") {
9378
+ const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, plugin ?? "all"));
9379
+ success({ allMarketplaces: true, marketplaces: results, target: targetDir }, "skills", "marketplace-sync", start);
9380
+ return;
9381
+ }
9382
+ let selectedMarketplace;
9383
+ let selectedPlugin = plugin;
9384
+ const canGoBack = !opts.marketplace && allMarketplaces.length > 1;
9385
+ if (opts.marketplace) {
9386
+ const found = allMarketplaces.find((m) => m.name === opts.marketplace || m.repoUrl === opts.marketplace);
9387
+ if (!found) {
9388
+ throw new Error(`Marketplace "${opts.marketplace}" not found. Run: pncli skills marketplace list`);
8560
9389
  }
8561
- selectedPlugin = plugin;
8562
- } else {
8563
- selectedPlugin = await select({
8564
- message: "Select a plugin to install:",
8565
- choices: [
9390
+ selectedMarketplace = found;
9391
+ } else if (allMarketplaces.length === 1) {
9392
+ selectedMarketplace = allMarketplaces[0];
9393
+ }
9394
+ for (; ; ) {
9395
+ if (!selectedMarketplace) {
9396
+ assertInteractive(`Multiple marketplaces are registered (${allMarketplaces.map(marketplaceLabel).join(", ")}). Pass --marketplace <name> (or --marketplace all) to run this non-interactively.`);
9397
+ const chosen = await select({
9398
+ message: "Select a marketplace to sync:",
9399
+ choices: [
9400
+ { value: ALL_MARKETPLACES, name: "All marketplaces \u2014 sync every plugin from every marketplace" },
9401
+ // Index-based values: labels alone aren't guaranteed unique (two marketplaces can share a derived name).
9402
+ ...allMarketplaces.map((m, i) => ({ value: String(i), name: `${marketplaceLabel(m)} \u2014 ${m.repoUrl ?? ""}` }))
9403
+ ]
9404
+ });
9405
+ if (chosen === ALL_MARKETPLACES) {
9406
+ const results2 = allMarketplaces.map((m) => syncMarketplacePlugins(m, targetDir, force, selectedPlugin ?? "all"));
9407
+ success({ allMarketplaces: true, marketplaces: results2, target: targetDir }, "skills", "marketplace-sync", start);
9408
+ return;
9409
+ }
9410
+ selectedMarketplace = allMarketplaces[Number(chosen)];
9411
+ if (!selectedMarketplace) {
9412
+ throw new Error(`Marketplace "${chosen}" not found.`);
9413
+ }
9414
+ }
9415
+ const marketplaceName = marketplaceLabel(selectedMarketplace);
9416
+ const marketplacePath = selectedMarketplace.localPath;
9417
+ if (!marketplacePath || !fs4.existsSync(marketplacePath)) {
9418
+ throw new Error(`Marketplace "${marketplaceName}" local path not found at ${marketplacePath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
9419
+ }
9420
+ const { updated } = pullMarketplace(marketplacePath, selectedMarketplace.repoUrl, selectedMarketplace.token, marketplaceName);
9421
+ const pluginChoices = resolvePluginChoices(marketplacePath);
9422
+ if (pluginChoices.length === 0) {
9423
+ throw new Error(`No plugins found in marketplace "${marketplaceName}". Check the marketplace repository structure.`);
9424
+ }
9425
+ if (!selectedPlugin) {
9426
+ assertInteractive(`Pass a plugin name (or "all") to run this non-interactively.`);
9427
+ const choices = [
8566
9428
  { value: "all", name: "All \u2014 install every plugin" },
8567
- ...pluginChoices.map((p) => ({
8568
- value: p.name,
8569
- name: p.description ? `${p.name} \u2014 ${p.description}` : p.name
8570
- }))
8571
- ]
8572
- });
8573
- }
8574
- if (selectedPlugin === "all") {
8575
- if (!marketplaceUpdated && !opts.force) {
9429
+ ...pluginChoices.map((p) => ({ value: p.name, name: p.description ? `${p.name} \u2014 ${p.description}` : p.name }))
9430
+ ];
9431
+ if (canGoBack) {
9432
+ choices.push({ value: BACK, name: "\u2190 Back to marketplace selection" });
9433
+ }
9434
+ const chosen = await select({ message: `Select a plugin from "${marketplaceName}" to install:`, choices });
9435
+ if (chosen === BACK) {
9436
+ selectedMarketplace = void 0;
9437
+ continue;
9438
+ }
9439
+ selectedPlugin = chosen;
9440
+ } else if (selectedPlugin !== "all" && !pluginChoices.some((p) => p.name === selectedPlugin)) {
9441
+ throw new Error(`Plugin "${selectedPlugin}" not found in "${marketplaceName}". Available: ${pluginChoices.map((p) => p.name).join(", ")}`);
9442
+ }
9443
+ if (!updated && !force) {
8576
9444
  success({
8577
- marketplace: marketplacePath,
9445
+ marketplace: marketplaceName,
8578
9446
  marketplaceUpdated: false,
8579
9447
  updated: false,
8580
9448
  skipped: true,
8581
- message: "No marketplace changes detected \u2014 skipping install."
9449
+ message: `No changes detected in "${marketplaceName}" \u2014 skipping install. Use --force to reinstall anyway.`
8582
9450
  }, "skills", "marketplace-sync", start);
8583
9451
  return;
8584
9452
  }
8585
- const results = {};
8586
- let totalInstalled = 0;
8587
- for (const pluginChoice of pluginChoices) {
8588
- const skillsSrc2 = resolveSkillsSrc(marketplacePath, pluginChoice.name);
8589
- if (!fs4.existsSync(skillsSrc2)) {
8590
- results[pluginChoice.name] = { installed: [], failed: [] };
8591
- warn(`No skills directory found for plugin "${pluginChoice.name}" \u2014 skipping.`);
8592
- continue;
8593
- }
8594
- const { installed: installed2, failed: failed2 } = copyPluginSkills(skillsSrc2, targetDir);
8595
- results[pluginChoice.name] = { installed: installed2, failed: failed2 };
8596
- totalInstalled += installed2.length;
8597
- for (const skill of installed2) {
8598
- warn(` ${skill}: ${path7.join(skillsSrc2, skill)} \u2192 ${path7.join(targetDir, skill)}`);
8599
- }
8600
- if (failed2.length > 0) {
8601
- warn(`Skipped ${failed2.length} skill(s) with invalid names in "${pluginChoice.name}": ${failed2.join(", ")}`);
8602
- }
9453
+ const pluginNames = selectedPlugin === "all" ? pluginChoices.map((p) => p.name) : [selectedPlugin];
9454
+ const { results, totalInstalled } = installPluginsForMarketplace(marketplacePath, marketplaceName, selectedMarketplace.repoUrl, pluginNames, targetDir);
9455
+ if (selectedPlugin === "all") {
9456
+ success({ marketplace: marketplaceName, plugins: results, total: totalInstalled, target: targetDir, marketplaceUpdated: updated }, "skills", "marketplace-sync", start);
9457
+ } else {
9458
+ const single = results[selectedPlugin] ?? { installed: [], failed: [] };
9459
+ success({
9460
+ marketplace: marketplaceName,
9461
+ plugin: selectedPlugin,
9462
+ installed: single.installed,
9463
+ failed: single.failed,
9464
+ total: single.installed.length,
9465
+ target: targetDir,
9466
+ marketplaceUpdated: updated
9467
+ }, "skills", "marketplace-sync", start);
8603
9468
  }
8604
- success({
8605
- plugins: results,
8606
- total: totalInstalled,
8607
- target: targetDir,
8608
- marketplaceUpdated
8609
- }, "skills", "marketplace-sync", start);
8610
9469
  return;
8611
9470
  }
8612
- const skillsSrc = resolveSkillsSrc(marketplacePath, selectedPlugin);
8613
- if (!fs4.existsSync(skillsSrc)) {
8614
- throw new Error(`No skills directory found at ${skillsSrc}`);
8615
- }
8616
- const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
8617
- for (const skill of installed) {
8618
- warn(` ${skill}: ${path7.join(skillsSrc, skill)} \u2192 ${path7.join(targetDir, skill)}`);
8619
- }
8620
- if (failed.length > 0) {
8621
- warn(`Skipped ${failed.length} skill(s) with invalid names: ${failed.join(", ")}`);
8622
- }
8623
- success({
8624
- plugin: selectedPlugin,
8625
- installed,
8626
- failed,
8627
- total: installed.length,
8628
- target: targetDir,
8629
- marketplaceUpdated
8630
- }, "skills", "marketplace-sync", start);
8631
9471
  } catch (err) {
8632
9472
  fail(err, "skills", "marketplace-sync", start);
8633
9473
  }
@@ -8658,7 +9498,7 @@ function resolveSkillsSrc(marketplacePath, selectedPlugin) {
8658
9498
  }
8659
9499
  return skillsSrc;
8660
9500
  }
8661
- function copyPluginSkills(skillsSrc, targetDir) {
9501
+ function copyPluginSkills(skillsSrc, targetDir, meta) {
8662
9502
  fs4.mkdirSync(targetDir, { recursive: true });
8663
9503
  const resolvedTarget = path7.resolve(targetDir);
8664
9504
  const skillNames = fs4.readdirSync(skillsSrc).filter(
@@ -8676,6 +9516,14 @@ function copyPluginSkills(skillsSrc, targetDir) {
8676
9516
  fs4.cpSync(path7.join(skillsSrc, skillName), dest, { recursive: true });
8677
9517
  installed.push(skillName);
8678
9518
  }
9519
+ if (meta) {
9520
+ recordInstalledSkills(targetDir, installed, {
9521
+ source: "marketplace",
9522
+ marketplace: meta.marketplace,
9523
+ plugin: meta.plugin,
9524
+ installedFrom: meta.installedFrom
9525
+ });
9526
+ }
8679
9527
  return { installed, failed };
8680
9528
  }
8681
9529