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

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 +196 -32
  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,41 +350,56 @@ 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
  }
@@ -317,7 +418,7 @@ function checkSymbol(check) {
317
418
  function formatCheckSummary(checks) {
318
419
  const running = checks.some((c) => c.status !== "COMPLETED");
319
420
  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("; ");
421
+ const errors = failed.length === 0 ? "none" : failed.map((c) => `${c.name}: ${c.description.trim() || c.detailsUrl || "no details available"}`).join("; ");
321
422
  return `Checks Running: ${String(running)}
322
423
  Check Errors: ${errors}
323
424
  `;
@@ -330,13 +431,32 @@ function formatChecks(checks) {
330
431
  const pending = check.status !== "COMPLETED" ? " (pending)" : "";
331
432
  lines.push(` ${sym} ${check.name}${pending}`);
332
433
  if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) {
333
- const detail = check.description.trim() || "(no details available)";
334
- lines.push(` Details: ${detail}`);
434
+ const desc = check.description.trim();
435
+ const url = check.detailsUrl.trim();
436
+ if (desc) lines.push(` Details: ${desc}`);
437
+ if (url) lines.push(` URL: ${url}`);
438
+ if (!desc && !url) lines.push(` Details: (no details available)`);
335
439
  }
336
440
  }
337
441
  return lines.join("\n") + "\n";
338
442
  }
339
- var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").addHelpText(
443
+ function sleep(ms) {
444
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
445
+ }
446
+ function formatFailedChecks(failed) {
447
+ const lines = [];
448
+ for (const check of failed) {
449
+ lines.push(` \u2717 ${check.name}`);
450
+ const desc = check.description.trim();
451
+ const url = check.detailsUrl.trim();
452
+ if (desc) lines.push(` Details: ${desc}`);
453
+ if (url) lines.push(` URL: ${url}`);
454
+ if (!desc && !url) lines.push(` Details: (no details available)`);
455
+ }
456
+ return lines.join("\n") + "\n";
457
+ }
458
+ var POLL_INTERVAL_MS = 1e4;
459
+ 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
460
  "after",
341
461
  `
342
462
  Check status symbols:
@@ -347,7 +467,7 @@ Check status symbols:
347
467
 
348
468
  Failure details are printed beneath each \u2717 check.
349
469
  See docs/git.md for full output reference.`
350
- ).action((options) => {
470
+ ).action(async (options) => {
351
471
  let branch;
352
472
  try {
353
473
  branch = getCurrentBranch();
@@ -356,9 +476,51 @@ See docs/git.md for full output reference.`
356
476
  `);
357
477
  process.exit(1);
358
478
  }
479
+ if (options.waitFinishChecks) {
480
+ let pr2;
481
+ while (true) {
482
+ try {
483
+ pr2 = getPrInfo2(branch);
484
+ } catch (err) {
485
+ process.stderr.write(`Error: ${err.message}
486
+ `);
487
+ process.exit(1);
488
+ }
489
+ if (pr2 === null) {
490
+ process.stderr.write(`Error: No pull request found for branch: ${branch}
491
+ `);
492
+ process.exit(1);
493
+ }
494
+ const running = pr2.checks.filter((c) => c.status !== "COMPLETED");
495
+ if (running.length === 0) break;
496
+ process.stdout.write(`Waiting for ${running.length} check(s) to complete...
497
+ `);
498
+ await sleep(POLL_INTERVAL_MS);
499
+ }
500
+ const failed = pr2.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
501
+ if (failed.length === 0) {
502
+ if (options.json) {
503
+ process.stdout.write(JSON.stringify({ result: "passed" }, null, 2) + "\n");
504
+ } else {
505
+ process.stdout.write(`All checks passed. \u2713
506
+ `);
507
+ }
508
+ process.exit(0);
509
+ } else {
510
+ if (options.json) {
511
+ process.stdout.write(JSON.stringify({ result: "failed", failed }, null, 2) + "\n");
512
+ } else {
513
+ process.stdout.write(`${failed.length} check(s) failed:
514
+ `);
515
+ process.stdout.write(formatFailedChecks(failed));
516
+ }
517
+ process.exit(1);
518
+ }
519
+ return;
520
+ }
359
521
  let pr;
360
522
  try {
361
- pr = getPrInfo(branch);
523
+ pr = getPrInfo2(branch);
362
524
  } catch (err) {
363
525
  process.stderr.write(`Error: ${err.message}
364
526
  `);
@@ -402,7 +564,7 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
402
564
  }
403
565
  let pr;
404
566
  try {
405
- pr = getPrInfo(branch);
567
+ pr = getPrInfo2(branch);
406
568
  } catch (err) {
407
569
  process.stderr.write(`Error: ${err.message}
408
570
  `);
@@ -454,14 +616,14 @@ var gitCommand = new Command2("git").description("Git workflow commands (require
454
616
 
455
617
  // src/commands/getReady.ts
456
618
  import { Command as Command3 } from "commander";
457
- import { spawnSync as spawnSync3 } from "child_process";
619
+ import { spawnSync as spawnSync4 } from "child_process";
458
620
  import { existsSync } from "fs";
459
621
  import { delimiter, join as join2 } from "path";
460
622
 
461
623
  // 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" });
624
+ import { spawnSync as spawnSync3 } from "child_process";
625
+ function run3(cmd, args) {
626
+ const result = spawnSync3(cmd, args, { encoding: "utf8" });
465
627
  if (result.error) {
466
628
  const err = result.error;
467
629
  if (err.code === "ENOENT") {
@@ -502,7 +664,7 @@ function listIssues(technique, value) {
502
664
  filterArgs = ["--search", `${value} in:title`];
503
665
  break;
504
666
  }
505
- const { stdout, stderr, status } = run2("gh", [...baseArgs, ...filterArgs]);
667
+ const { stdout, stderr, status } = run3("gh", [...baseArgs, ...filterArgs]);
506
668
  if (status !== 0) {
507
669
  throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
508
670
  }
@@ -513,7 +675,7 @@ function listIssues(technique, value) {
513
675
  return issues[0];
514
676
  }
515
677
  function postComment(issueNumber, body) {
516
- const { stderr, status } = run2("gh", ["issue", "comment", String(issueNumber), "--body", body]);
678
+ const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
517
679
  if (status !== 0) {
518
680
  throw new Error(stderr.trim() || `Failed to post comment on issue #${issueNumber}.`);
519
681
  }
@@ -533,7 +695,7 @@ function invokeClaudeCode(issue, systemPrompt) {
533
695
 
534
696
  ${issue.body}` : issue.body;
535
697
  const claudeBin = resolveCommand("claude");
536
- const result = spawnSync3(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
698
+ const result = spawnSync4(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
537
699
  if (result.error) {
538
700
  const err = result.error;
539
701
  if (err.code === "ENOENT") {
@@ -553,7 +715,9 @@ ${issue.body}` : issue.body;
553
715
  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
716
  const config = readConfig();
555
717
  if (config.remoteType !== "gh") {
556
- process.stderr.write("Error: get-ready is only available for GitHub (gh) mode.\n");
718
+ process.stderr.write(
719
+ "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"
720
+ );
557
721
  process.exit(1);
558
722
  }
559
723
  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.31",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {