automata-cli 0.2.0-develop.26 → 0.2.0-develop.40

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.
Files changed (2) hide show
  1. package/dist/index.js +322 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -233,9 +233,59 @@ var configCommand = new Command("config").description("Configure automata settin
233
233
  import { Command as Command2 } from "commander";
234
234
 
235
235
  // src/git/gitService.ts
236
+ import { spawnSync as spawnSync2 } from "child_process";
237
+
238
+ // src/config/azdoService.ts
236
239
  import { spawnSync } from "child_process";
237
240
  function run(cmd, args) {
238
241
  const result = spawnSync(cmd, args, { encoding: "utf8" });
242
+ if (result.error) {
243
+ const err = result.error;
244
+ if (err.code === "ENOENT") {
245
+ throw new Error("`azdo` CLI is not installed or not on PATH.");
246
+ }
247
+ throw new Error(err.message);
248
+ }
249
+ return {
250
+ stdout: result.stdout ?? "",
251
+ stderr: result.stderr ?? "",
252
+ status: result.status ?? 1
253
+ };
254
+ }
255
+ function mapStatus(azdoStatus) {
256
+ switch (azdoStatus) {
257
+ case "active":
258
+ return "OPEN";
259
+ case "completed":
260
+ return "MERGED";
261
+ case "abandoned":
262
+ return "CLOSED";
263
+ default:
264
+ return azdoStatus.toUpperCase();
265
+ }
266
+ }
267
+ function getPrInfo() {
268
+ const { stdout, stderr, status } = run("azdo", ["pr", "status", "--json"]);
269
+ if (status !== 0) {
270
+ throw new Error(stderr.trim() || "Failed to query Azure DevOps PR status. Is `azdo` installed and authenticated?");
271
+ }
272
+ const parsed = JSON.parse(stdout);
273
+ if (parsed.pullRequests.length === 0) {
274
+ return null;
275
+ }
276
+ const pr = parsed.pullRequests[0];
277
+ return {
278
+ number: pr.id,
279
+ title: pr.title,
280
+ state: mapStatus(pr.status),
281
+ url: pr.url,
282
+ checks: []
283
+ };
284
+ }
285
+
286
+ // src/git/gitService.ts
287
+ function run2(cmd, args) {
288
+ const result = spawnSync2(cmd, args, { encoding: "utf8" });
239
289
  return {
240
290
  stdout: result.stdout ?? "",
241
291
  stderr: result.stderr ?? "",
@@ -243,19 +293,55 @@ function run(cmd, args) {
243
293
  };
244
294
  }
245
295
  function getCurrentBranch() {
246
- const { stdout, status } = run("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
296
+ const { stdout, status } = run2("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
247
297
  if (status !== 0) {
248
298
  throw new Error("Failed to determine current branch. Are you inside a git repository?");
249
299
  }
250
300
  return stdout.trim();
251
301
  }
252
- function getPrInfo(branch) {
253
- const { stdout, stderr, status } = run("gh", [
302
+ function parseOwnerRepo() {
303
+ const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
304
+ if (status !== 0) return null;
305
+ const url = stdout.trim();
306
+ const https = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
307
+ if (https) return https[1];
308
+ const ssh = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
309
+ if (ssh) return ssh[1];
310
+ return null;
311
+ }
312
+ function extractLastMarkdownUrl(markdown) {
313
+ const matches = [...markdown.matchAll(/\]\((https?:\/\/[^)]+)\)/g)];
314
+ return matches.length > 0 ? matches[matches.length - 1][1] ?? null : null;
315
+ }
316
+ function fetchCheckRunOutputs(ownerRepo, sha) {
317
+ const { stdout, status } = run2("gh", [
318
+ "api",
319
+ `repos/${ownerRepo}/commits/${sha}/check-runs`,
320
+ "--jq",
321
+ ".check_runs[] | {name, html_url, details_url, output}"
322
+ ]);
323
+ const map = /* @__PURE__ */ new Map();
324
+ if (status !== 0) return map;
325
+ for (const line of stdout.trim().split("\n")) {
326
+ if (!line) continue;
327
+ try {
328
+ const item = JSON.parse(line);
329
+ const title = item.output?.title ?? "";
330
+ const summaryUrl = item.output?.summary ? extractLastMarkdownUrl(item.output.summary) : null;
331
+ const detailsUrl = summaryUrl ?? (item.details_url !== item.html_url ? item.details_url : "") ?? item.html_url;
332
+ map.set(item.name, { title, detailsUrl });
333
+ } catch {
334
+ }
335
+ }
336
+ return map;
337
+ }
338
+ function getPrInfoGh(branch) {
339
+ const { stdout, stderr, status } = run2("gh", [
254
340
  "pr",
255
341
  "view",
256
342
  branch,
257
343
  "--json",
258
- "number,title,state,url,statusCheckRollup"
344
+ "number,title,state,url,headRefOid,statusCheckRollup"
259
345
  ]);
260
346
  if (status !== 0) {
261
347
  if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
@@ -264,45 +350,127 @@ function getPrInfo(branch) {
264
350
  throw new Error(stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
265
351
  }
266
352
  const raw = JSON.parse(stdout);
267
- const checks = (raw.statusCheckRollup ?? []).map((c) => ({
268
- name: c.name,
269
- status: c.status,
270
- conclusion: c.conclusion,
271
- description: c.description ?? "",
272
- detailsUrl: c.detailsUrl ?? ""
273
- }));
353
+ const failedChecks = (raw.statusCheckRollup ?? []).filter(
354
+ (c) => c.conclusion !== null && ["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"].includes(c.conclusion)
355
+ );
356
+ const ownerRepo = failedChecks.length > 0 ? parseOwnerRepo() : null;
357
+ const checkOutputs = ownerRepo ? fetchCheckRunOutputs(ownerRepo, raw.headRefOid) : /* @__PURE__ */ new Map();
358
+ const checks = (raw.statusCheckRollup ?? []).map((c) => {
359
+ const enriched = checkOutputs.get(c.name);
360
+ return {
361
+ name: c.name,
362
+ status: c.status,
363
+ conclusion: c.conclusion,
364
+ description: enriched?.title || c.description || "",
365
+ detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
366
+ };
367
+ });
274
368
  return { number: raw.number, title: raw.title, state: raw.state, url: raw.url, checks };
275
369
  }
370
+ function getPrInfo2(branch) {
371
+ const config = readConfig();
372
+ if (config.remoteType === "azdo") {
373
+ return getPrInfo();
374
+ }
375
+ return getPrInfoGh(branch);
376
+ }
276
377
  function isUpstreamGone(branch) {
277
- const { status } = run("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
378
+ const { status } = run2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
278
379
  return status !== 0;
279
380
  }
280
381
  function hasUncommittedChanges() {
281
- const { stdout } = run("git", ["status", "--porcelain"]);
382
+ const { stdout } = run2("git", ["status", "--porcelain"]);
282
383
  return stdout.trim().length > 0;
283
384
  }
284
385
  function checkoutAndPull(targetBranch) {
285
- const checkout = run("git", ["checkout", targetBranch]);
386
+ const checkout = run2("git", ["checkout", targetBranch]);
286
387
  if (checkout.status !== 0) {
287
388
  throw new Error(`Failed to checkout ${targetBranch}: ${checkout.stderr.trim()}`);
288
389
  }
289
- const pull = run("git", ["pull"]);
390
+ const pull = run2("git", ["pull"]);
290
391
  if (pull.status !== 0) {
291
392
  throw new Error(`Failed to pull ${targetBranch}: ${pull.stderr.trim()}`);
292
393
  }
293
394
  }
294
395
  function fetchPrune() {
295
- const result = run("git", ["fetch", "--prune"]);
396
+ const result = run2("git", ["fetch", "--prune"]);
296
397
  if (result.status !== 0) {
297
398
  throw new Error(`Failed to fetch --prune: ${result.stderr.trim()}`);
298
399
  }
299
400
  }
300
401
  function deleteLocalBranch(branch) {
301
- const result = run("git", ["branch", "-d", branch]);
402
+ const result = run2("git", ["branch", "-D", branch]);
302
403
  if (result.status !== 0) {
303
404
  throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
304
405
  }
305
406
  }
407
+ var REVIEW_THREADS_QUERY = `
408
+ query($owner:String!,$repo:String!,$prNumber:Int!){
409
+ repository(owner:$owner,name:$repo){
410
+ pullRequest(number:$prNumber){
411
+ reviewThreads(first:100){
412
+ nodes{
413
+ isResolved
414
+ isOutdated
415
+ comments(first:1){
416
+ nodes{ author{login} body path line createdAt }
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }`.trim();
423
+ function getPrCommentsGh(branch) {
424
+ const prView = run2("gh", ["pr", "view", branch, "--json", "number"]);
425
+ if (prView.status !== 0) {
426
+ if (prView.stderr.includes("no pull requests found") || prView.stderr.includes("Could not resolve")) {
427
+ return null;
428
+ }
429
+ throw new Error(prView.stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
430
+ }
431
+ const { number: prNumber } = JSON.parse(prView.stdout);
432
+ const ownerRepo = parseOwnerRepo();
433
+ if (!ownerRepo) {
434
+ throw new Error("Could not determine GitHub owner/repo from git remote. Is 'origin' set to a GitHub URL?");
435
+ }
436
+ const slashIdx = ownerRepo.indexOf("/");
437
+ const owner = ownerRepo.slice(0, slashIdx);
438
+ const repo = ownerRepo.slice(slashIdx + 1);
439
+ const gql = run2("gh", [
440
+ "api",
441
+ "graphql",
442
+ "-f",
443
+ `query=${REVIEW_THREADS_QUERY}`,
444
+ "-f",
445
+ `owner=${owner}`,
446
+ "-f",
447
+ `repo=${repo}`,
448
+ "-F",
449
+ `prNumber=${String(prNumber)}`
450
+ ]);
451
+ if (gql.status !== 0) {
452
+ throw new Error(gql.stderr.trim() || "Failed to query GitHub GraphQL API.");
453
+ }
454
+ const response = JSON.parse(gql.stdout);
455
+ const threads = response.data.repository.pullRequest.reviewThreads.nodes;
456
+ return threads.filter((t) => !t.isResolved && t.comments.nodes.length > 0).map((t) => {
457
+ const c = t.comments.nodes[0];
458
+ return {
459
+ author: c.author.login,
460
+ body: c.body,
461
+ path: c.path,
462
+ line: c.line ?? null,
463
+ createdAt: c.createdAt
464
+ };
465
+ });
466
+ }
467
+ function getPrComments(branch) {
468
+ const config = readConfig();
469
+ if (config.remoteType === "azdo") {
470
+ return "unsupported";
471
+ }
472
+ return getPrCommentsGh(branch);
473
+ }
306
474
 
307
475
  // src/commands/git.ts
308
476
  var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
@@ -317,7 +485,7 @@ function checkSymbol(check) {
317
485
  function formatCheckSummary(checks) {
318
486
  const running = checks.some((c) => c.status !== "COMPLETED");
319
487
  const failed = checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
320
- const errors = failed.length === 0 ? "none" : failed.map((c) => `${c.name}: ${c.description.trim() || "no details available"}`).join("; ");
488
+ const errors = failed.length === 0 ? "none" : failed.map((c) => `${c.name}: ${c.description.trim() || c.detailsUrl || "no details available"}`).join("; ");
321
489
  return `Checks Running: ${String(running)}
322
490
  Check Errors: ${errors}
323
491
  `;
@@ -330,13 +498,32 @@ function formatChecks(checks) {
330
498
  const pending = check.status !== "COMPLETED" ? " (pending)" : "";
331
499
  lines.push(` ${sym} ${check.name}${pending}`);
332
500
  if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) {
333
- const detail = check.description.trim() || "(no details available)";
334
- lines.push(` Details: ${detail}`);
501
+ const desc = check.description.trim();
502
+ const url = check.detailsUrl.trim();
503
+ if (desc) lines.push(` Details: ${desc}`);
504
+ if (url) lines.push(` URL: ${url}`);
505
+ if (!desc && !url) lines.push(` Details: (no details available)`);
335
506
  }
336
507
  }
337
508
  return lines.join("\n") + "\n";
338
509
  }
339
- var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").addHelpText(
510
+ function sleep(ms) {
511
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
512
+ }
513
+ function formatFailedChecks(failed) {
514
+ const lines = [];
515
+ for (const check of failed) {
516
+ lines.push(` \u2717 ${check.name}`);
517
+ const desc = check.description.trim();
518
+ const url = check.detailsUrl.trim();
519
+ if (desc) lines.push(` Details: ${desc}`);
520
+ if (url) lines.push(` URL: ${url}`);
521
+ if (!desc && !url) lines.push(` Details: (no details available)`);
522
+ }
523
+ return lines.join("\n") + "\n";
524
+ }
525
+ var POLL_INTERVAL_MS = 1e4;
526
+ var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then report pass/fail (exit 1 on failure)").addHelpText(
340
527
  "after",
341
528
  `
342
529
  Check status symbols:
@@ -347,7 +534,7 @@ Check status symbols:
347
534
 
348
535
  Failure details are printed beneath each \u2717 check.
349
536
  See docs/git.md for full output reference.`
350
- ).action((options) => {
537
+ ).action(async (options) => {
351
538
  let branch;
352
539
  try {
353
540
  branch = getCurrentBranch();
@@ -356,9 +543,51 @@ See docs/git.md for full output reference.`
356
543
  `);
357
544
  process.exit(1);
358
545
  }
546
+ if (options.waitFinishChecks) {
547
+ let pr2;
548
+ while (true) {
549
+ try {
550
+ pr2 = getPrInfo2(branch);
551
+ } catch (err) {
552
+ process.stderr.write(`Error: ${err.message}
553
+ `);
554
+ process.exit(1);
555
+ }
556
+ if (pr2 === null) {
557
+ process.stderr.write(`Error: No pull request found for branch: ${branch}
558
+ `);
559
+ process.exit(1);
560
+ }
561
+ const running = pr2.checks.filter((c) => c.status !== "COMPLETED");
562
+ if (running.length === 0) break;
563
+ process.stdout.write(`Waiting for ${running.length} check(s) to complete...
564
+ `);
565
+ await sleep(POLL_INTERVAL_MS);
566
+ }
567
+ const failed = pr2.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
568
+ if (failed.length === 0) {
569
+ if (options.json) {
570
+ process.stdout.write(JSON.stringify({ result: "passed" }, null, 2) + "\n");
571
+ } else {
572
+ process.stdout.write(`All checks passed. \u2713
573
+ `);
574
+ }
575
+ process.exit(0);
576
+ } else {
577
+ if (options.json) {
578
+ process.stdout.write(JSON.stringify({ result: "failed", failed }, null, 2) + "\n");
579
+ } else {
580
+ process.stdout.write(`${failed.length} check(s) failed:
581
+ `);
582
+ process.stdout.write(formatFailedChecks(failed));
583
+ }
584
+ process.exit(1);
585
+ }
586
+ return;
587
+ }
359
588
  let pr;
360
589
  try {
361
- pr = getPrInfo(branch);
590
+ pr = getPrInfo2(branch);
362
591
  } catch (err) {
363
592
  process.stderr.write(`Error: ${err.message}
364
593
  `);
@@ -381,6 +610,64 @@ URL: ${pr.url}
381
610
  process.stdout.write(formatChecks(pr.checks));
382
611
  }
383
612
  });
613
+ var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
614
+ var CONTROL_CHARS_RE = new RegExp("[\0-\b\v\f-\x7F]", "g");
615
+ function sanitizeText(text) {
616
+ return text.replace(ANSI_ESCAPE_RE, "").replace(CONTROL_CHARS_RE, "");
617
+ }
618
+ var getPrCommentsCmd = new Command2("get-pr-comments").description("List open (unresolved) review comments on the pull request for the current branch (GitHub only)").option("--json", "Output as JSON array").addHelpText(
619
+ "after",
620
+ `
621
+ Only GitHub (remoteType: gh) is supported. Azure DevOps is not supported.
622
+ See docs/azdo-gap.md for details.`
623
+ ).action((options) => {
624
+ let branch;
625
+ try {
626
+ branch = getCurrentBranch();
627
+ } catch (err) {
628
+ process.stderr.write(`Error: ${err.message}
629
+ `);
630
+ process.exit(1);
631
+ }
632
+ let comments;
633
+ try {
634
+ comments = getPrComments(branch);
635
+ } catch (err) {
636
+ process.stderr.write(`Error: ${err.message}
637
+ `);
638
+ process.exit(1);
639
+ }
640
+ if (comments === "unsupported") {
641
+ process.stderr.write(
642
+ `Error: get-pr-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
643
+ `
644
+ );
645
+ process.exit(1);
646
+ }
647
+ if (comments === null) {
648
+ process.stderr.write(`Error: No pull request found for branch: ${branch}
649
+ `);
650
+ process.exit(1);
651
+ }
652
+ if (options.json) {
653
+ process.stdout.write(JSON.stringify(comments, null, 2) + "\n");
654
+ return;
655
+ }
656
+ if (comments.length === 0) {
657
+ process.stdout.write(`No open comments.
658
+ `);
659
+ return;
660
+ }
661
+ const lines = [];
662
+ for (const c of comments) {
663
+ const loc = c.line !== null ? `${c.path}:${String(c.line)}` : `${c.path}:(file)`;
664
+ const safeBody = sanitizeText(c.body);
665
+ const safeAuthor = sanitizeText(c.author);
666
+ lines.push(`[${safeAuthor}] on ${loc}
667
+ ${safeBody}`);
668
+ }
669
+ process.stdout.write(lines.join("\n\n") + "\n");
670
+ });
384
671
  var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(() => {
385
672
  let branch;
386
673
  try {
@@ -402,7 +689,7 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
402
689
  }
403
690
  let pr;
404
691
  try {
405
- pr = getPrInfo(branch);
692
+ pr = getPrInfo2(branch);
406
693
  } catch (err) {
407
694
  process.stderr.write(`Error: ${err.message}
408
695
  `);
@@ -450,18 +737,18 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
450
737
  process.exit(1);
451
738
  }
452
739
  });
453
- var gitCommand = new Command2("git").description("Git workflow commands (requires gh CLI)").addCommand(getPrInfoCmd).addCommand(finishFeatureCmd);
740
+ var gitCommand = new Command2("git").description("Git workflow commands (requires gh CLI)").addCommand(getPrInfoCmd).addCommand(getPrCommentsCmd).addCommand(finishFeatureCmd);
454
741
 
455
742
  // src/commands/getReady.ts
456
743
  import { Command as Command3 } from "commander";
457
- import { spawnSync as spawnSync3 } from "child_process";
744
+ import { spawnSync as spawnSync4 } from "child_process";
458
745
  import { existsSync } from "fs";
459
746
  import { delimiter, join as join2 } from "path";
460
747
 
461
748
  // src/config/githubService.ts
462
- import { spawnSync as spawnSync2 } from "child_process";
463
- function run2(cmd, args) {
464
- const result = spawnSync2(cmd, args, { encoding: "utf8" });
749
+ import { spawnSync as spawnSync3 } from "child_process";
750
+ function run3(cmd, args) {
751
+ const result = spawnSync3(cmd, args, { encoding: "utf8" });
465
752
  if (result.error) {
466
753
  const err = result.error;
467
754
  if (err.code === "ENOENT") {
@@ -502,7 +789,7 @@ function listIssues(technique, value) {
502
789
  filterArgs = ["--search", `${value} in:title`];
503
790
  break;
504
791
  }
505
- const { stdout, stderr, status } = run2("gh", [...baseArgs, ...filterArgs]);
792
+ const { stdout, stderr, status } = run3("gh", [...baseArgs, ...filterArgs]);
506
793
  if (status !== 0) {
507
794
  throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
508
795
  }
@@ -513,7 +800,7 @@ function listIssues(technique, value) {
513
800
  return issues[0];
514
801
  }
515
802
  function postComment(issueNumber, body) {
516
- const { stderr, status } = run2("gh", ["issue", "comment", String(issueNumber), "--body", body]);
803
+ const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
517
804
  if (status !== 0) {
518
805
  throw new Error(stderr.trim() || `Failed to post comment on issue #${issueNumber}.`);
519
806
  }
@@ -533,7 +820,7 @@ function invokeClaudeCode(issue, systemPrompt) {
533
820
 
534
821
  ${issue.body}` : issue.body;
535
822
  const claudeBin = resolveCommand("claude");
536
- const result = spawnSync3(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
823
+ const result = spawnSync4(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
537
824
  if (result.error) {
538
825
  const err = result.error;
539
826
  if (err.code === "ENOENT") {
@@ -553,7 +840,9 @@ ${issue.body}` : issue.body;
553
840
  var getReadyCommand = new Command3("get-ready").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke Claude Code").option("--json", "Output issue details as JSON").option("--no-claude", "Skip Claude Code invocation after claiming the issue").action((options) => {
554
841
  const config = readConfig();
555
842
  if (config.remoteType !== "gh") {
556
- process.stderr.write("Error: get-ready is only available for GitHub (gh) mode.\n");
843
+ process.stderr.write(
844
+ "Error: get-ready is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
845
+ );
557
846
  process.exit(1);
558
847
  }
559
848
  if (!config.issueDiscoveryTechnique) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.2.0-develop.26",
3
+ "version": "0.2.0-develop.40",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {