@blogic-cz/agent-tools 1.5.6 → 1.5.8
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.
- package/README.md +9 -1
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/azdo-tool/service.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts +2 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +13 -0
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/release.d.ts +46 -1
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts +5 -3
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/dist/k8s-tool/service.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +1 -2
- package/dist/logs-tool/main.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +1 -2
- package/dist/session-tool/main.d.ts.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +6 -0
- package/dist/shared/content-security.d.ts.map +1 -0
- package/dist/shared/credential-patterns.d.ts +11 -0
- package/dist/shared/credential-patterns.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts.map +1 -1
- package/dist/shared/error-renderer.d.ts.map +1 -1
- package/dist/shared/format.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/output-boundary.d.ts +3 -0
- package/dist/shared/output-boundary.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/audit-tool/main.ts +3 -1
- package/src/az-tool/main.ts +3 -1
- package/src/az-tool/service.ts +9 -0
- package/src/azdo-tool/main.ts +3 -1
- package/src/azdo-tool/service.ts +17 -0
- package/src/credential-guard/claude-hook.ts +2 -1
- package/src/credential-guard/index.ts +50 -60
- package/src/db-tool/main.ts +3 -1
- package/src/db-tool/service.ts +25 -2
- package/src/gh-tool/gist.ts +63 -64
- package/src/gh-tool/main.ts +3 -1
- package/src/gh-tool/pr/core.ts +10 -3
- package/src/gh-tool/pr/review.ts +199 -12
- package/src/gh-tool/release.ts +104 -29
- package/src/gh-tool/text-input.ts +19 -38
- package/src/k8s-tool/main.ts +3 -1
- package/src/k8s-tool/service.ts +25 -0
- package/src/logs-tool/main.ts +7 -2
- package/src/observability-tool/main.ts +5 -7
- package/src/observability-tool/shared.ts +41 -1
- package/src/session-tool/main.ts +18 -3
- package/src/shared/audit.ts +11 -10
- package/src/shared/content-security.ts +91 -0
- package/src/shared/credential-patterns.ts +62 -0
- package/src/shared/effect-version.ts +2 -1
- package/src/shared/error-renderer.ts +15 -2
- package/src/shared/format.ts +33 -6
- package/src/shared/index.ts +5 -0
- package/src/shared/output-boundary.ts +29 -0
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { CliToolOverride, CredentialGuardConfig } from "#config/types";
|
|
18
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
19
|
+
import { SECRET_PATTERNS } from "#shared/credential-patterns";
|
|
20
|
+
|
|
21
|
+
export { findSecretMatches } from "#shared/credential-patterns";
|
|
22
|
+
export { redactSensitiveText } from "#shared/content-security";
|
|
18
23
|
|
|
19
24
|
// ============================================================================
|
|
20
25
|
// TYPES
|
|
@@ -84,61 +89,6 @@ const DEFAULT_ALLOWED_PATH_PATTERNS: RegExp[] = [
|
|
|
84
89
|
/\.env\.sample$/,
|
|
85
90
|
];
|
|
86
91
|
|
|
87
|
-
/** Patterns to detect secrets in content. */
|
|
88
|
-
const SECRET_PATTERNS = [
|
|
89
|
-
{
|
|
90
|
-
name: "AWS Access Key",
|
|
91
|
-
pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: "GitHub Token",
|
|
95
|
-
pattern: /gh[ps]_[A-Za-z0-9]{36}/,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "GitHub PAT",
|
|
99
|
-
pattern: /github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}/,
|
|
100
|
-
},
|
|
101
|
-
{ name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{48}/ },
|
|
102
|
-
{
|
|
103
|
-
name: "Generic API Key",
|
|
104
|
-
pattern: /(?:api[_-]?key|apikey)["\s:=]+["']?([A-Za-z0-9_-]{20,})["']?/i,
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "Generic Secret",
|
|
108
|
-
pattern:
|
|
109
|
-
/(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
// eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
|
|
113
|
-
name: "Priv" + "ate Key",
|
|
114
|
-
pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "JWT Token",
|
|
118
|
-
pattern: /(?:["'=:\s]|^)eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "Azure SAS Token",
|
|
122
|
-
pattern: /[?&]sig=[A-Za-z0-9%+/=]{20,}/,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: "GCP Service Account Key",
|
|
126
|
-
pattern: /"type"\s*:\s*"service_account"/,
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "Slack Webhook URL",
|
|
130
|
-
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "Discord Webhook URL",
|
|
134
|
-
pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
name: "Database URL",
|
|
138
|
-
pattern: /(?:postgres(?:ql)?|mysql|mongodb):\/\/(?!\$\{)[^:]+:(?!\$\{)[^@]+@/,
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
|
|
142
92
|
/**
|
|
143
93
|
* CLI tools that must use wrapper tools for security and audit.
|
|
144
94
|
*/
|
|
@@ -547,17 +497,38 @@ function isLiteralTextProducer(argv: string[]): boolean {
|
|
|
547
497
|
return isPassiveTextCommand(argv) && argv[0]?.split("/").at(-1) !== "head";
|
|
548
498
|
}
|
|
549
499
|
|
|
500
|
+
function awkProgramIndex(argv: string[]): number | undefined {
|
|
501
|
+
if (argv[0]?.split("/").at(-1) !== "awk") return undefined;
|
|
502
|
+
let index = 1;
|
|
503
|
+
while (index < argv.length) {
|
|
504
|
+
const arg = argv[index] ?? "";
|
|
505
|
+
if (arg === "--") return index + 1 < argv.length ? index + 1 : undefined;
|
|
506
|
+
if (arg === "-F") {
|
|
507
|
+
if (argv[index + 1] === undefined) return undefined;
|
|
508
|
+
index += 2;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (arg.startsWith("-F") && arg.length > 2) {
|
|
512
|
+
index++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (arg.startsWith("-")) return undefined;
|
|
516
|
+
return index;
|
|
517
|
+
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
550
521
|
function isAwkExecution(argv: string[]): boolean {
|
|
551
522
|
if (argv[0]?.split("/").at(-1) !== "awk") return false;
|
|
552
|
-
// Only a literal inline program establishes passive behavior.
|
|
553
|
-
const
|
|
523
|
+
// Only a literal inline program with known-safe options establishes passive behavior.
|
|
524
|
+
const index = awkProgramIndex(argv);
|
|
525
|
+
const program = index === undefined ? undefined : argv[index];
|
|
554
526
|
return (
|
|
555
527
|
!program ||
|
|
556
|
-
program.startsWith("-") ||
|
|
557
528
|
/\b(?:system|getline|ENVIRON)\b|\||\b(?:print|printf)\b[^\n]*>|@(?:include|load)/.test(
|
|
558
529
|
program,
|
|
559
530
|
) ||
|
|
560
|
-
argv.slice(
|
|
531
|
+
argv.slice((index ?? -1) + 1).some((arg) => arg.startsWith("-"))
|
|
561
532
|
);
|
|
562
533
|
}
|
|
563
534
|
|
|
@@ -1404,6 +1375,25 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
1404
1375
|
}
|
|
1405
1376
|
|
|
1406
1377
|
function handleToolExecuteBefore(input: HookInput, output: HookOutput): void {
|
|
1378
|
+
try {
|
|
1379
|
+
enforceToolExecuteBefore(input, output);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1382
|
+
const sanitized = redactSensitiveText(message);
|
|
1383
|
+
if (error instanceof Error) {
|
|
1384
|
+
if (sanitized !== message) {
|
|
1385
|
+
error.message = sanitized;
|
|
1386
|
+
if (error.stack) error.stack = error.stack.replace(message, sanitized);
|
|
1387
|
+
}
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
// Do not preserve an unsanitized non-Error value as the new error's cause.
|
|
1391
|
+
// eslint-disable-next-line eslint/preserve-caught-error
|
|
1392
|
+
throw new Error(sanitized);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function enforceToolExecuteBefore(input: HookInput, output: HookOutput): void {
|
|
1407
1397
|
// Normalize tool name across platforms:
|
|
1408
1398
|
// - AI coding agents pass capitalized: "Bash", "Read", "Write", "Edit"
|
|
1409
1399
|
// - OpenCode MCP tools pass prefixed: "mcp_bash", "mcp_read", "mcp_write", "mcp_edit"
|
|
@@ -1432,7 +1422,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
1432
1422
|
if (detected) {
|
|
1433
1423
|
throw new Error(
|
|
1434
1424
|
`\u{1F6AB} Secret detected: Potential ${detected.name} found in content.\n\n` +
|
|
1435
|
-
`Matched:
|
|
1425
|
+
`Matched: [REDACTED]\n\n` +
|
|
1436
1426
|
`Never commit secrets to code. Use environment variables or secret managers.\n\n` +
|
|
1437
1427
|
`Think this is a false positive? Review the project repository, fix the pattern, and submit a PR.`,
|
|
1438
1428
|
);
|
package/src/db-tool/main.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
VERSION,
|
|
15
15
|
} from "#shared";
|
|
16
16
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
17
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
17
18
|
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
|
|
18
19
|
import { DbConfigService, makeDbConfigLayer } from "./config-service";
|
|
19
20
|
import { DbConnectionError } from "./errors";
|
|
@@ -238,6 +239,7 @@ const mainCommand = Command.make("db-tool", {}).pipe(
|
|
|
238
239
|
|
|
239
240
|
const cli = Command.run(mainCommand, {
|
|
240
241
|
version: VERSION,
|
|
242
|
+
renderErrors: false,
|
|
241
243
|
});
|
|
242
244
|
|
|
243
245
|
const dbConfigLayer = makeDbConfigLayer(profileArg);
|
|
@@ -252,7 +254,7 @@ const MainLayer = DbService.layer.pipe(
|
|
|
252
254
|
Layer.provideMerge(AuditServiceLayer),
|
|
253
255
|
);
|
|
254
256
|
|
|
255
|
-
const program = withAudit("db", cli).pipe(
|
|
257
|
+
const program = withRedactedOutput(withAudit("db", cli)).pipe(
|
|
256
258
|
Effect.provide(MainLayer),
|
|
257
259
|
Effect.tapCause(renderCauseToStderr),
|
|
258
260
|
);
|
package/src/db-tool/service.ts
CHANGED
|
@@ -6,6 +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
10
|
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
10
11
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
11
12
|
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
@@ -501,8 +502,18 @@ export class DbService extends Context.Service<
|
|
|
501
502
|
readOnly: isFullyReadOnly(config),
|
|
502
503
|
});
|
|
503
504
|
|
|
504
|
-
const
|
|
505
|
-
|
|
505
|
+
const unsafeSqlError = (reason: string) =>
|
|
506
|
+
new DbQueryError({
|
|
507
|
+
message: `Refusing to send SQL containing ${reason}.`,
|
|
508
|
+
sql: "[redacted]",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const runSql = (config: DbConfig, password: string, sql: string) => {
|
|
512
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
513
|
+
return unsafeReason === null
|
|
514
|
+
? sqlClient.run(toConnection(config, password), sql)
|
|
515
|
+
: Effect.fail(unsafeSqlError(unsafeReason));
|
|
516
|
+
};
|
|
506
517
|
|
|
507
518
|
const fetchSingleColumn = (config: DbConfig, password: string, sql: string) =>
|
|
508
519
|
runSql(config, password, sql).pipe(
|
|
@@ -772,6 +783,11 @@ export class DbService extends Context.Service<
|
|
|
772
783
|
sql: string,
|
|
773
784
|
limit?: number,
|
|
774
785
|
) {
|
|
786
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
787
|
+
if (unsafeReason !== null) {
|
|
788
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
789
|
+
}
|
|
790
|
+
|
|
775
791
|
const config = yield* getConfigForEnv(env);
|
|
776
792
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
777
793
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
|
@@ -827,6 +843,13 @@ export class DbService extends Context.Service<
|
|
|
827
843
|
mode: SchemaMode,
|
|
828
844
|
table?: string,
|
|
829
845
|
) {
|
|
846
|
+
if (table !== undefined) {
|
|
847
|
+
const unsafeReason = unsafeOutboundTextReason(table);
|
|
848
|
+
if (unsafeReason !== null) {
|
|
849
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
830
853
|
const config = yield* getConfigForEnv(env);
|
|
831
854
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
832
855
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
package/src/gh-tool/gist.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
|
|
5
7
|
import { formatOption, logFormatted } from "#shared";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isSensitivePath,
|
|
10
|
+
readValidatedOutboundFile,
|
|
11
|
+
resolveOptionalTextInput,
|
|
12
|
+
} from "#gh/text-input";
|
|
7
13
|
import { GitHubCommandError } from "./errors";
|
|
8
14
|
import { GitHubService } from "./service";
|
|
9
15
|
|
|
@@ -118,29 +124,13 @@ const validateFilePath = (path: string, command: string) =>
|
|
|
118
124
|
// `gh gist create/edit` read content from files only; inline --body is staged in a temp file
|
|
119
125
|
// whose basename becomes the gist filename.
|
|
120
126
|
const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
121
|
-
body: string;
|
|
127
|
+
body: string | Uint8Array;
|
|
122
128
|
filename: string;
|
|
123
129
|
command: string;
|
|
124
130
|
}) {
|
|
125
131
|
const directory = yield* Effect.acquireRelease(
|
|
126
132
|
Effect.tryPromise({
|
|
127
|
-
try:
|
|
128
|
-
const proc = Bun.spawn(
|
|
129
|
-
["mktemp", "-d", join(process.env.TMPDIR ?? "/tmp", "gh-tool-gist-XXXXXX")],
|
|
130
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
131
|
-
);
|
|
132
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
133
|
-
proc.exited,
|
|
134
|
-
new Response(proc.stdout).text(),
|
|
135
|
-
new Response(proc.stderr).text(),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
if (exitCode !== 0) {
|
|
139
|
-
throw new Error(stderr.trim() || `mktemp exited with code ${exitCode}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return stdout.trim();
|
|
143
|
-
},
|
|
133
|
+
try: () => mkdtemp(join(process.env.TMPDIR ?? tmpdir(), "gh-tool-gist-")),
|
|
144
134
|
catch: (error) =>
|
|
145
135
|
inputError(
|
|
146
136
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -149,20 +139,14 @@ const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
|
149
139
|
}),
|
|
150
140
|
(tempDirectory) =>
|
|
151
141
|
Effect.tryPromise({
|
|
152
|
-
try:
|
|
153
|
-
const proc = Bun.spawn(["rm", "-rf", tempDirectory], {
|
|
154
|
-
stdout: "ignore",
|
|
155
|
-
stderr: "ignore",
|
|
156
|
-
});
|
|
157
|
-
await proc.exited;
|
|
158
|
-
},
|
|
142
|
+
try: () => rm(tempDirectory, { recursive: true, force: true }),
|
|
159
143
|
catch: () => undefined,
|
|
160
144
|
}).pipe(Effect.ignore),
|
|
161
145
|
);
|
|
162
146
|
const path = join(directory, opts.filename);
|
|
163
147
|
|
|
164
148
|
yield* Effect.tryPromise({
|
|
165
|
-
try: () =>
|
|
149
|
+
try: () => writeFile(path, opts.body),
|
|
166
150
|
catch: (error) =>
|
|
167
151
|
inputError(
|
|
168
152
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -293,45 +277,55 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
|
|
|
293
277
|
public: boolean;
|
|
294
278
|
}) {
|
|
295
279
|
const gh = yield* GitHubService;
|
|
280
|
+
return yield* Effect.scoped(
|
|
281
|
+
Effect.gen(function* () {
|
|
282
|
+
const snapshots: string[] = [];
|
|
283
|
+
for (const path of opts.paths) {
|
|
284
|
+
const validated = yield* readValidatedOutboundFile(path, "gh-tool gist create");
|
|
285
|
+
const filename = path.split(/[\\/]/).at(-1) ?? path;
|
|
286
|
+
snapshots.push(
|
|
287
|
+
yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist create" }),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
296
290
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const args = ["gist", "create", ...opts.paths];
|
|
291
|
+
const args = ["gist", "create", ...snapshots];
|
|
300
292
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
if (opts.description !== null) {
|
|
294
|
+
args.push("--desc", opts.description);
|
|
295
|
+
}
|
|
304
296
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
297
|
+
if (opts.public) {
|
|
298
|
+
args.push("--public");
|
|
299
|
+
}
|
|
308
300
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
301
|
+
const result = yield* gh.runGh(args);
|
|
302
|
+
const url =
|
|
303
|
+
result.stdout
|
|
304
|
+
.trim()
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((line) => line.trim())
|
|
307
|
+
.findLast((line) => line.startsWith("https://")) ?? "";
|
|
308
|
+
|
|
309
|
+
if (url === "") {
|
|
310
|
+
return yield* Effect.fail(
|
|
311
|
+
inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
322
314
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
315
|
+
const created: GistCreateResult = {
|
|
316
|
+
created: true,
|
|
317
|
+
id: url.split("/").at(-1) ?? "",
|
|
318
|
+
url,
|
|
319
|
+
public: opts.public,
|
|
320
|
+
files: opts.paths.map((path) => path.split(/[\\/]/).at(-1) ?? path),
|
|
321
|
+
};
|
|
330
322
|
|
|
331
|
-
|
|
323
|
+
return created;
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
332
326
|
});
|
|
333
327
|
|
|
334
|
-
const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
328
|
+
export const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
335
329
|
id: string;
|
|
336
330
|
description: string | null;
|
|
337
331
|
add: string | null;
|
|
@@ -341,15 +335,13 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
341
335
|
}) {
|
|
342
336
|
const gh = yield* GitHubService;
|
|
343
337
|
|
|
344
|
-
for (const path of [opts.sourcePath, opts.add]) {
|
|
345
|
-
if (path !== null) yield* validateOutboundFile(path, "gh-tool gist edit");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
338
|
const args = ["gist", "edit", opts.id];
|
|
349
339
|
const changes: string[] = [];
|
|
350
340
|
|
|
351
341
|
if (opts.sourcePath !== null) {
|
|
352
|
-
|
|
342
|
+
const validated = yield* readValidatedOutboundFile(opts.sourcePath, "gh-tool gist edit");
|
|
343
|
+
const filename = opts.filename ?? opts.sourcePath.split(/[\\/]/).at(-1) ?? "";
|
|
344
|
+
args.push(yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist edit" }));
|
|
353
345
|
changes.push(`content:${opts.filename ?? opts.sourcePath.split("/").at(-1)}`);
|
|
354
346
|
}
|
|
355
347
|
|
|
@@ -359,7 +351,14 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
359
351
|
}
|
|
360
352
|
|
|
361
353
|
if (opts.add !== null) {
|
|
362
|
-
|
|
354
|
+
const validated = yield* readValidatedOutboundFile(opts.add, "gh-tool gist edit");
|
|
355
|
+
const filename = opts.add.split(/[\\/]/).at(-1) ?? opts.add;
|
|
356
|
+
const snapshot = yield* stageBody({
|
|
357
|
+
body: validated.bytes,
|
|
358
|
+
filename,
|
|
359
|
+
command: "gh-tool gist edit",
|
|
360
|
+
});
|
|
361
|
+
args.push("--add", snapshot);
|
|
363
362
|
changes.push(`add:${opts.add.split("/").at(-1)}`);
|
|
364
363
|
}
|
|
365
364
|
|
package/src/gh-tool/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Effect, Layer } from "effect";
|
|
|
5
5
|
|
|
6
6
|
import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
|
|
7
7
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
8
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
8
9
|
import { ConfigServiceLayer } from "#config";
|
|
9
10
|
import {
|
|
10
11
|
issueCloseCommand,
|
|
@@ -240,6 +241,7 @@ WORKFLOW FOR AI AGENTS:
|
|
|
240
241
|
|
|
241
242
|
const cli = Command.run(mainCommand, {
|
|
242
243
|
version: VERSION,
|
|
244
|
+
renderErrors: false,
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
const MainLayer = GitHubService.layer.pipe(
|
|
@@ -248,7 +250,7 @@ const MainLayer = GitHubService.layer.pipe(
|
|
|
248
250
|
Layer.provideMerge(ConfigServiceLayer),
|
|
249
251
|
);
|
|
250
252
|
|
|
251
|
-
const program = withAudit("gh", cli).pipe(
|
|
253
|
+
const program = withRedactedOutput(withAudit("gh", cli)).pipe(
|
|
252
254
|
Effect.provide(MainLayer),
|
|
253
255
|
Effect.tapCause(renderCauseToStderr),
|
|
254
256
|
);
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { GitHubCommandError, GitHubMergeError } from "#gh/errors";
|
|
|
21
21
|
import { GitHubService } from "#gh/service";
|
|
22
22
|
import type { GhResult } from "#gh/service";
|
|
23
23
|
import { logText } from "#shared";
|
|
24
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
24
25
|
|
|
25
26
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
26
27
|
import { pollUntilResolved } from "#shared/poll-until-resolved";
|
|
@@ -114,14 +115,18 @@ const resolveMergeSha = Effect.fn("pr.resolveMergeSha")(function* (
|
|
|
114
115
|
Effect.map((sha) => ({ sha, failed: false as const })),
|
|
115
116
|
Effect.catch((error) =>
|
|
116
117
|
Console.error(
|
|
117
|
-
|
|
118
|
+
redactSensitiveText(
|
|
119
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA could not be read: ${error.message}`,
|
|
120
|
+
),
|
|
118
121
|
).pipe(Effect.as({ sha: null, failed: true as const })),
|
|
119
122
|
),
|
|
120
123
|
);
|
|
121
124
|
|
|
122
125
|
if (result.sha === null && !result.failed) {
|
|
123
126
|
yield* Console.error(
|
|
124
|
-
|
|
127
|
+
redactSensitiveText(
|
|
128
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA is still unknown after ${MERGE_SHA_WAIT_SECONDS}s.`,
|
|
129
|
+
),
|
|
125
130
|
);
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -1088,7 +1093,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1088
1093
|
if (error._tag !== "GitHubCommandError") {
|
|
1089
1094
|
return rollbackNote === ""
|
|
1090
1095
|
? Effect.fail(error)
|
|
1091
|
-
: Console.error(rollbackNote.trim()).pipe(
|
|
1096
|
+
: Console.error(redactSensitiveText(rollbackNote.trim())).pipe(
|
|
1097
|
+
Effect.andThen(Effect.fail(error)),
|
|
1098
|
+
);
|
|
1092
1099
|
}
|
|
1093
1100
|
|
|
1094
1101
|
const stderr = error.stderr.toLowerCase();
|