@blogic-cz/agent-tools 0.18.0 → 1.0.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.
Files changed (73) hide show
  1. package/README.md +23 -6
  2. package/dist/az-tool/config.d.ts +60 -12
  3. package/dist/az-tool/config.d.ts.map +1 -1
  4. package/dist/az-tool/errors.d.ts +10 -1
  5. package/dist/az-tool/errors.d.ts.map +1 -1
  6. package/dist/az-tool/profile.d.ts +16 -0
  7. package/dist/az-tool/profile.d.ts.map +1 -0
  8. package/dist/az-tool/security.d.ts +20 -3
  9. package/dist/az-tool/security.d.ts.map +1 -1
  10. package/dist/az-tool/service.d.ts +9 -4
  11. package/dist/az-tool/service.d.ts.map +1 -1
  12. package/dist/az-tool/types.d.ts +10 -60
  13. package/dist/az-tool/types.d.ts.map +1 -1
  14. package/dist/{az-tool → azdo-tool}/build.d.ts +9 -9
  15. package/dist/azdo-tool/build.d.ts.map +1 -0
  16. package/dist/azdo-tool/config.d.ts +13 -0
  17. package/dist/azdo-tool/config.d.ts.map +1 -0
  18. package/dist/azdo-tool/errors.d.ts +43 -0
  19. package/dist/azdo-tool/errors.d.ts.map +1 -0
  20. package/dist/azdo-tool/extract-option-value.d.ts.map +1 -0
  21. package/dist/azdo-tool/index.d.ts +3 -0
  22. package/dist/azdo-tool/index.d.ts.map +1 -0
  23. package/dist/azdo-tool/security.d.ts +4 -0
  24. package/dist/azdo-tool/security.d.ts.map +1 -0
  25. package/dist/azdo-tool/service.d.ts +15 -0
  26. package/dist/azdo-tool/service.d.ts.map +1 -0
  27. package/dist/azdo-tool/transformers.d.ts.map +1 -0
  28. package/dist/azdo-tool/types.d.ts +64 -0
  29. package/dist/azdo-tool/types.d.ts.map +1 -0
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/loader.d.ts +1 -1
  33. package/dist/config/loader.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +18 -1
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/credential-guard/index.d.ts.map +1 -1
  37. package/dist/gh-tool/errors.d.ts +1 -1
  38. package/dist/gh-tool/errors.d.ts.map +1 -1
  39. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  40. package/dist/shared/azure-credentials.d.ts +20 -0
  41. package/dist/shared/azure-credentials.d.ts.map +1 -0
  42. package/dist/shared/binary-preflight.d.ts +1 -1
  43. package/package.json +8 -2
  44. package/schemas/agent-tools.schema.json +42 -1
  45. package/src/az-tool/config.ts +135 -25
  46. package/src/az-tool/errors.ts +14 -1
  47. package/src/az-tool/index.ts +67 -134
  48. package/src/az-tool/profile.ts +48 -0
  49. package/src/az-tool/security.ts +248 -89
  50. package/src/az-tool/service.ts +137 -242
  51. package/src/az-tool/types.ts +11 -64
  52. package/src/{az-tool → azdo-tool}/build.ts +11 -11
  53. package/src/azdo-tool/config.ts +33 -0
  54. package/src/azdo-tool/errors.ts +41 -0
  55. package/src/azdo-tool/index.ts +222 -0
  56. package/src/azdo-tool/security.ts +157 -0
  57. package/src/azdo-tool/service.ts +322 -0
  58. package/src/azdo-tool/types.ts +67 -0
  59. package/src/config/index.ts +1 -0
  60. package/src/config/loader.ts +10 -1
  61. package/src/config/types.ts +19 -1
  62. package/src/credential-guard/index.ts +7 -2
  63. package/src/gh-tool/errors.ts +7 -1
  64. package/src/gh-tool/pr/core.ts +179 -71
  65. package/src/shared/azure-credentials.ts +50 -0
  66. package/src/shared/binary-preflight.ts +1 -1
  67. package/dist/az-tool/build.d.ts.map +0 -1
  68. package/dist/az-tool/extract-option-value.d.ts.map +0 -1
  69. package/dist/az-tool/transformers.d.ts.map +0 -1
  70. /package/dist/{az-tool → azdo-tool}/extract-option-value.d.ts +0 -0
  71. /package/dist/{az-tool → azdo-tool}/transformers.d.ts +0 -0
  72. /package/src/{az-tool → azdo-tool}/extract-option-value.ts +0 -0
  73. /package/src/{az-tool → azdo-tool}/transformers.ts +0 -0
@@ -702,6 +702,99 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
702
702
  );
703
703
  });
704
704
 
705
+ // GitHub rejects GraphQL `mergePullRequest` for a PR that belongs to a native stack and points at
706
+ // the asynchronous REST merge instead. `gh pr merge` only speaks GraphQL.
707
+ const ASYNC_MERGE_REQUIRED_RE = /asynchronous merge/i;
708
+ const ASYNC_MERGE_POLL_INTERVAL_MS = 2000;
709
+ const MAX_ASYNC_MERGE_WAIT_SECONDS = 120;
710
+
711
+ type AsyncMergeResult = {
712
+ status: "pending" | "merged" | "enqueued" | "failed";
713
+ details?: { message?: string; uuid?: string; sha?: string };
714
+ };
715
+
716
+ const asyncMergeFailure = (
717
+ pr: number,
718
+ message: string,
719
+ hint: string,
720
+ reason: "merge_queue" | "unknown" = "unknown",
721
+ ) =>
722
+ new GitHubMergeError({
723
+ message: `Failed to merge PR #${pr} asynchronously: ${message}`,
724
+ reason,
725
+ hint,
726
+ nextCommand: `agent-tools-gh pr view --pr ${pr}`,
727
+ });
728
+
729
+ const mergeViaAsyncApi = Effect.fn("pr.mergeViaAsyncApi")(function* (opts: {
730
+ pr: number;
731
+ strategy: MergeStrategy;
732
+ }) {
733
+ const gh = yield* GitHubService;
734
+ const repo = yield* gh.getRepoInfo();
735
+ const asyncPath = `repos/${repo.owner}/${repo.name}/pulls/${opts.pr}/merge-async`;
736
+
737
+ let latest = yield* gh.runGhJson<AsyncMergeResult>([
738
+ "api",
739
+ "--method",
740
+ "PUT",
741
+ asyncPath,
742
+ "-f",
743
+ `merge_method=${opts.strategy}`,
744
+ ]);
745
+
746
+ const uuid = latest.details?.uuid;
747
+ if (latest.status === "pending" && uuid !== undefined) {
748
+ const start = yield* Clock.currentTimeMillis;
749
+ const deadlineMs = Number(start) + MAX_ASYNC_MERGE_WAIT_SECONDS * 1000;
750
+ let timedOut = false;
751
+
752
+ // Effect.whileLoop (not recursion) so TestClock.adjust can advance Effect.sleep without real waits.
753
+ yield* Effect.whileLoop({
754
+ while: () => latest.status === "pending" && !timedOut,
755
+ body: () =>
756
+ Effect.gen(function* () {
757
+ const now = yield* Clock.currentTimeMillis;
758
+ if (Number(now) >= deadlineMs) {
759
+ timedOut = true;
760
+ return;
761
+ }
762
+ const remaining = deadlineMs - Number(now);
763
+ yield* Effect.sleep(Duration.millis(Math.min(ASYNC_MERGE_POLL_INTERVAL_MS, remaining)));
764
+ latest = yield* gh.runGhJson<AsyncMergeResult>(["api", `${asyncPath}/${uuid}`]);
765
+ }),
766
+ step: () => undefined,
767
+ });
768
+ }
769
+
770
+ if (latest.status === "merged") {
771
+ return latest.details?.sha ?? null;
772
+ }
773
+
774
+ if (latest.status === "enqueued") {
775
+ return yield* asyncMergeFailure(
776
+ opts.pr,
777
+ latest.details?.message ?? "the pull request entered a merge queue",
778
+ "The merge queue owns the merge from here; it is not merged yet. Watch the PR until the queue drains.",
779
+ "merge_queue",
780
+ );
781
+ }
782
+
783
+ if (latest.status === "pending") {
784
+ return yield* asyncMergeFailure(
785
+ opts.pr,
786
+ `still pending after ${MAX_ASYNC_MERGE_WAIT_SECONDS}s`,
787
+ "The asynchronous merge is still running. Re-check the PR state before retrying so the merge is not requested twice.",
788
+ );
789
+ }
790
+
791
+ return yield* asyncMergeFailure(
792
+ opts.pr,
793
+ latest.details?.message ?? "the merge request failed",
794
+ "Inspect the PR state and branch protections, then retry.",
795
+ );
796
+ });
797
+
705
798
  export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
706
799
  pr: number;
707
800
  strategy: MergeStrategy;
@@ -847,77 +940,92 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
847
940
 
848
941
  const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
849
942
 
850
- const mergeResult = yield* gh.runGh(mergeArgs).pipe(
851
- Effect.catch((error) =>
852
- rollbackRetargets.pipe(
853
- Effect.andThen(
854
- (
855
- rollbackFailed,
856
- ): Effect.Effect<never, GitHubNotFoundError | GitHubAuthError | GitHubMergeError> => {
857
- const rollbackNote =
858
- rollbackFailed.length > 0
859
- ? ` ROLLBACK INCOMPLETE: dependent PR(s) ${rollbackFailed
860
- .map((child) => `#${child}`)
861
- .join(", ")} are still retargeted to \`${info.baseRefName}\`; ` +
862
- `manually restore their base to \`${info.headRefName}\`.`
863
- : "";
864
-
865
- if (error._tag !== "GitHubCommandError") {
866
- return rollbackNote === ""
867
- ? Effect.fail(error)
868
- : Console.error(rollbackNote.trim()).pipe(Effect.andThen(Effect.fail(error)));
869
- }
870
-
871
- const stderr = error.stderr.toLowerCase();
872
-
873
- if (stderr.includes("merge conflict") || stderr.includes("conflicts")) {
874
- return Effect.fail(
875
- new GitHubMergeError({
876
- message: `PR #${opts.pr} has merge conflicts`,
877
- reason: "conflicts",
878
- hint: `Resolve merge conflicts locally, push the fix, then retry the merge.${rollbackNote}`,
879
- nextCommand: `gh pr diff ${opts.pr}`,
880
- }),
881
- );
882
- }
883
-
884
- if (stderr.includes("required status check") || stderr.includes("checks")) {
885
- return Effect.fail(
886
- new GitHubMergeError({
887
- message: `PR #${opts.pr} has failing required checks`,
888
- reason: "checks_failing",
889
- hint: `Wait for CI checks to pass or investigate failures before merging.${rollbackNote}`,
890
- nextCommand: `agent-tools-gh pr checks --pr ${opts.pr}`,
891
- retryable: true,
892
- }),
893
- );
894
- }
895
-
896
- if (stderr.includes("protected branch")) {
897
- return Effect.fail(
898
- new GitHubMergeError({
899
- message: `PR #${opts.pr} targets a protected branch`,
900
- reason: "branch_protected",
901
- hint: `This branch has protection rules. Ensure required reviews and checks are satisfied, or ask a repo admin.${rollbackNote}`,
902
- }),
903
- );
904
- }
905
-
906
- return Effect.fail(
907
- new GitHubMergeError({
908
- message: `Failed to merge PR #${opts.pr}: ${error.stderr}`,
909
- reason: "unknown",
910
- hint: `Check the PR state and branch protections. The PR may already be merged or closed.${rollbackNote}`,
911
- nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
912
- }),
913
- );
914
- },
915
- ),
943
+ const mergedSha = yield* gh
944
+ .runGh(mergeArgs)
945
+ .pipe(
946
+ Effect.map((result) => result.stdout.match(/([0-9a-f]{7,40})/)?.[1] ?? null),
947
+ Effect.catchTag("GitHubCommandError", (error) =>
948
+ ASYNC_MERGE_REQUIRED_RE.test(error.stderr)
949
+ ? mergeViaAsyncApi({ pr: opts.pr, strategy: opts.strategy })
950
+ : Effect.fail(error),
916
951
  ),
917
- ),
918
- );
919
-
920
- const shaMatch = mergeResult.stdout.match(/([0-9a-f]{7,40})/);
952
+ )
953
+ .pipe(
954
+ Effect.catch((error) =>
955
+ // A queued merge still lands, and its head branch disappears with it. Rolling the
956
+ // dependents back onto that branch would close them (cli/cli#1168), so keep the retargets.
957
+ error._tag === "GitHubMergeError" && error.reason === "merge_queue"
958
+ ? Effect.fail(error)
959
+ : rollbackRetargets.pipe(
960
+ Effect.andThen(
961
+ (
962
+ rollbackFailed,
963
+ ): Effect.Effect<
964
+ never,
965
+ GitHubNotFoundError | GitHubAuthError | GitHubMergeError
966
+ > => {
967
+ const rollbackNote =
968
+ rollbackFailed.length > 0
969
+ ? ` ROLLBACK INCOMPLETE: dependent PR(s) ${rollbackFailed
970
+ .map((child) => `#${child}`)
971
+ .join(", ")} are still retargeted to \`${info.baseRefName}\`; ` +
972
+ `manually restore their base to \`${info.headRefName}\`.`
973
+ : "";
974
+
975
+ if (error._tag !== "GitHubCommandError") {
976
+ return rollbackNote === ""
977
+ ? Effect.fail(error)
978
+ : Console.error(rollbackNote.trim()).pipe(Effect.andThen(Effect.fail(error)));
979
+ }
980
+
981
+ const stderr = error.stderr.toLowerCase();
982
+
983
+ if (stderr.includes("merge conflict") || stderr.includes("conflicts")) {
984
+ return Effect.fail(
985
+ new GitHubMergeError({
986
+ message: `PR #${opts.pr} has merge conflicts`,
987
+ reason: "conflicts",
988
+ hint: `Resolve merge conflicts locally, push the fix, then retry the merge.${rollbackNote}`,
989
+ nextCommand: `gh pr diff ${opts.pr}`,
990
+ }),
991
+ );
992
+ }
993
+
994
+ if (stderr.includes("required status check") || stderr.includes("checks")) {
995
+ return Effect.fail(
996
+ new GitHubMergeError({
997
+ message: `PR #${opts.pr} has failing required checks`,
998
+ reason: "checks_failing",
999
+ hint: `Wait for CI checks to pass or investigate failures before merging.${rollbackNote}`,
1000
+ nextCommand: `agent-tools-gh pr checks --pr ${opts.pr}`,
1001
+ retryable: true,
1002
+ }),
1003
+ );
1004
+ }
1005
+
1006
+ if (stderr.includes("protected branch")) {
1007
+ return Effect.fail(
1008
+ new GitHubMergeError({
1009
+ message: `PR #${opts.pr} targets a protected branch`,
1010
+ reason: "branch_protected",
1011
+ hint: `This branch has protection rules. Ensure required reviews and checks are satisfied, or ask a repo admin.${rollbackNote}`,
1012
+ }),
1013
+ );
1014
+ }
1015
+
1016
+ return Effect.fail(
1017
+ new GitHubMergeError({
1018
+ message: `Failed to merge PR #${opts.pr}: ${error.stderr}`,
1019
+ reason: "unknown",
1020
+ hint: `Check the PR state and branch protections. The PR may already be merged or closed.${rollbackNote}`,
1021
+ nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
1022
+ }),
1023
+ );
1024
+ },
1025
+ ),
1026
+ ),
1027
+ ),
1028
+ );
921
1029
 
922
1030
  // `gh pr merge --delete-branch` aborts its remote delete when the head branch is checked out in a
923
1031
  // worktree; deleting the remote ref explicitly is worktree-independent. Local cleanup is separate.
@@ -944,7 +1052,7 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
944
1052
  merged: true,
945
1053
  strategy: opts.strategy,
946
1054
  branchDeleted: willDeleteBranch,
947
- sha: shaMatch?.[1] ?? null,
1055
+ sha: mergedSha,
948
1056
  retargetedChildren: retargetedChildren.length > 0 ? retargetedChildren : undefined,
949
1057
  branchDeleteSkipped: branchDeleteSkipped ? true : undefined,
950
1058
  };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Azure CLI surfaces that return credential material.
3
+ *
4
+ * Shared because two tools reach the Azure CLI: az-tool for the platform, and
5
+ * azdo-tool for the `acr`/`account` groups it still passes through for
6
+ * backwards compatibility. Both must refuse the same commands — a term added
7
+ * to one list and not the other reopens a credential read through whichever
8
+ * tool was missed.
9
+ */
10
+
11
+ /** Read-shaped verbs that return credential material, in any command group. */
12
+ export const CREDENTIAL_BLOCKED_VERBS = [
13
+ "get-access-token",
14
+ "get-credentials",
15
+ "get-secrets",
16
+ "list-account-keys",
17
+ "list-connection-strings",
18
+ "list-keys",
19
+ "list-publishing-credentials",
20
+ "list-publishing-profiles",
21
+ "list-secrets",
22
+ "show-connection-string",
23
+ "show-secret",
24
+ ] as const;
25
+
26
+ /**
27
+ * Command group segments that carry credential material. For these the listing
28
+ * itself returns values — `az storage account keys list` and
29
+ * `az webapp config appsettings list` both print secrets.
30
+ */
31
+ export const CREDENTIAL_BLOCKED_SEGMENTS = [
32
+ "admin-key",
33
+ "appsettings",
34
+ "connection-string",
35
+ "credential",
36
+ "credentials",
37
+ "key",
38
+ "keys",
39
+ "password",
40
+ "query-key",
41
+ "sas",
42
+ "secret",
43
+ "secrets",
44
+ ] as const;
45
+
46
+ export const isCredentialBlockedVerb = (word: string): boolean =>
47
+ (CREDENTIAL_BLOCKED_VERBS as readonly string[]).includes(word);
48
+
49
+ export const isCredentialBlockedSegment = (word: string): boolean =>
50
+ (CREDENTIAL_BLOCKED_SEGMENTS as readonly string[]).includes(word);
@@ -1,6 +1,6 @@
1
1
  export const REQUIRED_BINARIES = {
2
2
  az: {
3
- tools: "az-tool",
3
+ tools: "az-tool and azdo-tool",
4
4
  install:
5
5
  "Install it with `brew install azure-cli` (macOS) or see https://learn.microsoft.com/cli/azure/install-azure-cli.",
6
6
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/az-tool/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3F,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC;;GAEG;AACH,eAAO,MAAM,gBAAgB,yLAuD3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;UAOP,QAAQ,EAAE;YACN,QAAQ,EAAE;wIAE9B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY,qLAkCvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB,iMAwC7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB,wLAgC7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;0IAazB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;aAClB,MAAM;iBACF,MAAM;UACb,MAAM;oLAwDZ,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"extract-option-value.d.ts","sourceRoot":"","sources":["../../src/az-tool/extract-option-value.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAQpB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"transformers.d.ts","sourceRoot":"","sources":["../../src/az-tool/transformers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;CAChC,CAAC;AAwCF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoC/D;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,GAAG,WAAW,CA6B5D;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAQjE"}
File without changes
File without changes