@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/copilot-instructions.md +89 -10
- package/dist/{chunk-P5NWDQ2B.js → chunk-IVUDXPDP.js} +16 -5
- package/dist/chunk-IVUDXPDP.js.map +1 -0
- package/dist/{chunk-XPPEXJBP.js → chunk-WX4ZRHIC.js} +33 -7
- package/dist/chunk-WX4ZRHIC.js.map +1 -0
- package/dist/cli.js +1097 -249
- package/dist/cli.js.map +1 -1
- package/dist/{config-3QD5Y2O5.js → config-QN7KDHE4.js} +2 -2
- package/dist/{http-5WRBWEM6.js → http-DDY5DLF6.js} +2 -2
- package/package.json +1 -1
- package/skills/pncli/SKILL.md +5 -1
- package/skills/pncli/ado.md +19 -1
- package/skills/pncli/bitbucket.md +11 -1
- package/skills/pncli/confluence.md +5 -1
- package/skills/pncli/github.md +11 -1
- package/skills/pncli/jenkins.md +22 -0
- package/skills/pncli/jira.md +26 -2
- package/skills/pncli/marketplace.md +47 -7
- package/dist/chunk-P5NWDQ2B.js.map +0 -1
- package/dist/chunk-XPPEXJBP.js.map +0 -1
- /package/dist/{config-3QD5Y2O5.js.map → config-QN7KDHE4.js.map} +0 -0
- /package/dist/{http-5WRBWEM6.js.map → http-DDY5DLF6.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
setRepoConfigValue,
|
|
9
9
|
writeGlobalConfig,
|
|
10
10
|
writeRepoConfig
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-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-
|
|
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
|
|
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, ...)").
|
|
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
|
|
1222
|
-
const
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
|
1228
|
-
const
|
|
1229
|
-
|
|
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
|
|
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
|
|
1244
|
-
|
|
1245
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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").
|
|
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:
|
|
2802
|
+
body: bodyContent,
|
|
2415
2803
|
parentId: opts.parentId,
|
|
2416
|
-
representation
|
|
2804
|
+
representation
|
|
2417
2805
|
});
|
|
2418
2806
|
success(data, "confluence", "create-page", start);
|
|
2419
2807
|
} catch (err) {
|
|
2420
|
-
|
|
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:
|
|
2834
|
+
body: bodyContent,
|
|
2433
2835
|
status: opts.status,
|
|
2434
|
-
representation
|
|
2836
|
+
representation
|
|
2435
2837
|
});
|
|
2436
2838
|
success(data, "confluence", "update-page", start);
|
|
2437
2839
|
} catch (err) {
|
|
2438
|
-
|
|
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
|
|
4844
|
-
import { basename as
|
|
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 =
|
|
4950
|
-
const fileName =
|
|
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
|
|
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
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
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").
|
|
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":
|
|
6626
|
-
...
|
|
6627
|
-
...
|
|
6628
|
-
...
|
|
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 =
|
|
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,
|
|
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
|
|
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-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
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
|
|
8457
|
-
|
|
8458
|
-
|
|
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
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
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
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
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-
|
|
9341
|
+
fail(err, "skills", "marketplace-plugins", start);
|
|
8515
9342
|
}
|
|
8516
9343
|
});
|
|
8517
|
-
marketplace.command("
|
|
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
|
|
8522
|
-
const
|
|
8523
|
-
if (
|
|
8524
|
-
throw new Error(
|
|
8525
|
-
}
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
}
|
|
8555
|
-
|
|
8556
|
-
let selectedPlugin;
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
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
|
-
|
|
8562
|
-
} else {
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
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
|
-
|
|
8569
|
-
|
|
8570
|
-
})
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
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:
|
|
9445
|
+
marketplace: marketplaceName,
|
|
8578
9446
|
marketplaceUpdated: false,
|
|
8579
9447
|
updated: false,
|
|
8580
9448
|
skipped: true,
|
|
8581
|
-
message:
|
|
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
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
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
|
|