automata-cli 0.2.0-develop.21 → 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 (3) hide show
  1. package/README.md +7 -59
  2. package/dist/index.js +238 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # automata-cli
2
2
 
3
- A command-line interface tool.
3
+ A command-line interface tool for automating Git and project workflows.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,7 +8,7 @@ A command-line interface tool.
8
8
  npm install -g automata-cli
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Quick start
12
12
 
13
13
  ```bash
14
14
  automata --help
@@ -16,62 +16,10 @@ automata --help
16
16
 
17
17
  ## Commands
18
18
 
19
- ### `automata config`
20
-
21
- Launch the interactive configuration wizard. Use arrow keys to select the remote environment type and press Enter to save.
22
-
23
- ```bash
24
- automata config
25
- ```
26
-
27
- ### `automata config set type <value>`
28
-
29
- Set a configuration value non-interactively (useful in scripts or CI).
30
-
31
- ```bash
32
- automata config set type gh # GitHub
33
- automata config set type azdo # Azure DevOps
34
- ```
35
-
36
- Configuration is saved to `.automata/config.json` in the current directory.
37
-
38
- ### `automata git get-pr-info`
39
-
40
- Show the pull request associated with the current branch (requires [`gh` CLI](https://cli.github.com/) installed and authenticated).
41
-
42
- ```bash
43
- automata git get-pr-info # human-readable output
44
- automata git get-pr-info --json # JSON output
45
- ```
46
-
47
- Example output:
48
-
49
- ```
50
- PR: #42
51
- Title: Fix authentication bug
52
- State: MERGED
53
- URL: https://github.com/org/repo/pull/42
54
- ```
55
-
56
- If no PR exists for the current branch, a friendly message is printed and the command exits with code 0.
57
-
58
- ### `automata git finish-feature`
59
-
60
- Clean up a merged feature branch in one step: checkout `develop`, pull the latest, and delete the local branch.
61
-
62
- ```bash
63
- automata git finish-feature
64
- ```
65
-
66
- The command validates all preconditions before making any changes:
67
-
68
- - Must **not** be on the `develop` branch
69
- - Working tree must be clean (no uncommitted changes)
70
- - A pull request for the branch must exist
71
- - The PR must be in `merged` state (not open or closed-without-merge)
72
- - The remote tracking branch must no longer exist (`origin/<branch>` is gone)
73
-
74
- If any precondition fails, the command prints a descriptive error to stderr and exits with a non-zero code.
19
+ | Command group | Description | Docs |
20
+ |---|---|---|
21
+ | `automata config` | Configure the tool | [docs/config.md](docs/config.md) |
22
+ | `automata git` | Git workflow helpers (requires `gh` CLI) | [docs/git.md](docs/git.md) |
75
23
 
76
24
  ## Development
77
25
 
@@ -91,7 +39,7 @@ npm install
91
39
  ### Scripts
92
40
 
93
41
  | Command | Description |
94
- | --- | --- |
42
+ |---|---|
95
43
  | `npm run build` | Build the CLI with tsup |
96
44
  | `npm test` | Build and run tests with vitest |
97
45
  | `npm run lint` | Lint source files with ESLint |
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"
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")) {
@@ -263,41 +349,125 @@ function getPrInfo(branch) {
263
349
  }
264
350
  throw new Error(stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
265
351
  }
266
- return JSON.parse(stdout);
352
+ const raw = JSON.parse(stdout);
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
+ });
368
+ return { number: raw.number, title: raw.title, state: raw.state, url: raw.url, checks };
369
+ }
370
+ function getPrInfo2(branch) {
371
+ const config = readConfig();
372
+ if (config.remoteType === "azdo") {
373
+ return getPrInfo();
374
+ }
375
+ return getPrInfoGh(branch);
267
376
  }
268
377
  function isUpstreamGone(branch) {
269
- const { status } = run("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
378
+ const { status } = run2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
270
379
  return status !== 0;
271
380
  }
272
381
  function hasUncommittedChanges() {
273
- const { stdout } = run("git", ["status", "--porcelain"]);
382
+ const { stdout } = run2("git", ["status", "--porcelain"]);
274
383
  return stdout.trim().length > 0;
275
384
  }
276
385
  function checkoutAndPull(targetBranch) {
277
- const checkout = run("git", ["checkout", targetBranch]);
386
+ const checkout = run2("git", ["checkout", targetBranch]);
278
387
  if (checkout.status !== 0) {
279
388
  throw new Error(`Failed to checkout ${targetBranch}: ${checkout.stderr.trim()}`);
280
389
  }
281
- const pull = run("git", ["pull"]);
390
+ const pull = run2("git", ["pull"]);
282
391
  if (pull.status !== 0) {
283
392
  throw new Error(`Failed to pull ${targetBranch}: ${pull.stderr.trim()}`);
284
393
  }
285
394
  }
286
395
  function fetchPrune() {
287
- const result = run("git", ["fetch", "--prune"]);
396
+ const result = run2("git", ["fetch", "--prune"]);
288
397
  if (result.status !== 0) {
289
398
  throw new Error(`Failed to fetch --prune: ${result.stderr.trim()}`);
290
399
  }
291
400
  }
292
401
  function deleteLocalBranch(branch) {
293
- const result = run("git", ["branch", "-d", branch]);
402
+ const result = run2("git", ["branch", "-D", branch]);
294
403
  if (result.status !== 0) {
295
404
  throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
296
405
  }
297
406
  }
298
407
 
299
408
  // src/commands/git.ts
300
- var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").action((options) => {
409
+ var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
410
+ var SKIP_CONCLUSIONS = /* @__PURE__ */ new Set(["SKIPPED", "NEUTRAL"]);
411
+ function checkSymbol(check) {
412
+ if (check.status !== "COMPLETED") return "\u25CF";
413
+ if (check.conclusion === "SUCCESS") return "\u2713";
414
+ if (check.conclusion !== null && SKIP_CONCLUSIONS.has(check.conclusion)) return "\u25CB";
415
+ if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) return "\u2717";
416
+ return "\u25CF";
417
+ }
418
+ function formatCheckSummary(checks) {
419
+ const running = checks.some((c) => c.status !== "COMPLETED");
420
+ const failed = checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
421
+ const errors = failed.length === 0 ? "none" : failed.map((c) => `${c.name}: ${c.description.trim() || c.detailsUrl || "no details available"}`).join("; ");
422
+ return `Checks Running: ${String(running)}
423
+ Check Errors: ${errors}
424
+ `;
425
+ }
426
+ function formatChecks(checks) {
427
+ if (checks.length === 0) return "Checks: none\n";
428
+ const lines = ["Checks:"];
429
+ for (const check of checks) {
430
+ const sym = checkSymbol(check);
431
+ const pending = check.status !== "COMPLETED" ? " (pending)" : "";
432
+ lines.push(` ${sym} ${check.name}${pending}`);
433
+ if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) {
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)`);
439
+ }
440
+ }
441
+ return lines.join("\n") + "\n";
442
+ }
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(
460
+ "after",
461
+ `
462
+ Check status symbols:
463
+ \u2713 Passed (conclusion: SUCCESS)
464
+ \u2717 Failed (conclusion: FAILURE / TIMED_OUT / ACTION_REQUIRED / CANCELLED)
465
+ \u25CF Pending (status: QUEUED or IN_PROGRESS)
466
+ \u25CB Skipped (conclusion: SKIPPED or NEUTRAL)
467
+
468
+ Failure details are printed beneath each \u2717 check.
469
+ See docs/git.md for full output reference.`
470
+ ).action(async (options) => {
301
471
  let branch;
302
472
  try {
303
473
  branch = getCurrentBranch();
@@ -306,9 +476,51 @@ var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request in
306
476
  `);
307
477
  process.exit(1);
308
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
+ }
309
521
  let pr;
310
522
  try {
311
- pr = getPrInfo(branch);
523
+ pr = getPrInfo2(branch);
312
524
  } catch (err) {
313
525
  process.stderr.write(`Error: ${err.message}
314
526
  `);
@@ -327,6 +539,8 @@ Title: ${pr.title}
327
539
  State: ${pr.state}
328
540
  URL: ${pr.url}
329
541
  `);
542
+ process.stdout.write(formatCheckSummary(pr.checks));
543
+ process.stdout.write(formatChecks(pr.checks));
330
544
  }
331
545
  });
332
546
  var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(() => {
@@ -350,7 +564,7 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
350
564
  }
351
565
  let pr;
352
566
  try {
353
- pr = getPrInfo(branch);
567
+ pr = getPrInfo2(branch);
354
568
  } catch (err) {
355
569
  process.stderr.write(`Error: ${err.message}
356
570
  `);
@@ -402,14 +616,14 @@ var gitCommand = new Command2("git").description("Git workflow commands (require
402
616
 
403
617
  // src/commands/getReady.ts
404
618
  import { Command as Command3 } from "commander";
405
- import { spawnSync as spawnSync3 } from "child_process";
619
+ import { spawnSync as spawnSync4 } from "child_process";
406
620
  import { existsSync } from "fs";
407
621
  import { delimiter, join as join2 } from "path";
408
622
 
409
623
  // src/config/githubService.ts
410
- import { spawnSync as spawnSync2 } from "child_process";
411
- function run2(cmd, args) {
412
- 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" });
413
627
  if (result.error) {
414
628
  const err = result.error;
415
629
  if (err.code === "ENOENT") {
@@ -450,7 +664,7 @@ function listIssues(technique, value) {
450
664
  filterArgs = ["--search", `${value} in:title`];
451
665
  break;
452
666
  }
453
- const { stdout, stderr, status } = run2("gh", [...baseArgs, ...filterArgs]);
667
+ const { stdout, stderr, status } = run3("gh", [...baseArgs, ...filterArgs]);
454
668
  if (status !== 0) {
455
669
  throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
456
670
  }
@@ -461,7 +675,7 @@ function listIssues(technique, value) {
461
675
  return issues[0];
462
676
  }
463
677
  function postComment(issueNumber, body) {
464
- const { stderr, status } = run2("gh", ["issue", "comment", String(issueNumber), "--body", body]);
678
+ const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
465
679
  if (status !== 0) {
466
680
  throw new Error(stderr.trim() || `Failed to post comment on issue #${issueNumber}.`);
467
681
  }
@@ -481,7 +695,7 @@ function invokeClaudeCode(issue, systemPrompt) {
481
695
 
482
696
  ${issue.body}` : issue.body;
483
697
  const claudeBin = resolveCommand("claude");
484
- const result = spawnSync3(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
698
+ const result = spawnSync4(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
485
699
  if (result.error) {
486
700
  const err = result.error;
487
701
  if (err.code === "ENOENT") {
@@ -501,7 +715,9 @@ ${issue.body}` : issue.body;
501
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) => {
502
716
  const config = readConfig();
503
717
  if (config.remoteType !== "gh") {
504
- 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
+ );
505
721
  process.exit(1);
506
722
  }
507
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.21",
3
+ "version": "0.2.0-develop.31",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {