@blogic-cz/agent-tools 1.7.5 → 1.9.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 (142) hide show
  1. package/README.md +2 -1
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/types.d.ts +1 -0
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/credential-guard/index.d.ts.map +1 -1
  6. package/dist/db-tool/service.d.ts.map +1 -1
  7. package/dist/gh-tool/branch.d.ts +2 -3
  8. package/dist/gh-tool/branch.d.ts.map +1 -1
  9. package/dist/gh-tool/environment.d.ts +1 -1
  10. package/dist/gh-tool/environment.d.ts.map +1 -1
  11. package/dist/gh-tool/gist.d.ts +8 -28
  12. package/dist/gh-tool/gist.d.ts.map +1 -1
  13. package/dist/gh-tool/issue/commands.d.ts +11 -9
  14. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  15. package/dist/gh-tool/issue/core.d.ts +116 -9
  16. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  17. package/dist/gh-tool/issue/triage.d.ts +6 -6
  18. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  19. package/dist/gh-tool/label.d.ts +20 -0
  20. package/dist/gh-tool/label.d.ts.map +1 -0
  21. package/dist/gh-tool/pr/commands.d.ts +53 -46
  22. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  23. package/dist/gh-tool/pr/core.d.ts +156 -86
  24. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/review.d.ts +16 -16
  26. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  27. package/dist/gh-tool/pr/snapshot.d.ts +9 -0
  28. package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
  29. package/dist/gh-tool/pr/stack-read.d.ts +1 -6
  30. package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
  31. package/dist/gh-tool/pr/stack.d.ts +3 -3
  32. package/dist/gh-tool/pr/stack.d.ts.map +1 -1
  33. package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
  34. package/dist/gh-tool/read/api.d.ts.map +1 -0
  35. package/dist/gh-tool/read/cache.d.ts +44 -0
  36. package/dist/gh-tool/read/cache.d.ts.map +1 -0
  37. package/dist/gh-tool/read/catalog.d.ts +170 -0
  38. package/dist/gh-tool/read/catalog.d.ts.map +1 -0
  39. package/dist/gh-tool/read/classes.d.ts +98 -0
  40. package/dist/gh-tool/read/classes.d.ts.map +1 -0
  41. package/dist/gh-tool/read/commands.d.ts +3 -0
  42. package/dist/gh-tool/read/commands.d.ts.map +1 -0
  43. package/dist/gh-tool/read/commits.d.ts +10 -0
  44. package/dist/gh-tool/read/commits.d.ts.map +1 -0
  45. package/dist/gh-tool/read/gated.d.ts +14 -0
  46. package/dist/gh-tool/read/gated.d.ts.map +1 -0
  47. package/dist/gh-tool/read/index.d.ts +5 -0
  48. package/dist/gh-tool/read/index.d.ts.map +1 -0
  49. package/dist/gh-tool/read/issues.d.ts +80 -0
  50. package/dist/gh-tool/read/issues.d.ts.map +1 -0
  51. package/dist/gh-tool/read/keys.d.ts +64 -0
  52. package/dist/gh-tool/read/keys.d.ts.map +1 -0
  53. package/dist/gh-tool/read/porcelain.d.ts +68 -0
  54. package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
  55. package/dist/gh-tool/read/pulls.d.ts +137 -0
  56. package/dist/gh-tool/read/pulls.d.ts.map +1 -0
  57. package/dist/gh-tool/read/reads.d.ts +220 -0
  58. package/dist/gh-tool/read/reads.d.ts.map +1 -0
  59. package/dist/gh-tool/read/rest.d.ts +49 -0
  60. package/dist/gh-tool/read/rest.d.ts.map +1 -0
  61. package/dist/gh-tool/read/review-guards.d.ts +20 -0
  62. package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
  63. package/dist/gh-tool/read/review-model.d.ts +190 -0
  64. package/dist/gh-tool/read/review-model.d.ts.map +1 -0
  65. package/dist/gh-tool/read/reviews.d.ts +9 -0
  66. package/dist/gh-tool/read/reviews.d.ts.map +1 -0
  67. package/dist/gh-tool/read/stacks.d.ts +7 -0
  68. package/dist/gh-tool/read/stacks.d.ts.map +1 -0
  69. package/dist/gh-tool/read/store.d.ts +70 -0
  70. package/dist/gh-tool/read/store.d.ts.map +1 -0
  71. package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
  72. package/dist/gh-tool/read/transport.d.ts.map +1 -0
  73. package/dist/gh-tool/read/waits.d.ts +22 -0
  74. package/dist/gh-tool/read/waits.d.ts.map +1 -0
  75. package/dist/gh-tool/read/writes.d.ts +16 -0
  76. package/dist/gh-tool/read/writes.d.ts.map +1 -0
  77. package/dist/gh-tool/release.d.ts +10 -26
  78. package/dist/gh-tool/release.d.ts.map +1 -1
  79. package/dist/gh-tool/repo.d.ts +9 -4
  80. package/dist/gh-tool/repo.d.ts.map +1 -1
  81. package/dist/gh-tool/types.d.ts +1 -1
  82. package/dist/gh-tool/types.d.ts.map +1 -1
  83. package/dist/gh-tool/workflow.d.ts +43 -21
  84. package/dist/gh-tool/workflow.d.ts.map +1 -1
  85. package/dist/observability-tool/shared.d.ts.map +1 -1
  86. package/dist/shared/content-security.d.ts +2 -0
  87. package/dist/shared/content-security.d.ts.map +1 -1
  88. package/package.json +7 -6
  89. package/schemas/agent-tools.schema.json +5 -0
  90. package/src/api-tool/proxy-entry.ts +3 -1
  91. package/src/config/loader.ts +1 -0
  92. package/src/config/types.ts +1 -0
  93. package/src/credential-guard/index.ts +169 -1
  94. package/src/db-tool/service.ts +4 -1
  95. package/src/gh-tool/branch.ts +5 -8
  96. package/src/gh-tool/environment.ts +2 -1
  97. package/src/gh-tool/gist.ts +8 -35
  98. package/src/gh-tool/issue/commands.ts +12 -2
  99. package/src/gh-tool/issue/core.ts +58 -122
  100. package/src/gh-tool/issue/triage.ts +4 -26
  101. package/src/gh-tool/label.ts +57 -0
  102. package/src/gh-tool/main.ts +24 -4
  103. package/src/gh-tool/pr/commands.ts +17 -15
  104. package/src/gh-tool/pr/core.ts +486 -582
  105. package/src/gh-tool/pr/review.ts +106 -454
  106. package/src/gh-tool/pr/snapshot.ts +23 -0
  107. package/src/gh-tool/pr/stack-read.ts +1 -73
  108. package/src/gh-tool/pr/stack.ts +37 -36
  109. package/src/gh-tool/{api.ts → read/api.ts} +40 -7
  110. package/src/gh-tool/read/cache.ts +192 -0
  111. package/src/gh-tool/read/catalog.ts +310 -0
  112. package/src/gh-tool/read/classes.ts +31 -0
  113. package/src/gh-tool/read/commands.ts +25 -0
  114. package/src/gh-tool/read/commits.ts +28 -0
  115. package/src/gh-tool/read/gated.ts +56 -0
  116. package/src/gh-tool/read/index.ts +4 -0
  117. package/src/gh-tool/read/issues.ts +124 -0
  118. package/src/gh-tool/read/keys.ts +104 -0
  119. package/src/gh-tool/read/porcelain.ts +58 -0
  120. package/src/gh-tool/read/pulls.ts +266 -0
  121. package/src/gh-tool/read/reads.ts +306 -0
  122. package/src/gh-tool/read/rest.ts +111 -0
  123. package/src/gh-tool/read/review-guards.ts +61 -0
  124. package/src/gh-tool/read/review-model.ts +286 -0
  125. package/src/gh-tool/read/reviews.ts +114 -0
  126. package/src/gh-tool/read/stacks.ts +73 -0
  127. package/src/gh-tool/read/store.ts +504 -0
  128. package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
  129. package/src/gh-tool/read/waits.ts +52 -0
  130. package/src/gh-tool/read/writes.ts +37 -0
  131. package/src/gh-tool/release.ts +15 -168
  132. package/src/gh-tool/repo.ts +26 -78
  133. package/src/gh-tool/types.ts +1 -1
  134. package/src/gh-tool/workflow.ts +123 -214
  135. package/src/observability-tool/shared.ts +2 -1
  136. package/src/shared/content-security.ts +10 -1
  137. package/src/shared/credential-patterns.ts +1 -1
  138. package/dist/gh-tool/api.d.ts.map +0 -1
  139. package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
  140. package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
  141. package/dist/gh-tool/service.d.ts.map +0 -1
  142. package/src/gh-tool/pr/check-status-cache.ts +0 -142
@@ -3,7 +3,9 @@ import { startProxy } from "./server";
3
3
  import { readPrivateFile } from "./store";
4
4
  import { safeError } from "./errors";
5
5
 
6
- if (Bun.argv.length !== 4 || Bun.argv[2] !== "--config") {
6
+ if (Bun.argv.length === 3 && (Bun.argv[2] === "--help" || Bun.argv[2] === "-h")) {
7
+ process.stdout.write("Usage: api-proxy --config /operator/private/operator.json\n");
8
+ } else if (Bun.argv.length !== 4 || Bun.argv[2] !== "--config") {
7
9
  process.stderr.write("Usage: api-proxy --config /operator/private/operator.json\n");
8
10
  process.exitCode = 1;
9
11
  } else {
@@ -11,6 +11,7 @@ const CliToolOverrideSchema = Schema.Struct({
11
11
  });
12
12
 
13
13
  const CredentialGuardConfigSchema = Schema.Struct({
14
+ chainCopyDestinations: Schema.optionalKey(Schema.Array(Schema.String)),
14
15
  allowedEnvironmentVariables: Schema.optionalKey(Schema.Array(Schema.String)),
15
16
  additionalBlockedPaths: Schema.optionalKey(Schema.Array(Schema.String)),
16
17
  additionalAllowedPaths: Schema.optionalKey(Schema.Array(Schema.String)),
@@ -187,6 +187,7 @@ export type CliToolOverride = {
187
187
  export type CredentialGuardConfig = {
188
188
  /** Exact, case-sensitive names approved for static printenv reads. Empty by default. */
189
189
  allowedEnvironmentVariables?: string[];
190
+ chainCopyDestinations?: string[];
190
191
  additionalBlockedPaths?: string[];
191
192
  additionalAllowedPaths?: string[];
192
193
  additionalBlockedCliTools?: CliToolOverride[];
@@ -18,6 +18,7 @@ import type { CliToolOverride, CredentialGuardConfig } from "#config/types";
18
18
  import { redactSensitiveText } from "#shared/content-security";
19
19
  import { SECRET_PATTERNS } from "#shared/credential-patterns";
20
20
  import { posix } from "node:path";
21
+ import { lstatSync, realpathSync } from "node:fs";
21
22
  import { passiveJqProgramIndex } from "./passive-jq";
22
23
 
23
24
  export { findSecretMatches } from "#shared/credential-patterns";
@@ -2558,6 +2559,131 @@ function hasStaticEnvironmentRead(
2558
2559
  );
2559
2560
  }
2560
2561
 
2562
+ function commandChainSources(command: string): string[] | undefined {
2563
+ const parts: string[] = [];
2564
+ let start = 0;
2565
+ let quote: "'" | '"' | "`" | undefined;
2566
+ let depth = 0;
2567
+ for (let index = 0; index < command.length; index++) {
2568
+ const char = command[index];
2569
+ if (char === "\\" && quote !== "'") {
2570
+ index++;
2571
+ } else if (quote) {
2572
+ if (char === quote) quote = undefined;
2573
+ } else if (char === "'" || char === '"' || char === "`") {
2574
+ quote = char;
2575
+ } else if (char === "#") {
2576
+ return undefined;
2577
+ } else if (char === "(") {
2578
+ depth++;
2579
+ } else if (char === ")") {
2580
+ if (--depth < 0) return undefined;
2581
+ } else if (!depth && (char === ";" || command.startsWith("&&", index))) {
2582
+ const part = command.slice(start, index).trim();
2583
+ if (!part || parts.length >= 31) return undefined;
2584
+ parts.push(part);
2585
+ if (char === "&") index++;
2586
+ start = index + 1;
2587
+ }
2588
+ }
2589
+ const last = command.slice(start).trim();
2590
+ return !quote && !depth && last && parts.length ? [...parts, last] : undefined;
2591
+ }
2592
+
2593
+ type LiteralChainPart = { source: string; argv: string[]; redirects: boolean };
2594
+
2595
+ function literalCommandChain(command: string): LiteralChainPart[] | undefined {
2596
+ const literal = literalBindingText(command, "", "");
2597
+ if (!literal || /[|&\r\n]/.test(literal.syntax.replaceAll("&&", ""))) return undefined;
2598
+ const boundaries = [...literal.syntax.matchAll(/&&|;/g)];
2599
+ if (!boundaries.length || boundaries.length > 31) return undefined;
2600
+ const parts: LiteralChainPart[] = [];
2601
+ let start = 0;
2602
+ for (const end of [...boundaries.map((match) => match.index), command.length]) {
2603
+ const source = command.slice(start, end).trim();
2604
+ const parsed = parseStaticShellCommands(source);
2605
+ if (
2606
+ !parsed ||
2607
+ typeof parsed === "string" ||
2608
+ parsed.pipelines.length !== 1 ||
2609
+ parsed.pipelines[0]?.length !== 1
2610
+ )
2611
+ return undefined;
2612
+ const argv = unwrapProofCommand(parsed.pipelines[0][0] ?? [], "\0");
2613
+ if (!argv) return undefined;
2614
+ parts.push({ source, argv, redirects: parsed.redirects.length > 0 });
2615
+ start = end + (literal.syntax.startsWith("&&", end) ? 2 : 1);
2616
+ }
2617
+ return parts;
2618
+ }
2619
+
2620
+ function isIndependentHerdrOperation(argv: string[]): boolean {
2621
+ if (isLiteralHerdrPrompt(argv)) return argv.length === 5;
2622
+ if (argv[0] !== "herdr") return false;
2623
+ if (argv[1] === "agent" && argv[2] === "start") {
2624
+ const separator = argv.indexOf("--", 3);
2625
+ const options = argv.slice(3, separator < 0 ? undefined : separator);
2626
+ const kinds = options.filter((arg) => arg === "--kind" || arg.startsWith("--kind="));
2627
+ return (
2628
+ kinds.length === 1 &&
2629
+ kinds[0] === "--kind" &&
2630
+ options[options.indexOf("--kind") + 1] === "codex"
2631
+ );
2632
+ }
2633
+ if (argv[1] !== "pane") return false;
2634
+ if (argv[2] === "run")
2635
+ return argv.length === 5 && argv[4] === "exec bash ~/.codex/codex-clean.sh bash --norc -i";
2636
+ if (argv[2] !== "wait-output" || argv.length < 6) return false;
2637
+ const seen = new Set<string>();
2638
+ for (let index = 4; index < argv.length; index += 2) {
2639
+ const option = argv[index] ?? "";
2640
+ if (seen.has(option)) return false;
2641
+ seen.add(option);
2642
+ if (!["--match", "--regex", "--timeout"].includes(option) || !argv[index + 1]) return false;
2643
+ if (argv[index] === "--timeout" && !/^\d+$/.test(argv[index + 1] ?? "")) return false;
2644
+ }
2645
+ return true;
2646
+ }
2647
+
2648
+ function isIndependentDataCopy(argv: string[], destinations: string[]): boolean {
2649
+ if (!destinations.length || argv[0] !== "cp" || argv.length !== 3) return false;
2650
+ const source = argv[1] ?? "";
2651
+ const target = argv[2] ?? "";
2652
+ const literalPath = (path: string) =>
2653
+ /^\/[A-Za-z0-9_./ -]+$/.test(path) &&
2654
+ !path.split("/").some((part) => part === "." || part === "..");
2655
+ if (!/^(?:~\/)?[A-Za-z0-9_./-]+$/.test(source) || source.startsWith("-") || !literalPath(target))
2656
+ return false;
2657
+ try {
2658
+ const targetPath = target.replace(/\/+$/, "");
2659
+ const targetStat = lstatSync(targetPath, { throwIfNoEntry: false });
2660
+ if (target.endsWith("/") && !targetStat?.isDirectory()) return false;
2661
+ const destination = targetStat?.isDirectory()
2662
+ ? posix.join(targetPath, posix.basename(source))
2663
+ : targetPath;
2664
+ for (const directory of destinations) {
2665
+ if (!literalPath(directory)) continue;
2666
+ const root = directory.replace(/\/+$/, "");
2667
+ if (!destination.startsWith(root + "/")) continue;
2668
+ if (!lstatSync(root).isDirectory() || realpathSync(root) !== root) return false;
2669
+ const parts = destination.slice(root.length + 1).split("/");
2670
+ let current = root;
2671
+ for (const [index, part] of parts.entries()) {
2672
+ current += "/" + part;
2673
+ const stat = lstatSync(current, { throwIfNoEntry: false });
2674
+ const last = index === parts.length - 1;
2675
+ if (!stat) return last;
2676
+ if (stat.isSymbolicLink() || (!last && !stat.isDirectory())) return false;
2677
+ if (realpathSync(current) !== current) return false;
2678
+ }
2679
+ return true;
2680
+ }
2681
+ } catch {
2682
+ return false;
2683
+ }
2684
+ return false;
2685
+ }
2686
+
2561
2687
  function isLiteralHerdrPrompt(argv: string[]): boolean {
2562
2688
  return argv[0] === "herdr" && argv[1] === "agent" && argv[2] === "prompt" && argv.length >= 5;
2563
2689
  }
@@ -3730,6 +3856,31 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
3730
3856
  });
3731
3857
  if (unterminatedQuote)
3732
3858
  return "cannot prove shell syntax: an unterminated quoted argument was found";
3859
+ const chain = literalCommandChain(command);
3860
+ if (chain?.some(({ argv }) => isLiteralHerdrPrompt(argv)) && !isStaticHerdrPrompt(command)) {
3861
+ let copied = false;
3862
+ for (const [index, part] of chain.entries()) {
3863
+ const reason = getDangerousBashReason(part.source, shellDepth);
3864
+ if (reason) return `cannot prove chain part ${index + 1} (${part.source}): ${reason}`;
3865
+ if (
3866
+ !part.redirects &&
3867
+ isIndependentDataCopy(part.argv, config?.chainCopyDestinations ?? []) &&
3868
+ !copied
3869
+ ) {
3870
+ copied = true;
3871
+ continue;
3872
+ }
3873
+ if (
3874
+ part.redirects ||
3875
+ !isIndependentHerdrOperation(part.argv) ||
3876
+ (copied && !(isLiteralHerdrPrompt(part.argv) && part.argv.length === 5))
3877
+ )
3878
+ return `cannot prove independent chain part ${index + 1} (${part.source}) under the environment/execution policy`;
3879
+ }
3880
+ if (dangerousBashPatterns.some((pattern) => pattern.test(originalCommand)))
3881
+ return "matches a configured dangerous-command pattern";
3882
+ return null;
3883
+ }
3733
3884
  const allowPackageInventory = hasProvedPackageInventoryInvocation(originalCommand);
3734
3885
  const promptWords = isStaticHerdrPrompt(command)
3735
3886
  ? parseStaticShellCommands(command)
@@ -4140,7 +4291,24 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
4140
4291
  if (tool === "bash") {
4141
4292
  const command = extractCommand(args);
4142
4293
 
4143
- const dangerousReason = getDangerousBashReason(command);
4294
+ let dangerousReason = getDangerousBashReason(command);
4295
+ if (dangerousReason && !dangerousReason.includes("chain part")) {
4296
+ let unproved: string | undefined;
4297
+ for (const [index, source] of (commandChainSources(command) ?? []).entries()) {
4298
+ const reason = getDangerousBashReason(source);
4299
+ const parsed = parseStaticShellCommands(source);
4300
+ const literal = literalBindingText(source, "", "");
4301
+ const words = parsed && typeof parsed !== "string" ? parsed.pipelines[0]?.[0] : undefined;
4302
+ const argv = words ? unwrapProofCommand(words, "\0") : undefined;
4303
+ if (reason) {
4304
+ dangerousReason += `; cannot prove chain part ${index + 1} (${source}): ${reason}`;
4305
+ break;
4306
+ }
4307
+ if (!unproved && (!literal || /[<>]/.test(literal.syntax) || !argv))
4308
+ unproved = `cannot prove chain part ${index + 1} (${source}): independent composition is unproved`;
4309
+ }
4310
+ if (!dangerousReason.includes("chain part") && unproved) dangerousReason += `; ${unproved}`;
4311
+ }
4144
4312
  if (dangerousReason) {
4145
4313
  throw new Error(
4146
4314
  `\u{1F6AB} Command blocked: ${dangerousReason}.\n\n` +
@@ -6,7 +6,7 @@ import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./t
6
6
  import { ConfigService } from "#config";
7
7
  import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
8
8
  import { resolveEnvTemplate } from "#shared/env-template";
9
- import { unsafeOutboundTextReason } from "#shared/content-security";
9
+ import { registerResolvedSecret, unsafeOutboundTextReason } from "#shared/content-security";
10
10
  import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
11
11
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
12
12
  import { buildApiProbeArgs } from "#shared/k8s-probe";
@@ -227,18 +227,21 @@ export class DbService extends Context.Service<
227
227
  env: string,
228
228
  ) {
229
229
  if (config.password) {
230
+ registerResolvedSecret(config.password);
230
231
  return config.password;
231
232
  }
232
233
 
233
234
  if (config.passwordEnvVar) {
234
235
  const fromEnv = process.env[config.passwordEnvVar];
235
236
  if (fromEnv) {
237
+ registerResolvedSecret(fromEnv);
236
238
  return fromEnv;
237
239
  }
238
240
 
239
241
  const zshrcEnv = yield* loadEnvFromZshrc();
240
242
  const fromZsh = zshrcEnv[config.passwordEnvVar];
241
243
  if (fromZsh) {
244
+ registerResolvedSecret(fromZsh);
242
245
  return fromZsh;
243
246
  }
244
247
 
@@ -3,7 +3,9 @@ import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted } from "#shared";
5
5
  import type { BranchRenameResult } from "./types";
6
- import { GitHubService } from "./service";
6
+ import { writes } from "./read/writes";
7
+ import { resolveRepo } from "./read/reads";
8
+ import { repoSlug } from "./read/keys";
7
9
 
8
10
  // ---------------------------------------------------------------------------
9
11
  // Internal handlers
@@ -15,8 +17,6 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
15
17
  confirm: boolean;
16
18
  repo: string | null;
17
19
  }) {
18
- const gh = yield* GitHubService;
19
-
20
20
  if (!opts.confirm) {
21
21
  const scope = opts.repo !== null ? ` in ${opts.repo}` : "";
22
22
 
@@ -31,10 +31,7 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
31
31
  return dryRun;
32
32
  }
33
33
 
34
- const repoInfo =
35
- opts.repo !== null
36
- ? opts.repo
37
- : yield* gh.getRepoInfo().pipe(Effect.map((r) => `${r.owner}/${r.name}`));
34
+ const repoInfo = opts.repo !== null ? opts.repo : repoSlug(yield* resolveRepo(null));
38
35
 
39
36
  const args = [
40
37
  "api",
@@ -45,7 +42,7 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
45
42
  `new_name=${opts.newName}`,
46
43
  ];
47
44
 
48
- yield* gh.runGh(args);
45
+ yield* writes.run(args, [`repo:${repoInfo.toLowerCase()}`]);
49
46
 
50
47
  const result: BranchRenameResult = {
51
48
  renamed: true,
@@ -41,10 +41,11 @@ const GH_ENVIRONMENT_NAMES = [
41
41
 
42
42
  export const ghEnvironment = (
43
43
  environment: Record<string, string | undefined> = process.env,
44
+ credential: string | null = null,
44
45
  ): Record<string, string> =>
45
46
  Object.fromEntries(
46
47
  GH_ENVIRONMENT_NAMES.flatMap((name) => {
47
- const value = environment[name];
48
+ const value = name === "GH_TOKEN" && credential !== null ? credential : environment[name];
48
49
  return value === undefined ? [] : [[name, value]];
49
50
  }),
50
51
  );
@@ -1,3 +1,6 @@
1
+ import { passthrough } from "./read/catalog";
2
+ import type { GistApi } from "./read/catalog";
3
+ import { writes } from "./read/writes";
1
4
  import { Command, Flag } from "effect/cli";
2
5
  import { Effect, Option } from "effect";
3
6
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -11,33 +14,12 @@ import {
11
14
  resolveOptionalTextInput,
12
15
  } from "#gh/text-input";
13
16
  import { GitHubCommandError } from "./errors";
14
- import { GitHubService } from "./service";
15
17
 
16
18
  // ---------------------------------------------------------------------------
17
19
  // Types
18
20
  // ---------------------------------------------------------------------------
19
21
 
20
22
  // `gh gist list/view` have no --json, so reads go through `gh api gists` (REST shape).
21
- type GistApiFile = {
22
- filename: string;
23
- language: string | null;
24
- type: string;
25
- size: number;
26
- truncated?: boolean;
27
- content?: string;
28
- };
29
-
30
- type GistApi = {
31
- id: string;
32
- description: string | null;
33
- public: boolean;
34
- html_url: string;
35
- created_at: string;
36
- updated_at: string;
37
- owner: { login: string } | null;
38
- files: Record<string, GistApiFile>;
39
- };
40
-
41
23
  type GistListItem = {
42
24
  id: string;
43
25
  description: string | null;
@@ -247,9 +229,7 @@ const listGists = Effect.fn("gist.listGists")(function* (opts: {
247
229
  limit: number;
248
230
  visibility: string | null;
249
231
  }) {
250
- const gh = yield* GitHubService;
251
-
252
- const gists = yield* gh.runGhJson<GistApi[]>(["api", `gists?per_page=${opts.limit}`]);
232
+ const gists = yield* passthrough.gistList(opts.limit);
253
233
 
254
234
  return gists
255
235
  .filter(
@@ -264,9 +244,7 @@ const viewGist = Effect.fn("gist.viewGist")(function* (opts: {
264
244
  filename: string | null;
265
245
  withContent: boolean;
266
246
  }) {
267
- const gh = yield* GitHubService;
268
-
269
- const gist = yield* gh.runGhJson<GistApi>(["api", `gists/${opts.id}`]);
247
+ const gist = yield* passthrough.gistView(opts.id);
270
248
 
271
249
  return toGistDetail(gist, { filename: opts.filename, withContent: opts.withContent });
272
250
  });
@@ -276,7 +254,6 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
276
254
  description: string | null;
277
255
  public: boolean;
278
256
  }) {
279
- const gh = yield* GitHubService;
280
257
  return yield* Effect.scoped(
281
258
  Effect.gen(function* () {
282
259
  const snapshots: string[] = [];
@@ -298,7 +275,7 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
298
275
  args.push("--public");
299
276
  }
300
277
 
301
- const result = yield* gh.runGh(args);
278
+ const result = yield* writes.run(args, []);
302
279
  const url =
303
280
  result.stdout
304
281
  .trim()
@@ -333,8 +310,6 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
333
310
  filename: string | null;
334
311
  sourcePath: string | null;
335
312
  }) {
336
- const gh = yield* GitHubService;
337
-
338
313
  const args = ["gist", "edit", opts.id];
339
314
  const changes: string[] = [];
340
315
 
@@ -371,7 +346,7 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
371
346
  args.push("--filename", opts.filename);
372
347
  }
373
348
 
374
- yield* gh.runGh(args);
349
+ yield* writes.run(args, []);
375
350
 
376
351
  const edited: GistEditResult = { edited: true, id: opts.id, changes };
377
352
 
@@ -379,8 +354,6 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
379
354
  });
380
355
 
381
356
  const deleteGist = Effect.fn("gist.deleteGist")(function* (opts: { id: string; confirm: boolean }) {
382
- const gh = yield* GitHubService;
383
-
384
357
  if (!opts.confirm) {
385
358
  const dryRun: GistDeleteResult = {
386
359
  deleted: false,
@@ -392,7 +365,7 @@ const deleteGist = Effect.fn("gist.deleteGist")(function* (opts: { id: string; c
392
365
  return dryRun;
393
366
  }
394
367
 
395
- yield* gh.runGh(["gist", "delete", opts.id, "--yes"]);
368
+ yield* writes.run(["gist", "delete", opts.id, "--yes"], []);
396
369
 
397
370
  const deleted: GistDeleteResult = { deleted: true, id: opts.id };
398
371
 
@@ -18,7 +18,7 @@ import {
18
18
  reopenIssue,
19
19
  viewIssue,
20
20
  } from "./core";
21
- import { GitHubService } from "#gh/service";
21
+ import { GitHubService } from "#gh/read/transport";
22
22
 
23
23
  const repoOption = Flag.String("repo").pipe(
24
24
  Flag.withDescription("Target repository profile name or owner/name"),
@@ -35,6 +35,14 @@ export const issueListCommand = Command.make(
35
35
  "list",
36
36
  {
37
37
  format: formatOption,
38
+ includeBody: Flag.Boolean("include-body").pipe(
39
+ Flag.withDescription("Include each issue's body in the result"),
40
+ Flag.withDefault(false),
41
+ ),
42
+ fresh: Flag.Boolean("fresh").pipe(
43
+ Flag.withDescription("Read from GitHub instead of the shared cache"),
44
+ Flag.withDefault(false),
45
+ ),
38
46
  labels: Flag.String("labels").pipe(
39
47
  Flag.withDescription("Filter by label (comma-separated)"),
40
48
  Flag.optional,
@@ -49,12 +57,14 @@ export const issueListCommand = Command.make(
49
57
  Flag.withDefault("open"),
50
58
  ),
51
59
  },
52
- ({ format, labels, limit, repo, state }) =>
60
+ ({ format, fresh, includeBody, labels, limit, repo, state }) =>
53
61
  withRepo(
54
62
  repo,
55
63
  Effect.gen(function* () {
56
64
  const issues = yield* listIssues({
57
65
  labels: Option.getOrNull(labels),
66
+ includeBody,
67
+ fresh,
58
68
  limit,
59
69
  state,
60
70
  });