@thanaen/ado-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/cli.js +200 -29
- package/package.json +1 -1
- package/skills/ado-workflows/SKILL.md +4 -0
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ On-prem / localserver example:
|
|
|
37
37
|
```bash
|
|
38
38
|
export DEVOPS_PAT="***"
|
|
39
39
|
export ADO_COLLECTION_URL="https://localserver/DefaultCollection"
|
|
40
|
-
export ADO_PROJECT="
|
|
41
|
-
export ADO_REPO="
|
|
40
|
+
export ADO_PROJECT="ExampleProject"
|
|
41
|
+
export ADO_REPO="Example Repository"
|
|
42
42
|
export ADO_INSECURE=1
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -59,6 +59,9 @@ ado smoke
|
|
|
59
59
|
|
|
60
60
|
## Commands
|
|
61
61
|
|
|
62
|
+
- `-v`, `--version`
|
|
63
|
+
- `init [--local]`
|
|
64
|
+
- `config`
|
|
62
65
|
- `smoke`
|
|
63
66
|
- `repos`
|
|
64
67
|
- `branches [repo]`
|
|
@@ -71,6 +74,7 @@ ado smoke
|
|
|
71
74
|
- `pr-get <id> [repo]`
|
|
72
75
|
- `pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456]`
|
|
73
76
|
- `pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]`
|
|
77
|
+
- `pr-cherry-pick <id> --target=... [--topic=branch-name] [--repo=...]`
|
|
74
78
|
- `pr-approve <id> [repo]`
|
|
75
79
|
- `pr-autocomplete <id> [repo]`
|
|
76
80
|
- `builds [top]`
|
package/dist/cli.js
CHANGED
|
@@ -57187,6 +57187,7 @@ import { homedir } from "node:os";
|
|
|
57187
57187
|
var DEFAULT_COLLECTION_URL = "https://dev.azure.com/<your-org>";
|
|
57188
57188
|
var DEFAULT_PROJECT = "<your-project>";
|
|
57189
57189
|
var DEFAULT_REPO = "<your-repository>";
|
|
57190
|
+
var LOCAL_CONFIG_FILENAME = "ado.json";
|
|
57190
57191
|
function isDefaultPlaceholder(value) {
|
|
57191
57192
|
return value.includes("<your-");
|
|
57192
57193
|
}
|
|
@@ -57211,23 +57212,46 @@ function loadFileConfig() {
|
|
|
57211
57212
|
return {};
|
|
57212
57213
|
}
|
|
57213
57214
|
}
|
|
57215
|
+
function getLocalConfigFilePath() {
|
|
57216
|
+
return join(process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
57217
|
+
}
|
|
57218
|
+
function loadLocalConfig() {
|
|
57219
|
+
const localPath = getLocalConfigFilePath();
|
|
57220
|
+
if (!existsSync(localPath)) {
|
|
57221
|
+
return {};
|
|
57222
|
+
}
|
|
57223
|
+
const content = readFileSync(localPath, "utf8");
|
|
57224
|
+
try {
|
|
57225
|
+
return JSON.parse(content);
|
|
57226
|
+
} catch {
|
|
57227
|
+
console.error(`Warning: could not parse local config file at ${localPath}. Please check the JSON syntax. Ignoring.`);
|
|
57228
|
+
return {};
|
|
57229
|
+
}
|
|
57230
|
+
}
|
|
57231
|
+
function censorPat(pat) {
|
|
57232
|
+
if (pat.length <= 8) {
|
|
57233
|
+
return "****";
|
|
57234
|
+
}
|
|
57235
|
+
return `${pat.slice(0, 4)}${"*".repeat(pat.length - 8)}${pat.slice(-4)}`;
|
|
57236
|
+
}
|
|
57214
57237
|
function getConfig() {
|
|
57215
57238
|
const fileConfig = loadFileConfig();
|
|
57216
|
-
const
|
|
57239
|
+
const localConfig = loadLocalConfig();
|
|
57240
|
+
const pat = process.env.DEVOPS_PAT ?? localConfig.pat ?? fileConfig.pat;
|
|
57217
57241
|
if (!pat) {
|
|
57218
57242
|
console.error("Missing DEVOPS_PAT environment variable or pat in config file.");
|
|
57219
57243
|
console.error(`Run "ado init" to create a config file at ${getConfigFilePath()}`);
|
|
57220
57244
|
process.exit(1);
|
|
57221
57245
|
}
|
|
57222
|
-
const collectionUrl = process.env.ADO_COLLECTION_URL ?? fileConfig.collectionUrl ?? DEFAULT_COLLECTION_URL;
|
|
57223
|
-
const project = process.env.ADO_PROJECT ?? fileConfig.project ?? DEFAULT_PROJECT;
|
|
57224
|
-
const repo = process.env.ADO_REPO ?? fileConfig.repo ?? DEFAULT_REPO;
|
|
57246
|
+
const collectionUrl = process.env.ADO_COLLECTION_URL ?? localConfig.collectionUrl ?? fileConfig.collectionUrl ?? DEFAULT_COLLECTION_URL;
|
|
57247
|
+
const project = process.env.ADO_PROJECT ?? localConfig.project ?? fileConfig.project ?? DEFAULT_PROJECT;
|
|
57248
|
+
const repo = process.env.ADO_REPO ?? localConfig.repo ?? fileConfig.repo ?? DEFAULT_REPO;
|
|
57225
57249
|
if (isDefaultPlaceholder(collectionUrl) || isDefaultPlaceholder(project) || isDefaultPlaceholder(repo)) {
|
|
57226
57250
|
console.error("ADO configuration is incomplete. Set ADO_COLLECTION_URL, ADO_PROJECT, and ADO_REPO.");
|
|
57227
57251
|
console.error(`You can also run "ado init" to create a config file at ${getConfigFilePath()}`);
|
|
57228
57252
|
process.exit(1);
|
|
57229
57253
|
}
|
|
57230
|
-
const insecure = process.env.ADO_INSECURE === "1" || fileConfig.insecure === true;
|
|
57254
|
+
const insecure = process.env.ADO_INSECURE === "1" || localConfig.insecure === true || fileConfig.insecure === true;
|
|
57231
57255
|
if (insecure) {
|
|
57232
57256
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
57233
57257
|
}
|
|
@@ -57341,6 +57365,42 @@ function buildRecentWorkItemsWiql(filters = {}) {
|
|
|
57341
57365
|
return `SELECT [System.Id] FROM WorkItems${whereClause} ORDER BY [System.ChangedDate] DESC`;
|
|
57342
57366
|
}
|
|
57343
57367
|
|
|
57368
|
+
// src/cherry-pick.ts
|
|
57369
|
+
function parseCherryPickArgs(args) {
|
|
57370
|
+
const kv = Object.fromEntries(args.filter((a) => a.startsWith("--")).map((arg) => {
|
|
57371
|
+
const [k, ...rest] = arg.split("=");
|
|
57372
|
+
return [k.replace(/^--/, ""), rest.join("=")];
|
|
57373
|
+
}));
|
|
57374
|
+
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
57375
|
+
const prId = Number(positionals[0]);
|
|
57376
|
+
if (!Number.isFinite(prId) || prId <= 0) {
|
|
57377
|
+
throw new Error("A valid pull request ID is required as the first argument.");
|
|
57378
|
+
}
|
|
57379
|
+
const target = kv.target;
|
|
57380
|
+
if (!target || target.trim().length === 0) {
|
|
57381
|
+
throw new Error("--target is required.");
|
|
57382
|
+
}
|
|
57383
|
+
const allowedOptions = new Set(["target", "topic", "repo"]);
|
|
57384
|
+
for (const key of Object.keys(kv)) {
|
|
57385
|
+
if (!allowedOptions.has(key)) {
|
|
57386
|
+
throw new Error(`Unknown option: --${key}`);
|
|
57387
|
+
}
|
|
57388
|
+
}
|
|
57389
|
+
return {
|
|
57390
|
+
prId,
|
|
57391
|
+
target: target.trim(),
|
|
57392
|
+
topic: kv.topic?.trim() || undefined,
|
|
57393
|
+
repo: kv.repo?.trim() || undefined
|
|
57394
|
+
};
|
|
57395
|
+
}
|
|
57396
|
+
function buildGeneratedRefName(prId, target, topic) {
|
|
57397
|
+
if (topic) {
|
|
57398
|
+
return topic.startsWith("refs/heads/") ? topic : `refs/heads/${topic}`;
|
|
57399
|
+
}
|
|
57400
|
+
const safeBranch = target.replace(/^refs\/heads\//, "");
|
|
57401
|
+
return `refs/heads/cherry-pick-pr-${prId}-onto-${safeBranch}`;
|
|
57402
|
+
}
|
|
57403
|
+
|
|
57344
57404
|
// src/cli.ts
|
|
57345
57405
|
var import_GitInterfaces = __toESM(require_GitInterfaces(), 1);
|
|
57346
57406
|
var import_WorkItemTrackingInterfaces = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
@@ -57766,43 +57826,137 @@ async function cmdPrUpdate(config, idRaw, args) {
|
|
|
57766
57826
|
await linkWorkItemsToPr(config, repo, updated, workItemIds);
|
|
57767
57827
|
}
|
|
57768
57828
|
}
|
|
57769
|
-
async function
|
|
57829
|
+
async function cmdPrCherryPick(config, args) {
|
|
57830
|
+
const usage = "Usage: pr-cherry-pick <id> --target=main [--topic=branch-name] [--repo=...]";
|
|
57831
|
+
let parsed;
|
|
57832
|
+
try {
|
|
57833
|
+
parsed = parseCherryPickArgs(args);
|
|
57834
|
+
} catch (error) {
|
|
57835
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
57836
|
+
console.error(usage);
|
|
57837
|
+
process.exit(1);
|
|
57838
|
+
}
|
|
57839
|
+
const repo = pickRepo(config, parsed.repo);
|
|
57840
|
+
const gitApi = await config.connection.getGitApi();
|
|
57841
|
+
const repository = await gitApi.getRepository(repo, config.project);
|
|
57842
|
+
if (!repository?.id) {
|
|
57843
|
+
console.error(`Repository "${repo}" not found.`);
|
|
57844
|
+
process.exit(1);
|
|
57845
|
+
}
|
|
57846
|
+
const targetRef = parsed.target.startsWith("refs/") ? parsed.target : `refs/heads/${parsed.target}`;
|
|
57847
|
+
const generatedRefName = buildGeneratedRefName(parsed.prId, parsed.target, parsed.topic);
|
|
57848
|
+
const cherryPick = await gitApi.createCherryPick({
|
|
57849
|
+
source: { pullRequestId: parsed.prId },
|
|
57850
|
+
ontoRefName: targetRef,
|
|
57851
|
+
generatedRefName,
|
|
57852
|
+
repository: { id: repository.id }
|
|
57853
|
+
}, config.project, repository.id);
|
|
57854
|
+
let status = cherryPick.status;
|
|
57855
|
+
const cherryPickId = cherryPick.cherryPickId;
|
|
57856
|
+
if (cherryPickId == null) {
|
|
57857
|
+
console.error("Cherry-pick operation could not be started.");
|
|
57858
|
+
process.exit(1);
|
|
57859
|
+
}
|
|
57860
|
+
const MAX_POLL_ATTEMPTS = 30;
|
|
57861
|
+
const POLL_INTERVAL_MS = 1000;
|
|
57862
|
+
for (let attempt = 0;attempt < MAX_POLL_ATTEMPTS && (status === import_GitInterfaces.GitAsyncOperationStatus.Queued || status === import_GitInterfaces.GitAsyncOperationStatus.InProgress); attempt++) {
|
|
57863
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
57864
|
+
const result = await gitApi.getCherryPick(config.project, cherryPickId, repository.id);
|
|
57865
|
+
status = result.status;
|
|
57866
|
+
}
|
|
57867
|
+
if (status === import_GitInterfaces.GitAsyncOperationStatus.Completed) {
|
|
57868
|
+
console.log(`Cherry-pick of PR #${parsed.prId} completed. Branch created: ${generatedRefName.replace("refs/heads/", "")}`);
|
|
57869
|
+
} else if (status === import_GitInterfaces.GitAsyncOperationStatus.Failed) {
|
|
57870
|
+
console.error(`Cherry-pick of PR #${parsed.prId} failed.`);
|
|
57871
|
+
process.exit(1);
|
|
57872
|
+
} else {
|
|
57873
|
+
console.error(`Cherry-pick of PR #${parsed.prId} did not complete in time (status: ${import_GitInterfaces.GitAsyncOperationStatus[status ?? 0] ?? "unknown"}).`);
|
|
57874
|
+
process.exit(1);
|
|
57875
|
+
}
|
|
57876
|
+
}
|
|
57877
|
+
async function cmdInit(args) {
|
|
57878
|
+
const isLocal = args.includes("--local");
|
|
57770
57879
|
const { createInterface } = await import("node:readline/promises");
|
|
57771
57880
|
const { mkdirSync, writeFileSync, existsSync: existsSync2 } = await import("node:fs");
|
|
57772
57881
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
57773
|
-
const existing = loadFileConfig();
|
|
57774
|
-
const configPath = getConfigFilePath();
|
|
57775
|
-
console.log(
|
|
57882
|
+
const existing = isLocal ? loadLocalConfig() : loadFileConfig();
|
|
57883
|
+
const configPath = isLocal ? getLocalConfigFilePath() : getConfigFilePath();
|
|
57884
|
+
console.log(`Azure DevOps CLI — ${isLocal ? "Local " : ""}Configuration`);
|
|
57776
57885
|
console.log(`Config file: ${configPath}`);
|
|
57777
57886
|
console.log(`Press Enter to keep existing values shown in brackets.
|
|
57778
57887
|
`);
|
|
57779
|
-
|
|
57780
|
-
|
|
57781
|
-
|
|
57782
|
-
|
|
57783
|
-
|
|
57784
|
-
|
|
57785
|
-
|
|
57786
|
-
|
|
57787
|
-
|
|
57888
|
+
if (isLocal) {
|
|
57889
|
+
const collectionUrl = await rl.question(`Collection URL${existing.collectionUrl ? ` [${existing.collectionUrl}]` : ""}: `) || existing.collectionUrl || "";
|
|
57890
|
+
const project = await rl.question(`Project${existing.project ? ` [${existing.project}]` : ""}: `) || existing.project || "";
|
|
57891
|
+
const repo = await rl.question(`Repository${existing.repo ? ` [${existing.repo}]` : ""}: `) || existing.repo || "";
|
|
57892
|
+
rl.close();
|
|
57893
|
+
const config = {};
|
|
57894
|
+
if (collectionUrl)
|
|
57895
|
+
config.collectionUrl = collectionUrl;
|
|
57896
|
+
if (project)
|
|
57897
|
+
config.project = project;
|
|
57898
|
+
if (repo)
|
|
57899
|
+
config.repo = repo;
|
|
57900
|
+
if (Object.keys(config).length === 0) {
|
|
57901
|
+
console.error(`
|
|
57902
|
+
At least one field must be provided for local config.`);
|
|
57903
|
+
process.exit(1);
|
|
57904
|
+
}
|
|
57905
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
57906
|
+
`, "utf8");
|
|
57907
|
+
console.log(`
|
|
57908
|
+
Local configuration saved to ${configPath}`);
|
|
57909
|
+
} else {
|
|
57910
|
+
const pat = await rl.question(`Personal Access Token (PAT)${existing.pat ? " [****]" : ""}: `) || existing.pat || "";
|
|
57911
|
+
const collectionUrl = await rl.question(`Collection URL${existing.collectionUrl ? ` [${existing.collectionUrl}]` : ""}: `) || existing.collectionUrl || "";
|
|
57912
|
+
const project = await rl.question(`Project${existing.project ? ` [${existing.project}]` : ""}: `) || existing.project || "";
|
|
57913
|
+
const repo = await rl.question(`Repository${existing.repo ? ` [${existing.repo}]` : ""}: `) || existing.repo || "";
|
|
57914
|
+
const insecureInput = await rl.question(`Disable TLS verification (insecure)? (y/N)${existing.insecure ? " [y]" : ""}: `) || (existing.insecure ? "y" : "n");
|
|
57915
|
+
const insecure = insecureInput.toLowerCase() === "y" || insecureInput === "1";
|
|
57916
|
+
rl.close();
|
|
57917
|
+
if (!pat || !collectionUrl || !project || !repo) {
|
|
57918
|
+
console.error(`
|
|
57788
57919
|
All fields (PAT, Collection URL, Project, Repository) are required.`);
|
|
57789
|
-
|
|
57790
|
-
|
|
57791
|
-
|
|
57792
|
-
|
|
57793
|
-
|
|
57794
|
-
|
|
57795
|
-
|
|
57796
|
-
|
|
57920
|
+
process.exit(1);
|
|
57921
|
+
}
|
|
57922
|
+
const config = { pat, collectionUrl, project, repo, insecure };
|
|
57923
|
+
const configDir = getConfigDir();
|
|
57924
|
+
if (!existsSync2(configDir)) {
|
|
57925
|
+
mkdirSync(configDir, { recursive: true });
|
|
57926
|
+
}
|
|
57927
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
57797
57928
|
`, "utf8");
|
|
57798
|
-
|
|
57929
|
+
console.log(`
|
|
57799
57930
|
Configuration saved to ${configPath}`);
|
|
57931
|
+
}
|
|
57932
|
+
}
|
|
57933
|
+
function cmdConfig() {
|
|
57934
|
+
const fileConfig = loadFileConfig();
|
|
57935
|
+
const localConfig = loadLocalConfig();
|
|
57936
|
+
const pat = process.env.DEVOPS_PAT ?? localConfig.pat ?? fileConfig.pat;
|
|
57937
|
+
const collectionUrl = process.env.ADO_COLLECTION_URL ?? localConfig.collectionUrl ?? fileConfig.collectionUrl;
|
|
57938
|
+
const project = process.env.ADO_PROJECT ?? localConfig.project ?? fileConfig.project;
|
|
57939
|
+
const repo = process.env.ADO_REPO ?? localConfig.repo ?? fileConfig.repo;
|
|
57940
|
+
const insecure = process.env.ADO_INSECURE === "1" || localConfig.insecure === true || fileConfig.insecure === true;
|
|
57941
|
+
console.log(JSON.stringify({
|
|
57942
|
+
pat: pat ? censorPat(pat) : undefined,
|
|
57943
|
+
collectionUrl: collectionUrl ?? undefined,
|
|
57944
|
+
project: project ?? undefined,
|
|
57945
|
+
repo: repo ?? undefined,
|
|
57946
|
+
insecure,
|
|
57947
|
+
sources: {
|
|
57948
|
+
globalConfig: getConfigFilePath(),
|
|
57949
|
+
localConfig: getLocalConfigFilePath()
|
|
57950
|
+
}
|
|
57951
|
+
}, null, 2));
|
|
57800
57952
|
}
|
|
57801
57953
|
function printHelp() {
|
|
57802
57954
|
console.log(`Azure DevOps CLI
|
|
57803
57955
|
|
|
57804
57956
|
Commands:
|
|
57805
|
-
|
|
57957
|
+
-v, --version
|
|
57958
|
+
init [--local]
|
|
57959
|
+
config
|
|
57806
57960
|
smoke
|
|
57807
57961
|
repos
|
|
57808
57962
|
branches [repo]
|
|
@@ -57815,19 +57969,33 @@ Commands:
|
|
|
57815
57969
|
pr-get <id> [repo]
|
|
57816
57970
|
pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456]
|
|
57817
57971
|
pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]
|
|
57972
|
+
pr-cherry-pick <id> --target=... [--topic=branch-name] [--repo=...]
|
|
57818
57973
|
pr-approve <id> [repo]
|
|
57819
57974
|
pr-autocomplete <id> [repo]
|
|
57820
57975
|
builds [top]
|
|
57821
57976
|
`);
|
|
57822
57977
|
}
|
|
57978
|
+
async function printVersion() {
|
|
57979
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
57980
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
57981
|
+
console.log(pkg.version);
|
|
57982
|
+
}
|
|
57823
57983
|
async function main() {
|
|
57824
57984
|
const [command = "smoke", ...args] = process.argv.slice(2);
|
|
57825
57985
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
57826
57986
|
printHelp();
|
|
57827
57987
|
return;
|
|
57828
57988
|
}
|
|
57989
|
+
if (command === "-v" || command === "--version") {
|
|
57990
|
+
await printVersion();
|
|
57991
|
+
return;
|
|
57992
|
+
}
|
|
57829
57993
|
if (command === "init") {
|
|
57830
|
-
await cmdInit();
|
|
57994
|
+
await cmdInit(args);
|
|
57995
|
+
return;
|
|
57996
|
+
}
|
|
57997
|
+
if (command === "config") {
|
|
57998
|
+
cmdConfig();
|
|
57831
57999
|
return;
|
|
57832
58000
|
}
|
|
57833
58001
|
const config = getConfig();
|
|
@@ -57868,6 +58036,9 @@ async function main() {
|
|
|
57868
58036
|
case "pr-update":
|
|
57869
58037
|
await cmdPrUpdate(config, args[0], args.slice(1));
|
|
57870
58038
|
break;
|
|
58039
|
+
case "pr-cherry-pick":
|
|
58040
|
+
await cmdPrCherryPick(config, args);
|
|
58041
|
+
break;
|
|
57871
58042
|
case "pr-approve":
|
|
57872
58043
|
await cmdPrApprove(config, args[0], args[1]);
|
|
57873
58044
|
break;
|
package/package.json
CHANGED
|
@@ -19,6 +19,9 @@ If auth is missing, stop and ask for `DEVOPS_PAT`.
|
|
|
19
19
|
|
|
20
20
|
## Core commands
|
|
21
21
|
|
|
22
|
+
- Initialize global config: `ado init`
|
|
23
|
+
- Initialize local (per-project) config: `ado init --local`
|
|
24
|
+
- Show resolved config: `ado config`
|
|
22
25
|
- List repos: `ado repos`
|
|
23
26
|
- List branches: `ado branches "MyRepo"`
|
|
24
27
|
- Get work item: `ado workitem-get <id>`
|
|
@@ -32,6 +35,7 @@ If auth is missing, stop and ask for `DEVOPS_PAT`.
|
|
|
32
35
|
- Get PR: `ado pr-get <id> "MyRepo"`
|
|
33
36
|
- Create PR: `ado pr-create --title="..." --source="feature/x" --target="develop" --description="..." --repo="MyRepo" --work-items=123,456`
|
|
34
37
|
- Update PR: `ado pr-update <id> --title="..." --description="..." --repo="MyRepo" --work-items=123,456`
|
|
38
|
+
- Cherry-pick PR onto another branch: `ado pr-cherry-pick <id> --target="main" --topic="cherry-pick-branch" --repo="MyRepo"`
|
|
35
39
|
- Approve PR: `ado pr-approve <id> "MyRepo"`
|
|
36
40
|
- Enable auto-complete: `ado pr-autocomplete <id> "MyRepo"`
|
|
37
41
|
- List builds: `ado builds 10`
|