@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 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="UserLock"
41
- export ADO_REPO="Ulysse Interface"
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 pat = process.env.DEVOPS_PAT ?? fileConfig.pat;
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 cmdInit() {
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("Azure DevOps CLI — Configuration");
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
- const pat = await rl.question(`Personal Access Token (PAT)${existing.pat ? " [****]" : ""}: `) || existing.pat || "";
57780
- const collectionUrl = await rl.question(`Collection URL${existing.collectionUrl ? ` [${existing.collectionUrl}]` : ""}: `) || existing.collectionUrl || "";
57781
- const project = await rl.question(`Project${existing.project ? ` [${existing.project}]` : ""}: `) || existing.project || "";
57782
- const repo = await rl.question(`Repository${existing.repo ? ` [${existing.repo}]` : ""}: `) || existing.repo || "";
57783
- const insecureInput = await rl.question(`Disable TLS verification (insecure)? (y/N)${existing.insecure ? " [y]" : ""}: `) || (existing.insecure ? "y" : "n");
57784
- const insecure = insecureInput.toLowerCase() === "y" || insecureInput === "1";
57785
- rl.close();
57786
- if (!pat || !collectionUrl || !project || !repo) {
57787
- console.error(`
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
- process.exit(1);
57790
- }
57791
- const config = { pat, collectionUrl, project, repo, insecure };
57792
- const configDir = getConfigDir();
57793
- if (!existsSync2(configDir)) {
57794
- mkdirSync(configDir, { recursive: true });
57795
- }
57796
- writeFileSync(configPath, JSON.stringify(config, null, 2) + `
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
- console.log(`
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
- init
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thanaen/ado-cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight Azure DevOps CLI for repos, work items, pull requests, and builds",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,16 +9,14 @@ Use the `ado` CLI instead of ad-hoc curl commands.
9
9
 
10
10
  ## Preflight
11
11
 
12
- 1. Verify auth is available:
13
- - `test -n "$DEVOPS_PAT" && echo OK || echo MISSING`
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`