typeclaw 0.3.1 → 0.4.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.
- package/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/bundled-plugins/security/index.ts +3 -2
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +286 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +256 -27
- package/src/cli/model.ts +4 -2
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +75 -0
- package/src/container/start.ts +30 -3
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +45 -5
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +59 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/index.ts +505 -9
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +6 -1
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +42 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +138 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +110 -3
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +5 -4
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +35 -4
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/typeclaw.schema.json +254 -1
package/src/init/dockerfile.ts
CHANGED
|
@@ -56,6 +56,19 @@ export const CURL_IMPERSONATE_SHA256_ARM64 = '6766bc67fd3e8e2313875f32b36b5a3fab
|
|
|
56
56
|
// the impersonation to whatever `curl_chrome` resolves to.
|
|
57
57
|
export const CURL_IMPERSONATE_PROFILE = 'chrome136'
|
|
58
58
|
|
|
59
|
+
// cloudflared powers `cloudflare-quick` tunnels. Pinned-version + per-arch
|
|
60
|
+
// SHA256 mirrors the curl-impersonate pattern above: bumping requires updating
|
|
61
|
+
// all three constants in the same commit, and the build fails loudly at
|
|
62
|
+
// `sha256sum -c` if either hash is wrong for the version. To bump: pick a
|
|
63
|
+
// release from https://github.com/cloudflare/cloudflared/releases, then
|
|
64
|
+
// curl -fsSLO .../cloudflared-linux-amd64 && shasum -a 256 cloudflared-linux-amd64
|
|
65
|
+
// for each architecture. The version literal is the release tag exactly as it
|
|
66
|
+
// appears on GitHub (no `v` prefix).
|
|
67
|
+
export const CLOUDFLARED_VERSION = '2025.5.0'
|
|
68
|
+
export const CLOUDFLARED_SHA256_AMD64 = 'a62266fd02041374f1fca0d85694aafdf7e26e171a314467356b471d4ebb2393'
|
|
69
|
+
export const CLOUDFLARED_SHA256_ARM64 = '47e55e6eba2755239f641c2c4f89878643ac0d9eaa127a6c84a2cb43fa2e0f03'
|
|
70
|
+
export const CLOUDFLARED_RELEASE_URL_BASE = 'https://github.com/cloudflare/cloudflared/releases/download'
|
|
71
|
+
|
|
59
72
|
export const TYPECLAW_ENTRYPOINT_PATH = '/usr/local/bin/typeclaw-entrypoint'
|
|
60
73
|
|
|
61
74
|
// IPv4 networks the container is forbidden to egress to when
|
|
@@ -283,9 +296,11 @@ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
|
|
|
283
296
|
// names are the trixie-renamed variants from the 64-bit time_t ABI
|
|
284
297
|
// transition; SONAMEs (libglib-2.0.so.0 etc.) are unchanged. Packages
|
|
285
298
|
// without t64 here have no t64 sibling on trixie — verified against
|
|
286
|
-
// packages.debian.org/trixie. Fonts are intentionally omitted
|
|
287
|
-
//
|
|
288
|
-
//
|
|
299
|
+
// packages.debian.org/trixie. Fonts are intentionally omitted from this
|
|
300
|
+
// list: the failure these packages address is launch-time linker errors,
|
|
301
|
+
// not rendering glyphs. CJK glyph rendering is a separate concern handled
|
|
302
|
+
// by the `cjkFonts` toggle (see CJK_FONTS_PACKAGE / APT_FEATURES below),
|
|
303
|
+
// which layers `fonts-noto-cjk` on top via the toggle apt install path.
|
|
289
304
|
export const CHROME_RUNTIME_APT_PACKAGES_AMD64 = [
|
|
290
305
|
'libasound2t64',
|
|
291
306
|
'libatk-bridge2.0-0t64',
|
|
@@ -310,17 +325,26 @@ export const CHROME_RUNTIME_APT_PACKAGES_AMD64 = [
|
|
|
310
325
|
'libxrandr2',
|
|
311
326
|
] as const
|
|
312
327
|
|
|
328
|
+
// `fonts-noto-cjk` provides CJK glyphs for Chromium-rendered output
|
|
329
|
+
// (screenshots, page.pdf()). Without it CJK text in agent-browser output
|
|
330
|
+
// renders as `.notdef` tofu boxes. Treated as a toggle apt package (like
|
|
331
|
+
// gh/tmux) rather than a base-image staple so users with `cjkFonts: false`
|
|
332
|
+
// genuinely skip the ~56MB layer; baking into the base image would force
|
|
333
|
+
// every GHCR-base user to ship the fonts regardless of their opt-out.
|
|
334
|
+
export const CJK_FONTS_PACKAGE = 'fonts-noto-cjk'
|
|
335
|
+
|
|
313
336
|
type AptFeature = {
|
|
314
337
|
toAptArgs: (toggle: DockerfileFeatureToggle) => string[]
|
|
315
338
|
}
|
|
316
339
|
|
|
317
|
-
const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python', AptFeature> = {
|
|
340
|
+
const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python' | 'cjkFonts', AptFeature> = {
|
|
318
341
|
ffmpeg: { toAptArgs: (v) => singlePackageArgs('ffmpeg', v) },
|
|
319
342
|
gh: { toAptArgs: (v) => singlePackageArgs('gh', v) },
|
|
320
343
|
tmux: { toAptArgs: (v) => singlePackageArgs('tmux', v) },
|
|
321
344
|
python: {
|
|
322
345
|
toAptArgs: (v) => (v === true ? ['python3', 'python3-pip', 'python3-venv', 'python-is-python3'] : []),
|
|
323
346
|
},
|
|
347
|
+
cjkFonts: { toAptArgs: (v) => (v === true ? [CJK_FONTS_PACKAGE] : []) },
|
|
324
348
|
}
|
|
325
349
|
|
|
326
350
|
export function buildDockerfile(
|
|
@@ -329,13 +353,14 @@ export function buildDockerfile(
|
|
|
329
353
|
): string {
|
|
330
354
|
const toggleAptArgs = collectToggleAptArgs(config)
|
|
331
355
|
const ghKeyringLayer = renderGhKeyringLayer(config.gh)
|
|
356
|
+
const cloudflaredLayer = renderCloudflaredLayer(config.cloudflared)
|
|
332
357
|
const customLines = renderCustomDockerfileLines(config.append)
|
|
333
358
|
const baseImageVersion = options.baseImageVersion ?? null
|
|
334
359
|
|
|
335
360
|
const fromAndHeavyLayers =
|
|
336
361
|
baseImageVersion !== null
|
|
337
|
-
? renderVersionedHead(baseImageVersion, ghKeyringLayer, toggleAptArgs)
|
|
338
|
-
: renderInlineHead(ghKeyringLayer, toggleAptArgs)
|
|
362
|
+
? renderVersionedHead(baseImageVersion, ghKeyringLayer, toggleAptArgs, cloudflaredLayer)
|
|
363
|
+
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer)
|
|
339
364
|
|
|
340
365
|
return `${BUILDKIT_HEADER}
|
|
341
366
|
# AUTOGENERATED by typeclaw — do not edit.
|
|
@@ -377,7 +402,12 @@ CMD ["run"]
|
|
|
377
402
|
// npm + `typeclaw start --build` immediately, instead of being blocked on a
|
|
378
403
|
// fresh base-image release. The base image's copy is harmlessly overwritten
|
|
379
404
|
// by this RUN — same path, same chmod.
|
|
380
|
-
function renderVersionedHead(
|
|
405
|
+
function renderVersionedHead(
|
|
406
|
+
baseImageVersion: string,
|
|
407
|
+
ghKeyringLayer: string,
|
|
408
|
+
toggleAptArgs: string[],
|
|
409
|
+
cloudflaredLayer: string,
|
|
410
|
+
): string {
|
|
381
411
|
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
|
|
382
412
|
return `FROM ${GHCR_BASE_IMAGE_REPO}:${baseImageVersion}
|
|
383
413
|
|
|
@@ -385,7 +415,7 @@ WORKDIR /agent
|
|
|
385
415
|
|
|
386
416
|
ARG TARGETARCH
|
|
387
417
|
|
|
388
|
-
${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
|
|
418
|
+
${ghKeyringLayer}${toggleAptLayer}${cloudflaredLayer}${renderEntrypointShimLayer()}
|
|
389
419
|
|
|
390
420
|
`
|
|
391
421
|
}
|
|
@@ -394,7 +424,7 @@ ${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
|
|
|
394
424
|
// dev-mode runs (typeclaw installed via file: / link: spec) where the
|
|
395
425
|
// matching :version GHCR tag does not yet exist, and by the test suite to
|
|
396
426
|
// keep coverage of the full-stack layers independent of GHCR availability.
|
|
397
|
-
function renderInlineHead(ghKeyringLayer: string, toggleAptArgs: string[]): string {
|
|
427
|
+
function renderInlineHead(ghKeyringLayer: string, toggleAptArgs: string[], cloudflaredLayer: string): string {
|
|
398
428
|
const baselineAndToggleArgs = [...BASELINE_APT_PACKAGES, ...toggleAptArgs]
|
|
399
429
|
return `${FROM_AND_WORKDIR}
|
|
400
430
|
|
|
@@ -436,7 +466,23 @@ ${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
|
436
466
|
|
|
437
467
|
${LAYER_5_CHROME_FOR_TESTING}
|
|
438
468
|
|
|
439
|
-
${renderEntrypointShimLayer()}
|
|
469
|
+
${cloudflaredLayer}${renderEntrypointShimLayer()}
|
|
470
|
+
|
|
471
|
+
`
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function renderCloudflaredLayer(enabled: boolean): string {
|
|
475
|
+
if (!enabled) return ''
|
|
476
|
+
return `# Layer 5.5 (optional): pinned cloudflared for cloudflare-quick tunnels.
|
|
477
|
+
RUN ARCH_BIN="$(if [ "$TARGETARCH" = "arm64" ]; then echo arm64; else echo amd64; fi)" \
|
|
478
|
+
&& ARCH_SHA="$(if [ "$TARGETARCH" = "arm64" ]; then echo ${CLOUDFLARED_SHA256_ARM64}; else echo ${CLOUDFLARED_SHA256_AMD64}; fi)" \
|
|
479
|
+
&& cd /tmp \
|
|
480
|
+
&& curl -fsSL -o cloudflared \
|
|
481
|
+
"${CLOUDFLARED_RELEASE_URL_BASE}/${CLOUDFLARED_VERSION}/cloudflared-linux-\${ARCH_BIN}" \
|
|
482
|
+
&& echo "\${ARCH_SHA} cloudflared" | sha256sum -c - \
|
|
483
|
+
&& chmod +x cloudflared \
|
|
484
|
+
&& mv cloudflared /usr/local/bin/cloudflared \
|
|
485
|
+
&& /usr/local/bin/cloudflared --version > /dev/null
|
|
440
486
|
|
|
441
487
|
`
|
|
442
488
|
}
|
|
@@ -444,7 +490,7 @@ ${renderEntrypointShimLayer()}
|
|
|
444
490
|
function renderToggleAptInstallLayer(toggleAptArgs: string[]): string {
|
|
445
491
|
return `# Layer 1 (toggle apt install): packages requested via typeclaw.json
|
|
446
492
|
# #docker.file toggles. Baseline + Chrome runtime libs are already in the
|
|
447
|
-
# base image; this layer only adds gh/tmux/python/ffmpeg if enabled.
|
|
493
|
+
# base image; this layer only adds gh/tmux/python/ffmpeg/cjkFonts if enabled.
|
|
448
494
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
449
495
|
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
450
496
|
apt-get update \\
|
|
@@ -570,12 +616,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
|
570
616
|
fi`
|
|
571
617
|
|
|
572
618
|
function defaultConfig(): DockerfileConfig {
|
|
573
|
-
return { ffmpeg: false, gh: true, python: true, tmux: true, append: [] }
|
|
619
|
+
return { ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: true, cloudflared: true, append: [] }
|
|
574
620
|
}
|
|
575
621
|
|
|
576
622
|
function collectToggleAptArgs(config: DockerfileConfig): string[] {
|
|
577
623
|
const args: string[] = []
|
|
578
|
-
for (const key of ['ffmpeg', 'gh', 'python', 'tmux'] as const) {
|
|
624
|
+
for (const key of ['ffmpeg', 'gh', 'python', 'tmux', 'cjkFonts'] as const) {
|
|
579
625
|
args.push(...APT_FEATURES[key].toAptArgs(config[key]))
|
|
580
626
|
}
|
|
581
627
|
return args
|
package/src/init/ensure-deps.ts
CHANGED
|
@@ -15,24 +15,35 @@ export type EnsureDepsOptions = {
|
|
|
15
15
|
cwd: string
|
|
16
16
|
install?: InstallRunner
|
|
17
17
|
detect?: (cwd: string) => Promise<readonly string[]>
|
|
18
|
+
// Skip the pre-install drift detection and run `bun install --force`
|
|
19
|
+
// unconditionally. Used by `typeclaw start --build` when the agent declares
|
|
20
|
+
// typeclaw via `file:` or `link:`: bun's file-dep cache otherwise treats
|
|
21
|
+
// unchanged name+version as a cache hit even after the source on disk has
|
|
22
|
+
// changed, so the post-install re-detect at the bottom (the silent-no-op
|
|
23
|
+
// guard) stays — `--force` does NOT excuse us from verifying the install
|
|
24
|
+
// actually populated the agent's node_modules/.
|
|
25
|
+
force?: boolean
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export async function ensureDepsInstalled(options: EnsureDepsOptions): Promise<EnsureDepsResult> {
|
|
21
29
|
const { cwd } = options
|
|
22
30
|
const install = options.install ?? runBunInstall
|
|
23
31
|
const detect = options.detect ?? detectMissingDeps
|
|
32
|
+
const force = options.force ?? false
|
|
24
33
|
|
|
25
|
-
const missing = await detect(cwd)
|
|
26
|
-
if (missing.length === 0) return { ok: true, installed: false }
|
|
34
|
+
const missing = force ? [] : await detect(cwd)
|
|
35
|
+
if (!force && missing.length === 0) return { ok: true, installed: false }
|
|
27
36
|
|
|
28
|
-
const result = await install(cwd)
|
|
37
|
+
const result = await install(cwd, { force })
|
|
29
38
|
if (!result.ok) return { ok: false, reason: result.reason, missing }
|
|
30
39
|
|
|
31
40
|
// Re-probe: `bun install` returns 0 even when a file:-linked dep's own
|
|
32
41
|
// package.json is unreachable (it silently no-ops on the target). Without
|
|
33
42
|
// this check, we'd proceed to `docker run` with a known-broken
|
|
34
43
|
// node_modules/ and the agent would crash with a confusing in-container
|
|
35
|
-
// `Cannot find package 'x'`.
|
|
44
|
+
// `Cannot find package 'x'`. This guard remains under `force` too —
|
|
45
|
+
// --force bypasses the cache but does not guarantee the install actually
|
|
46
|
+
// landed every declared dep.
|
|
36
47
|
const stillMissing = await detect(cwd)
|
|
37
48
|
if (stillMissing.length > 0) {
|
|
38
49
|
return {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { buildAuthStrategy } from '@/channels/adapters/github/auth'
|
|
2
|
+
import { applyManagedPath, buildManagedPath, resolveAgentId } from '@/channels/adapters/github/managed-path'
|
|
3
|
+
import { registerGithubWebhooks, type WebhookRegistrationResult } from '@/channels/adapters/github/webhook-register'
|
|
4
|
+
import { DEFAULT_GITHUB_EVENT_ALLOWLIST } from '@/channels/schema'
|
|
5
|
+
|
|
6
|
+
import type { GithubInitCredentials } from './index'
|
|
7
|
+
|
|
8
|
+
// Host-side webhook install for `typeclaw channel add github` (and the
|
|
9
|
+
// init-time GitHub branch). The container-side adapter still re-runs this on
|
|
10
|
+
// every start so a missing/rotated tunnel URL eventually catches up, but
|
|
11
|
+
// doing it eagerly here means the user sees the install succeed at CLI
|
|
12
|
+
// time — no more "I added the channel, why isn't GitHub delivering events?"
|
|
13
|
+
// when the URL is already known (external provider, or a user-set
|
|
14
|
+
// webhookUrl).
|
|
15
|
+
//
|
|
16
|
+
// Only fires when an effective webhook URL is known up front: external
|
|
17
|
+
// tunnel provider, or an explicit `webhookUrl`. Cloudflare quick tunnels
|
|
18
|
+
// don't resolve until cloudflared boots inside the container, so they
|
|
19
|
+
// stay on the existing deferred (tunnel-bridge → restartAdapter) path.
|
|
20
|
+
|
|
21
|
+
export type EagerGithubWebhookInstallOptions = {
|
|
22
|
+
webhookUrl: string
|
|
23
|
+
webhookSecret: string
|
|
24
|
+
repos: readonly string[]
|
|
25
|
+
events?: readonly string[]
|
|
26
|
+
auth: GithubInitCredentials['auth']
|
|
27
|
+
// Agent folder (or container name). Used to derive a stable webhook URL
|
|
28
|
+
// path marker so this eager-installed hook is recognizable to the
|
|
29
|
+
// container-side adapter on every subsequent start, even after the
|
|
30
|
+
// public URL's hostname rotates. Optional only because legacy direct
|
|
31
|
+
// callers (host-side init flows on stable user-set webhookUrls) may
|
|
32
|
+
// omit it; production wiring in src/init/index.ts always passes it.
|
|
33
|
+
agentDir?: string
|
|
34
|
+
fetchImpl?: typeof fetch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type EagerGithubWebhookInstallResult = WebhookRegistrationResult | { error: string; repos: [] }
|
|
38
|
+
|
|
39
|
+
export async function installGithubWebhooksEagerly(
|
|
40
|
+
options: EagerGithubWebhookInstallOptions,
|
|
41
|
+
): Promise<EagerGithubWebhookInstallResult> {
|
|
42
|
+
if (options.repos.length === 0) return { repos: [] }
|
|
43
|
+
|
|
44
|
+
let strategy: ReturnType<typeof buildAuthStrategy>
|
|
45
|
+
try {
|
|
46
|
+
strategy = buildAuthStrategy({
|
|
47
|
+
auth: authToSecretBlock(options.auth),
|
|
48
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
49
|
+
})
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { error: describe(err), repos: [] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const managedPath =
|
|
55
|
+
options.agentDir !== undefined ? buildManagedPath(resolveAgentId({ agentDir: options.agentDir })) : undefined
|
|
56
|
+
const webhookUrl = managedPath !== undefined ? applyManagedPath(options.webhookUrl, managedPath) : options.webhookUrl
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await registerGithubWebhooks({
|
|
60
|
+
token: () => strategy.token(),
|
|
61
|
+
webhookUrl,
|
|
62
|
+
webhookSecret: options.webhookSecret,
|
|
63
|
+
repos: options.repos,
|
|
64
|
+
events: options.events ?? DEFAULT_GITHUB_EVENT_ALLOWLIST,
|
|
65
|
+
...(managedPath !== undefined ? { managedPath } : {}),
|
|
66
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
67
|
+
})
|
|
68
|
+
return result
|
|
69
|
+
} finally {
|
|
70
|
+
// PatAuthStrategy.dispose() is a no-op and AppAuthStrategy clears its
|
|
71
|
+
// cached installation token. Either way, releasing it here keeps the
|
|
72
|
+
// host CLI from holding onto credentials longer than needed.
|
|
73
|
+
await strategy.dispose().catch(() => {})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Bridge the wizard/CLI's plaintext credentials union into the Secret-wrapped
|
|
78
|
+
// shape buildAuthStrategy expects. Plain strings are wrapped as `{ value }`
|
|
79
|
+
// so the underlying PatAuthStrategy resolver doesn't try (and fail) to read
|
|
80
|
+
// from process.env.
|
|
81
|
+
function authToSecretBlock(auth: GithubInitCredentials['auth']) {
|
|
82
|
+
if (auth.type === 'pat') {
|
|
83
|
+
return { type: 'pat' as const, token: { value: auth.pat } }
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
type: 'app' as const,
|
|
87
|
+
appId: auth.appId,
|
|
88
|
+
privateKey: { value: auth.privateKey },
|
|
89
|
+
...(auth.installationId !== undefined ? { installationId: auth.installationId } : {}),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function describe(err: unknown): string {
|
|
94
|
+
return err instanceof Error ? err.message : String(err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatEagerGithubWebhookInstallResult(result: EagerGithubWebhookInstallResult): string {
|
|
98
|
+
if ('error' in result) return `GitHub webhook install failed: ${result.error}`
|
|
99
|
+
const created = result.repos.filter((r) => r.action === 'created').length
|
|
100
|
+
const updated = result.repos.filter((r) => r.action === 'updated').length
|
|
101
|
+
const failed = result.repos.filter((r) => r.action === 'failed')
|
|
102
|
+
const parts: string[] = []
|
|
103
|
+
if (created > 0) parts.push(`${created} created`)
|
|
104
|
+
if (updated > 0) parts.push(`${updated} updated`)
|
|
105
|
+
if (failed.length > 0) parts.push(`${failed.length} failed`)
|
|
106
|
+
const summary = parts.length > 0 ? parts.join(', ') : 'no repos'
|
|
107
|
+
const tail = failed.length > 0 ? ` (${failed.map((f) => `${f.repo}: ${f.error}`).join('; ')})` : ''
|
|
108
|
+
return `GitHub webhooks: ${summary}.${tail}`
|
|
109
|
+
}
|