@thanaen/ado-cli 0.2.0 → 0.4.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 +8 -4
- package/dist/cli.js +277 -35
- package/package.json +1 -1
- package/skills/ado-workflows/SKILL.md +8 -9
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]`
|
|
@@ -69,8 +72,9 @@ ado smoke
|
|
|
69
72
|
- `workitem-comment-update <id> <commentId> --text="..." [--file=path]`
|
|
70
73
|
- `prs [status] [top] [repo]`
|
|
71
74
|
- `pr-get <id> [repo]`
|
|
72
|
-
- `pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456]`
|
|
73
|
-
- `pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]`
|
|
75
|
+
- `pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]`
|
|
76
|
+
- `pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]`
|
|
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
|
}
|
|
@@ -57249,6 +57273,12 @@ function parseWorkItemIds(rawValue) {
|
|
|
57249
57273
|
const ids = rawValue.split(",").map((part) => Number(part.trim())).filter((id) => Number.isFinite(id) && id > 0);
|
|
57250
57274
|
return [...new Set(ids)];
|
|
57251
57275
|
}
|
|
57276
|
+
function parsePrTags(rawValue) {
|
|
57277
|
+
if (!rawValue)
|
|
57278
|
+
return [];
|
|
57279
|
+
const tags = rawValue.split(",").map((part) => part.trim()).filter((tag) => tag.length > 0);
|
|
57280
|
+
return [...new Set(tags)];
|
|
57281
|
+
}
|
|
57252
57282
|
function buildPullRequestArtifactUrl(pr) {
|
|
57253
57283
|
const projectId = pr?.repository?.project?.id;
|
|
57254
57284
|
const repoId = pr?.repository?.id;
|
|
@@ -57341,6 +57371,42 @@ function buildRecentWorkItemsWiql(filters = {}) {
|
|
|
57341
57371
|
return `SELECT [System.Id] FROM WorkItems${whereClause} ORDER BY [System.ChangedDate] DESC`;
|
|
57342
57372
|
}
|
|
57343
57373
|
|
|
57374
|
+
// src/cherry-pick.ts
|
|
57375
|
+
function parseCherryPickArgs(args) {
|
|
57376
|
+
const kv = Object.fromEntries(args.filter((a) => a.startsWith("--")).map((arg) => {
|
|
57377
|
+
const [k, ...rest] = arg.split("=");
|
|
57378
|
+
return [k.replace(/^--/, ""), rest.join("=")];
|
|
57379
|
+
}));
|
|
57380
|
+
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
57381
|
+
const prId = Number(positionals[0]);
|
|
57382
|
+
if (!Number.isFinite(prId) || prId <= 0) {
|
|
57383
|
+
throw new Error("A valid pull request ID is required as the first argument.");
|
|
57384
|
+
}
|
|
57385
|
+
const target = kv.target;
|
|
57386
|
+
if (!target || target.trim().length === 0) {
|
|
57387
|
+
throw new Error("--target is required.");
|
|
57388
|
+
}
|
|
57389
|
+
const allowedOptions = new Set(["target", "topic", "repo"]);
|
|
57390
|
+
for (const key of Object.keys(kv)) {
|
|
57391
|
+
if (!allowedOptions.has(key)) {
|
|
57392
|
+
throw new Error(`Unknown option: --${key}`);
|
|
57393
|
+
}
|
|
57394
|
+
}
|
|
57395
|
+
return {
|
|
57396
|
+
prId,
|
|
57397
|
+
target: target.trim(),
|
|
57398
|
+
topic: kv.topic?.trim() || undefined,
|
|
57399
|
+
repo: kv.repo?.trim() || undefined
|
|
57400
|
+
};
|
|
57401
|
+
}
|
|
57402
|
+
function buildGeneratedRefName(prId, target, topic) {
|
|
57403
|
+
if (topic) {
|
|
57404
|
+
return topic.startsWith("refs/heads/") ? topic : `refs/heads/${topic}`;
|
|
57405
|
+
}
|
|
57406
|
+
const safeBranch = target.replace(/^refs\/heads\//, "");
|
|
57407
|
+
return `refs/heads/cherry-pick-pr-${prId}-onto-${safeBranch}`;
|
|
57408
|
+
}
|
|
57409
|
+
|
|
57344
57410
|
// src/cli.ts
|
|
57345
57411
|
var import_GitInterfaces = __toESM(require_GitInterfaces(), 1);
|
|
57346
57412
|
var import_WorkItemTrackingInterfaces = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
@@ -57398,6 +57464,40 @@ async function cmdSmoke(config) {
|
|
|
57398
57464
|
console.log("Pull request: none found");
|
|
57399
57465
|
}
|
|
57400
57466
|
}
|
|
57467
|
+
async function cmdStatus() {
|
|
57468
|
+
const localConfig = loadLocalConfig();
|
|
57469
|
+
const fileConfig = loadFileConfig();
|
|
57470
|
+
const pat = process.env.DEVOPS_PAT ?? localConfig.pat ?? fileConfig.pat;
|
|
57471
|
+
if (!pat) {
|
|
57472
|
+
console.error("Not ready: DEVOPS_PAT is not set");
|
|
57473
|
+
process.exit(1);
|
|
57474
|
+
}
|
|
57475
|
+
const collectionUrl = process.env.ADO_COLLECTION_URL ?? localConfig.collectionUrl ?? fileConfig.collectionUrl ?? "";
|
|
57476
|
+
const project = process.env.ADO_PROJECT ?? localConfig.project ?? fileConfig.project ?? "";
|
|
57477
|
+
const repo = process.env.ADO_REPO ?? localConfig.repo ?? fileConfig.repo ?? "";
|
|
57478
|
+
const hasPlaceholder = (v) => v.includes("<your-");
|
|
57479
|
+
if (!collectionUrl || hasPlaceholder(collectionUrl)) {
|
|
57480
|
+
console.error("Not ready: ADO_COLLECTION_URL is not configured");
|
|
57481
|
+
process.exit(1);
|
|
57482
|
+
}
|
|
57483
|
+
if (!project || hasPlaceholder(project)) {
|
|
57484
|
+
console.error("Not ready: ADO_PROJECT is not configured");
|
|
57485
|
+
process.exit(1);
|
|
57486
|
+
}
|
|
57487
|
+
if (!repo || hasPlaceholder(repo)) {
|
|
57488
|
+
console.error("Not ready: ADO_REPO is not configured");
|
|
57489
|
+
process.exit(1);
|
|
57490
|
+
}
|
|
57491
|
+
try {
|
|
57492
|
+
const config = getConfig();
|
|
57493
|
+
const witApi = await config.connection.getWorkItemTrackingApi();
|
|
57494
|
+
await witApi.queryByWiql({ query: "SELECT [System.Id] FROM WorkItems ORDER BY [System.ChangedDate] DESC" }, { project: config.project }, undefined, 1);
|
|
57495
|
+
console.log("Ready");
|
|
57496
|
+
} catch (err) {
|
|
57497
|
+
console.error(`Not ready: ${err instanceof Error ? err.message : String(err)}`);
|
|
57498
|
+
process.exit(1);
|
|
57499
|
+
}
|
|
57500
|
+
}
|
|
57401
57501
|
async function cmdRepos(config) {
|
|
57402
57502
|
const gitApi = await config.connection.getGitApi();
|
|
57403
57503
|
const repos = await gitApi.getRepositories(config.project);
|
|
@@ -57705,6 +57805,24 @@ async function linkWorkItemsToPr(config, _repo, pr, workItemIds) {
|
|
|
57705
57805
|
console.log(`Linked work item #${workItemId} to PR #${pr?.pullRequestId}`);
|
|
57706
57806
|
}
|
|
57707
57807
|
}
|
|
57808
|
+
async function addTagsToPr(config, repoName, pullRequestId, tags) {
|
|
57809
|
+
if (tags.length === 0)
|
|
57810
|
+
return;
|
|
57811
|
+
const gitApi = await config.connection.getGitApi();
|
|
57812
|
+
const repository = await gitApi.getRepository(repoName, config.project);
|
|
57813
|
+
const repositoryId = repository?.id;
|
|
57814
|
+
if (!repositoryId) {
|
|
57815
|
+
throw new Error(`Repository "${repoName}" not found.`);
|
|
57816
|
+
}
|
|
57817
|
+
const existing = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, config.project);
|
|
57818
|
+
const existingTags = new Set(existing.map((label) => label.name).filter(Boolean));
|
|
57819
|
+
for (const tag of tags) {
|
|
57820
|
+
if (existingTags.has(tag))
|
|
57821
|
+
continue;
|
|
57822
|
+
await gitApi.createPullRequestLabel({ name: tag }, repositoryId, pullRequestId, config.project);
|
|
57823
|
+
console.log(`Added tag "${tag}" to PR #${pullRequestId}`);
|
|
57824
|
+
}
|
|
57825
|
+
}
|
|
57708
57826
|
async function cmdPrCreate(config, args) {
|
|
57709
57827
|
const kv = Object.fromEntries(args.map((arg) => {
|
|
57710
57828
|
const [k, ...rest] = arg.split("=");
|
|
@@ -57716,8 +57834,9 @@ async function cmdPrCreate(config, args) {
|
|
|
57716
57834
|
const description = kv.description ?? "";
|
|
57717
57835
|
const repo = pickRepo(config, kv.repo);
|
|
57718
57836
|
const workItemIds = parseWorkItemIds(kv["work-items"]);
|
|
57837
|
+
const tags = parsePrTags(kv.tags);
|
|
57719
57838
|
if (!title || !source || !target) {
|
|
57720
|
-
console.error("Usage: pr-create --title=... --source=feature/x --target=develop [--description=...] [--repo=...] [--work-items=123,456]");
|
|
57839
|
+
console.error("Usage: pr-create --title=... --source=feature/x --target=develop [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]");
|
|
57721
57840
|
process.exit(1);
|
|
57722
57841
|
}
|
|
57723
57842
|
const gitApi = await config.connection.getGitApi();
|
|
@@ -57732,11 +57851,14 @@ async function cmdPrCreate(config, args) {
|
|
|
57732
57851
|
const createdPr = await gitApi.getPullRequestById(created.pullRequestId, config.project);
|
|
57733
57852
|
await linkWorkItemsToPr(config, repo, createdPr, workItemIds);
|
|
57734
57853
|
}
|
|
57854
|
+
if (created.pullRequestId && tags.length > 0) {
|
|
57855
|
+
await addTagsToPr(config, repo, created.pullRequestId, tags);
|
|
57856
|
+
}
|
|
57735
57857
|
}
|
|
57736
57858
|
async function cmdPrUpdate(config, idRaw, args) {
|
|
57737
57859
|
const id = Number(idRaw);
|
|
57738
57860
|
if (!Number.isFinite(id)) {
|
|
57739
|
-
console.error("Usage: pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]");
|
|
57861
|
+
console.error("Usage: pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]");
|
|
57740
57862
|
process.exit(1);
|
|
57741
57863
|
}
|
|
57742
57864
|
const kv = Object.fromEntries(args.map((arg) => {
|
|
@@ -57746,12 +57868,13 @@ async function cmdPrUpdate(config, idRaw, args) {
|
|
|
57746
57868
|
const repo = pickRepo(config, kv.repo);
|
|
57747
57869
|
const body = {};
|
|
57748
57870
|
const workItemIds = parseWorkItemIds(kv["work-items"]);
|
|
57871
|
+
const tags = parsePrTags(kv.tags);
|
|
57749
57872
|
if (kv.title !== undefined)
|
|
57750
57873
|
body.title = kv.title;
|
|
57751
57874
|
if (kv.description !== undefined)
|
|
57752
57875
|
body.description = kv.description;
|
|
57753
|
-
if (Object.keys(body).length === 0 && workItemIds.length === 0) {
|
|
57754
|
-
console.error("Usage: pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]");
|
|
57876
|
+
if (Object.keys(body).length === 0 && workItemIds.length === 0 && tags.length === 0) {
|
|
57877
|
+
console.error("Usage: pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]");
|
|
57755
57878
|
process.exit(1);
|
|
57756
57879
|
}
|
|
57757
57880
|
const gitApi = await config.connection.getGitApi();
|
|
@@ -57765,44 +57888,142 @@ async function cmdPrUpdate(config, idRaw, args) {
|
|
|
57765
57888
|
if (workItemIds.length > 0) {
|
|
57766
57889
|
await linkWorkItemsToPr(config, repo, updated, workItemIds);
|
|
57767
57890
|
}
|
|
57891
|
+
if (tags.length > 0) {
|
|
57892
|
+
await addTagsToPr(config, repo, id, tags);
|
|
57893
|
+
}
|
|
57768
57894
|
}
|
|
57769
|
-
async function
|
|
57895
|
+
async function cmdPrCherryPick(config, args) {
|
|
57896
|
+
const usage = "Usage: pr-cherry-pick <id> --target=main [--topic=branch-name] [--repo=...]";
|
|
57897
|
+
let parsed;
|
|
57898
|
+
try {
|
|
57899
|
+
parsed = parseCherryPickArgs(args);
|
|
57900
|
+
} catch (error) {
|
|
57901
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
57902
|
+
console.error(usage);
|
|
57903
|
+
process.exit(1);
|
|
57904
|
+
}
|
|
57905
|
+
const repo = pickRepo(config, parsed.repo);
|
|
57906
|
+
const gitApi = await config.connection.getGitApi();
|
|
57907
|
+
const repository = await gitApi.getRepository(repo, config.project);
|
|
57908
|
+
if (!repository?.id) {
|
|
57909
|
+
console.error(`Repository "${repo}" not found.`);
|
|
57910
|
+
process.exit(1);
|
|
57911
|
+
}
|
|
57912
|
+
const targetRef = parsed.target.startsWith("refs/") ? parsed.target : `refs/heads/${parsed.target}`;
|
|
57913
|
+
const generatedRefName = buildGeneratedRefName(parsed.prId, parsed.target, parsed.topic);
|
|
57914
|
+
const cherryPick = await gitApi.createCherryPick({
|
|
57915
|
+
source: { pullRequestId: parsed.prId },
|
|
57916
|
+
ontoRefName: targetRef,
|
|
57917
|
+
generatedRefName,
|
|
57918
|
+
repository: { id: repository.id }
|
|
57919
|
+
}, config.project, repository.id);
|
|
57920
|
+
let status = cherryPick.status;
|
|
57921
|
+
const cherryPickId = cherryPick.cherryPickId;
|
|
57922
|
+
if (cherryPickId == null) {
|
|
57923
|
+
console.error("Cherry-pick operation could not be started.");
|
|
57924
|
+
process.exit(1);
|
|
57925
|
+
}
|
|
57926
|
+
const MAX_POLL_ATTEMPTS = 30;
|
|
57927
|
+
const POLL_INTERVAL_MS = 1000;
|
|
57928
|
+
for (let attempt = 0;attempt < MAX_POLL_ATTEMPTS && (status === import_GitInterfaces.GitAsyncOperationStatus.Queued || status === import_GitInterfaces.GitAsyncOperationStatus.InProgress); attempt++) {
|
|
57929
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
57930
|
+
const result = await gitApi.getCherryPick(config.project, cherryPickId, repository.id);
|
|
57931
|
+
status = result.status;
|
|
57932
|
+
}
|
|
57933
|
+
if (status === import_GitInterfaces.GitAsyncOperationStatus.Completed) {
|
|
57934
|
+
console.log(`Cherry-pick of PR #${parsed.prId} completed. Branch created: ${generatedRefName.replace("refs/heads/", "")}`);
|
|
57935
|
+
} else if (status === import_GitInterfaces.GitAsyncOperationStatus.Failed) {
|
|
57936
|
+
console.error(`Cherry-pick of PR #${parsed.prId} failed.`);
|
|
57937
|
+
process.exit(1);
|
|
57938
|
+
} else {
|
|
57939
|
+
console.error(`Cherry-pick of PR #${parsed.prId} did not complete in time (status: ${import_GitInterfaces.GitAsyncOperationStatus[status ?? 0] ?? "unknown"}).`);
|
|
57940
|
+
process.exit(1);
|
|
57941
|
+
}
|
|
57942
|
+
}
|
|
57943
|
+
async function cmdInit(args) {
|
|
57944
|
+
const isLocal = args.includes("--local");
|
|
57770
57945
|
const { createInterface } = await import("node:readline/promises");
|
|
57771
57946
|
const { mkdirSync, writeFileSync, existsSync: existsSync2 } = await import("node:fs");
|
|
57772
57947
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
57773
|
-
const existing = loadFileConfig();
|
|
57774
|
-
const configPath = getConfigFilePath();
|
|
57775
|
-
console.log(
|
|
57948
|
+
const existing = isLocal ? loadLocalConfig() : loadFileConfig();
|
|
57949
|
+
const configPath = isLocal ? getLocalConfigFilePath() : getConfigFilePath();
|
|
57950
|
+
console.log(`Azure DevOps CLI — ${isLocal ? "Local " : ""}Configuration`);
|
|
57776
57951
|
console.log(`Config file: ${configPath}`);
|
|
57777
57952
|
console.log(`Press Enter to keep existing values shown in brackets.
|
|
57778
57953
|
`);
|
|
57779
|
-
|
|
57780
|
-
|
|
57781
|
-
|
|
57782
|
-
|
|
57783
|
-
|
|
57784
|
-
|
|
57785
|
-
|
|
57786
|
-
|
|
57787
|
-
|
|
57954
|
+
if (isLocal) {
|
|
57955
|
+
const collectionUrl = await rl.question(`Collection URL${existing.collectionUrl ? ` [${existing.collectionUrl}]` : ""}: `) || existing.collectionUrl || "";
|
|
57956
|
+
const project = await rl.question(`Project${existing.project ? ` [${existing.project}]` : ""}: `) || existing.project || "";
|
|
57957
|
+
const repo = await rl.question(`Repository${existing.repo ? ` [${existing.repo}]` : ""}: `) || existing.repo || "";
|
|
57958
|
+
rl.close();
|
|
57959
|
+
const config = {};
|
|
57960
|
+
if (collectionUrl)
|
|
57961
|
+
config.collectionUrl = collectionUrl;
|
|
57962
|
+
if (project)
|
|
57963
|
+
config.project = project;
|
|
57964
|
+
if (repo)
|
|
57965
|
+
config.repo = repo;
|
|
57966
|
+
if (Object.keys(config).length === 0) {
|
|
57967
|
+
console.error(`
|
|
57968
|
+
At least one field must be provided for local config.`);
|
|
57969
|
+
process.exit(1);
|
|
57970
|
+
}
|
|
57971
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
57972
|
+
`, "utf8");
|
|
57973
|
+
console.log(`
|
|
57974
|
+
Local configuration saved to ${configPath}`);
|
|
57975
|
+
} else {
|
|
57976
|
+
const pat = await rl.question(`Personal Access Token (PAT)${existing.pat ? " [****]" : ""}: `) || existing.pat || "";
|
|
57977
|
+
const collectionUrl = await rl.question(`Collection URL${existing.collectionUrl ? ` [${existing.collectionUrl}]` : ""}: `) || existing.collectionUrl || "";
|
|
57978
|
+
const project = await rl.question(`Project${existing.project ? ` [${existing.project}]` : ""}: `) || existing.project || "";
|
|
57979
|
+
const repo = await rl.question(`Repository${existing.repo ? ` [${existing.repo}]` : ""}: `) || existing.repo || "";
|
|
57980
|
+
const insecureInput = await rl.question(`Disable TLS verification (insecure)? (y/N)${existing.insecure ? " [y]" : ""}: `) || (existing.insecure ? "y" : "n");
|
|
57981
|
+
const insecure = insecureInput.toLowerCase() === "y" || insecureInput === "1";
|
|
57982
|
+
rl.close();
|
|
57983
|
+
if (!pat || !collectionUrl || !project || !repo) {
|
|
57984
|
+
console.error(`
|
|
57788
57985
|
All fields (PAT, Collection URL, Project, Repository) are required.`);
|
|
57789
|
-
|
|
57790
|
-
|
|
57791
|
-
|
|
57792
|
-
|
|
57793
|
-
|
|
57794
|
-
|
|
57795
|
-
|
|
57796
|
-
|
|
57986
|
+
process.exit(1);
|
|
57987
|
+
}
|
|
57988
|
+
const config = { pat, collectionUrl, project, repo, insecure };
|
|
57989
|
+
const configDir = getConfigDir();
|
|
57990
|
+
if (!existsSync2(configDir)) {
|
|
57991
|
+
mkdirSync(configDir, { recursive: true });
|
|
57992
|
+
}
|
|
57993
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
57797
57994
|
`, "utf8");
|
|
57798
|
-
|
|
57995
|
+
console.log(`
|
|
57799
57996
|
Configuration saved to ${configPath}`);
|
|
57997
|
+
}
|
|
57998
|
+
}
|
|
57999
|
+
function cmdConfig() {
|
|
58000
|
+
const fileConfig = loadFileConfig();
|
|
58001
|
+
const localConfig = loadLocalConfig();
|
|
58002
|
+
const pat = process.env.DEVOPS_PAT ?? localConfig.pat ?? fileConfig.pat;
|
|
58003
|
+
const collectionUrl = process.env.ADO_COLLECTION_URL ?? localConfig.collectionUrl ?? fileConfig.collectionUrl;
|
|
58004
|
+
const project = process.env.ADO_PROJECT ?? localConfig.project ?? fileConfig.project;
|
|
58005
|
+
const repo = process.env.ADO_REPO ?? localConfig.repo ?? fileConfig.repo;
|
|
58006
|
+
const insecure = process.env.ADO_INSECURE === "1" || localConfig.insecure === true || fileConfig.insecure === true;
|
|
58007
|
+
console.log(JSON.stringify({
|
|
58008
|
+
pat: pat ? censorPat(pat) : undefined,
|
|
58009
|
+
collectionUrl: collectionUrl ?? undefined,
|
|
58010
|
+
project: project ?? undefined,
|
|
58011
|
+
repo: repo ?? undefined,
|
|
58012
|
+
insecure,
|
|
58013
|
+
sources: {
|
|
58014
|
+
globalConfig: getConfigFilePath(),
|
|
58015
|
+
localConfig: getLocalConfigFilePath()
|
|
58016
|
+
}
|
|
58017
|
+
}, null, 2));
|
|
57800
58018
|
}
|
|
57801
58019
|
function printHelp() {
|
|
57802
58020
|
console.log(`Azure DevOps CLI
|
|
57803
58021
|
|
|
57804
58022
|
Commands:
|
|
57805
|
-
|
|
58023
|
+
-v, --version
|
|
58024
|
+
init [--local]
|
|
58025
|
+
config
|
|
58026
|
+
status
|
|
57806
58027
|
smoke
|
|
57807
58028
|
repos
|
|
57808
58029
|
branches [repo]
|
|
@@ -57813,21 +58034,39 @@ Commands:
|
|
|
57813
58034
|
workitem-comment-update <id> <commentId> --text="..." [--file=path]
|
|
57814
58035
|
prs [status] [top] [repo]
|
|
57815
58036
|
pr-get <id> [repo]
|
|
57816
|
-
pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456]
|
|
57817
|
-
pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456]
|
|
58037
|
+
pr-create --title=... --source=... --target=... [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]
|
|
58038
|
+
pr-update <id> [--title=...] [--description=...] [--repo=...] [--work-items=123,456] [--tags=tag-a,tag-b]
|
|
58039
|
+
pr-cherry-pick <id> --target=... [--topic=branch-name] [--repo=...]
|
|
57818
58040
|
pr-approve <id> [repo]
|
|
57819
58041
|
pr-autocomplete <id> [repo]
|
|
57820
58042
|
builds [top]
|
|
57821
58043
|
`);
|
|
57822
58044
|
}
|
|
58045
|
+
async function printVersion() {
|
|
58046
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
58047
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
58048
|
+
console.log(pkg.version);
|
|
58049
|
+
}
|
|
57823
58050
|
async function main() {
|
|
57824
58051
|
const [command = "smoke", ...args] = process.argv.slice(2);
|
|
57825
58052
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
57826
58053
|
printHelp();
|
|
57827
58054
|
return;
|
|
57828
58055
|
}
|
|
58056
|
+
if (command === "-v" || command === "--version") {
|
|
58057
|
+
await printVersion();
|
|
58058
|
+
return;
|
|
58059
|
+
}
|
|
57829
58060
|
if (command === "init") {
|
|
57830
|
-
await cmdInit();
|
|
58061
|
+
await cmdInit(args);
|
|
58062
|
+
return;
|
|
58063
|
+
}
|
|
58064
|
+
if (command === "config") {
|
|
58065
|
+
cmdConfig();
|
|
58066
|
+
return;
|
|
58067
|
+
}
|
|
58068
|
+
if (command === "status") {
|
|
58069
|
+
await cmdStatus();
|
|
57831
58070
|
return;
|
|
57832
58071
|
}
|
|
57833
58072
|
const config = getConfig();
|
|
@@ -57868,6 +58107,9 @@ async function main() {
|
|
|
57868
58107
|
case "pr-update":
|
|
57869
58108
|
await cmdPrUpdate(config, args[0], args.slice(1));
|
|
57870
58109
|
break;
|
|
58110
|
+
case "pr-cherry-pick":
|
|
58111
|
+
await cmdPrCherryPick(config, args);
|
|
58112
|
+
break;
|
|
57871
58113
|
case "pr-approve":
|
|
57872
58114
|
await cmdPrApprove(config, args[0], args[1]);
|
|
57873
58115
|
break;
|
package/package.json
CHANGED
|
@@ -9,16 +9,14 @@ Use the `ado` CLI instead of ad-hoc curl commands.
|
|
|
9
9
|
|
|
10
10
|
## Preflight
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
2. Verify connectivity:
|
|
15
|
-
- `ado smoke`
|
|
16
|
-
- If needed, run an independent host check (outside the CLI) to distinguish network issues from ADO config issues.
|
|
17
|
-
|
|
18
|
-
If auth is missing, stop and ask for `DEVOPS_PAT`.
|
|
12
|
+
Run `ado status` before any other command. It checks token, configuration, and connectivity in one step.
|
|
13
|
+
If it reports an issue, guide the user to run `ado init` (or `ado init --local` for per-project setup) to configure the CLI.
|
|
19
14
|
|
|
20
15
|
## Core commands
|
|
21
16
|
|
|
17
|
+
- Initialize global config: `ado init`
|
|
18
|
+
- Initialize local (per-project) config: `ado init --local`
|
|
19
|
+
- Show resolved config: `ado config`
|
|
22
20
|
- List repos: `ado repos`
|
|
23
21
|
- List branches: `ado branches "MyRepo"`
|
|
24
22
|
- Get work item: `ado workitem-get <id>`
|
|
@@ -30,8 +28,9 @@ If auth is missing, stop and ask for `DEVOPS_PAT`.
|
|
|
30
28
|
- Update an existing comment on a work item: `ado workitem-comment-update <id> <commentId> --text="..."`
|
|
31
29
|
- List PRs: `ado prs active 10 "MyRepo"`
|
|
32
30
|
- Get PR: `ado pr-get <id> "MyRepo"`
|
|
33
|
-
- Create PR: `ado pr-create --title="..." --source="feature/x" --target="develop" --description="..." --repo="MyRepo" --work-items=123,456`
|
|
34
|
-
- Update PR: `ado pr-update <id> --title="..." --description="..." --repo="MyRepo" --work-items=123,456`
|
|
31
|
+
- Create PR: `ado pr-create --title="..." --source="feature/x" --target="develop" --description="..." --repo="MyRepo" --work-items=123,456 --tags=backend,release-1`
|
|
32
|
+
- Update PR: `ado pr-update <id> --title="..." --description="..." --repo="MyRepo" --work-items=123,456 --tags=backend,release-1`
|
|
33
|
+
- Cherry-pick PR onto another branch: `ado pr-cherry-pick <id> --target="main" --topic="cherry-pick-branch" --repo="MyRepo"`
|
|
35
34
|
- Approve PR: `ado pr-approve <id> "MyRepo"`
|
|
36
35
|
- Enable auto-complete: `ado pr-autocomplete <id> "MyRepo"`
|
|
37
36
|
- List builds: `ado builds 10`
|