fix(hub): make guided setup choices actionable (#3677)

Show a concise login success state, scope starter choices to available Hub connections, wait for daemon capabilities, and avoid partial starter bundles when setup cannot proceed.
This commit is contained in:
Mohamed Boudra
2026-08-22 10:37:19 +02:00
committed by GitHub
parent c763b80824
commit 7beff9cb0b
8 changed files with 358 additions and 111 deletions
+10 -2
View File
@@ -87,6 +87,7 @@ describe("Hub commands", () => {
assert.deepEqual(events, [
"progress:Logging in to https://hub.paseo.sh",
"authorize:https://hub.paseo.sh",
"progress:Logged in",
]);
assert.equal(result.data.origin, "https://hub.paseo.sh");
});
@@ -113,11 +114,18 @@ describe("Hub commands", () => {
events.push("init");
events.push("deploy");
},
reporter: quietReporter,
reporter: { progress: (message) => events.push(`progress:${message}`) },
},
);
assert.deepEqual(events, ["login", "connect:https://hub.test", "init", "deploy"]);
assert.deepEqual(events, [
"progress:Logging in to https://hub.test",
"login",
"progress:Logged in",
"connect:https://hub.test",
"init",
"deploy",
]);
});
it("JSON and noninteractive login remain login-only", async () => {
+102 -21
View File
@@ -3,6 +3,7 @@ import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, it } from "vitest";
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
import type { HubCredentialStore, StoredHubCredential } from "./credentials.js";
import type { HubDaemonClient, HubStatus } from "./daemon-client.js";
import type { HubHttpClient } from "./hub-client/index.js";
@@ -24,11 +25,7 @@ describe("Hub guided setup continuation", () => {
const cwd = await temporaryDirectory();
const credentials = new MemoryCredentials();
const daemon = new SetupDaemon();
const prompts = new PromptAnswers(
[true, true],
["codex", "gpt-5", "full-access", "slack"],
["U123"],
);
const prompts = new PromptAnswers([true, true], ["codex", "gpt-5", "full-access"], ["U123"]);
const calls: Array<{ operation: string; origin: string; files?: readonly string[] }> = [];
const environment = setupEnvironment(cwd, credentials, daemon, prompts, calls);
@@ -46,24 +43,22 @@ describe("Hub guided setup continuation", () => {
);
assert.deepEqual(prompts.confirmations, [
"Connect this daemon to https://hub.test?",
"Connect this daemon to this Hub?",
"Initialize and deploy a starter workflow?",
]);
assert.deepEqual(prompts.selections, [
"Starter agent provider",
"Starter agent model",
"Starter agent mode",
"Trigger provider",
]);
assert.deepEqual(prompts.selectionOptions, [
["Codex (suggested)"],
["Codex"],
["GPT-5 (suggested)", "GPT-5 mini"],
["Read only", "Full access (suggested)"],
[
"GitHub (issue or pull request comment)",
"Slack (channel mention)",
"Discord (channel mention)",
],
]);
assert.deepEqual(prompts.messages, [
"Hub app connections ready for this workflow:\nSlack — Paseo\n\nOnly configured connections are shown. To add another, open Hub → Apps, then run `paseo hub init` again.",
"Using Slack — Paseo",
]);
assert.deepEqual(calls, [
{ operation: "token", origin: "https://hub.test" },
@@ -111,6 +106,36 @@ describe("Hub guided setup continuation", () => {
assert.deepEqual(initDeclined.messages, ["Skipped starter workflow. Run: paseo hub init"]);
});
it("keeps login successful when no Hub app connection can initialize a workflow", async () => {
const cwd = await temporaryDirectory();
const credentials = new MemoryCredentials();
const daemon = new SetupDaemon();
const prompts = new PromptAnswers([true, true], [], []);
await runHubLogin(
"https://hub.test",
{},
{
env: {},
credentials,
flow: { authorize: async () => "paseo_cli_prefix_durable-secret" },
isInteractive: () => true,
continueGuidedSetup: (origin) =>
continueHubGuidedSetup(
origin,
setupEnvironment(cwd, credentials, daemon, prompts, [], {
setupResources: { github: [], slack: [], discord: [] },
}),
),
reporter: { progress() {} },
},
);
assert.deepEqual(prompts.messages, [
"No Hub app connection is ready for this workflow.\nConnect GitHub, Slack, or Discord in Hub → Apps, then run `paseo hub init` again.",
]);
});
it("keeps hub init's existing replacement confirmation", async () => {
const cwd = await temporaryDirectory();
await mkdir(path.join(cwd, ".paseo"));
@@ -140,7 +165,7 @@ describe("Hub guided setup continuation", () => {
},
]);
const prompts = new PromptAnswers([true], ["claude", "sonnet", "auto", "slack"], ["U123"]);
const prompts = new PromptAnswers([true], ["claude", "sonnet", "auto"], ["U123"]);
const calls: Array<{ operation: string; origin: string; files?: readonly string[] }> = [];
await runHubGuidedSetup(setupEnvironment(cwd, credentials, daemon, prompts, calls), {
@@ -162,6 +187,52 @@ describe("Hub guided setup continuation", () => {
["validate", "install"],
);
});
it("stops before runtime discovery and file writes when no Hub app connection is usable", async () => {
const cwd = await temporaryDirectory();
const credentials = new MemoryCredentials();
credentials.save({ origin: "https://hub.test", credential: "secret" });
const daemon = new SetupDaemon();
const prompts = new PromptAnswers([], [], []);
await assert.rejects(
runHubGuidedSetup(
setupEnvironment(cwd, credentials, daemon, prompts, [], {
setupResources: { github: [], slack: [], discord: [] },
}),
{ origin: "https://hub.test", daemonId: "daemon-1", deploy: true },
),
/No Hub app connection is ready for this workflow/u,
);
assert.equal(daemon.snapshotCwds.length, 0);
await assert.rejects(readFile(path.join(cwd, ".paseo", "hub.yml")), { code: "ENOENT" });
});
it("waits for fresh-daemon provider discovery before offering runtime choices", async () => {
const cwd = await temporaryDirectory();
const credentials = new MemoryCredentials();
credentials.save({ origin: "https://hub.test", credential: "secret" });
const ready = new SetupDaemon().providerEntries;
const daemon = new SetupDaemon(ready, [
[{ provider: "codex", status: "loading", enabled: true }],
ready,
]);
const prompts = new PromptAnswers([], ["codex", "gpt-5", "full-access"], ["U123"]);
await runHubGuidedSetup(setupEnvironment(cwd, credentials, daemon, prompts, []), {
origin: "https://hub.test",
daemonId: "daemon-1",
deploy: true,
});
assert.equal(daemon.snapshotCwds.length, 2);
assert.deepEqual(prompts.selections.slice(0, 3), [
"Starter agent provider",
"Starter agent model",
"Starter agent mode",
]);
});
});
function setupEnvironment(
@@ -170,6 +241,9 @@ function setupEnvironment(
daemon: SetupDaemon,
prompts: PromptAnswers,
calls: Array<{ operation: string; origin: string; files?: readonly string[] }>,
options: {
setupResources?: Awaited<ReturnType<HubHttpClient["listSetupResources"]>>;
} = {},
): HubGuidedSetupEnvironment {
const hub = {
async issueEnrollmentToken(origin: string) {
@@ -182,11 +256,13 @@ function setupEnvironment(
},
async listSetupResources(origin: string) {
calls.push({ operation: "setup", origin });
return {
github: [],
discord: [],
slack: [{ teamId: "T123", teamName: "Paseo" }],
};
return (
options.setupResources ?? {
github: [],
discord: [],
slack: [{ teamId: "T123", teamName: "Paseo" }],
}
);
},
async listConfigurationResources(origin: string) {
calls.push({ operation: "resources", origin });
@@ -294,7 +370,7 @@ class SetupDaemon implements HubDaemonClient {
private origin: string | null = null;
constructor(
private readonly providerEntries = [
readonly providerEntries: readonly ProviderSnapshotEntry[] = [
{
provider: "codex",
status: "ready" as const,
@@ -311,6 +387,7 @@ class SetupDaemon implements HubDaemonClient {
defaultModeId: "full-access",
},
],
private readonly providerEntrySequence?: readonly (readonly ProviderSnapshotEntry[])[],
) {}
async connectHub(origin: string) {
@@ -325,7 +402,11 @@ class SetupDaemon implements HubDaemonClient {
async getProvidersSnapshot(options?: { cwd?: string }) {
this.snapshotCwds.push(options?.cwd);
return { entries: this.providerEntries };
const index = Math.min(
this.snapshotCwds.length - 1,
(this.providerEntrySequence?.length ?? 1) - 1,
);
return { entries: this.providerEntrySequence?.[index] ?? this.providerEntries };
}
async disconnectHub() {
+112 -81
View File
@@ -34,7 +34,6 @@ import {
planHubInitOpening,
resolveHubInitConnection,
resolveHubInitProjects,
type HubInitProvider,
} from "./init-plan.js";
import type { CliLoginFlow } from "./login-flow.js";
import { runHubConnect } from "./connect.js";
@@ -43,16 +42,21 @@ import { runHubProjects } from "./projects.js";
import type { HubReporter } from "./reporter.js";
import { normalizeHubOrigin } from "./origin.js";
import {
availableStarterAgentProviders,
selectedStarterAgentRuntime,
starterAgentProviderSnapshotState,
suggestedStarterAgentChoice,
type HubStarterAgentProvider,
type HubStarterAgentRuntime,
} from "./starter-agent-runtime.js";
import {
availableStarterTriggerConnections,
type HubStarterTriggerConnection,
} from "./starter-trigger.js";
const execFileAsync = promisify(execFile);
const DAEMON_READY_TIMEOUT_MS = 60_000;
const DAEMON_READY_POLL_MS = 250;
const PROVIDER_READY_TIMEOUT_MS = 60_000;
export interface HubGuidedSetupEnvironment {
env: Readonly<Record<string, string | undefined>>;
@@ -132,16 +136,18 @@ export async function runHubGuidedSetup(
const daemonId = state.daemonId ?? (await ensureDaemonConnection(origin, environment));
const project = await chooseProject(origin, environment);
const resources = await loadSetupResources(origin, environment);
const triggerConnections = await resolveStarterTriggerConnections(resources, cwd);
reportStarterTriggerConnections(environment, triggerConnections);
const trigger = await chooseStarterTriggerConnection(environment, triggerConnections);
const daemon = await loadDaemonResource(origin, daemonId, environment);
log.success(`Connected as ${daemon.slug}`);
const agent = await chooseStarterAgentRuntime(environment, cwd);
const provider = await chooseProvider(environment);
const providerFilters = await collectProviderFilters(provider, resources, cwd, environment);
const providerFilters = await collectProviderIdentity(trigger, environment);
const scaffold = createHubInitScaffold({
cwd,
daemonSlug: daemon.slug,
agent,
provider,
provider: trigger.provider,
providerFilters,
});
@@ -185,7 +191,7 @@ export async function continueHubGuidedSetup(
origin: string,
environment: HubGuidedSetupEnvironment,
): Promise<void> {
if (!(await requiredConfirm(environment, `Connect this daemon to ${origin}?`, true))) {
if (!(await requiredConfirm(environment, "Connect this daemon to this Hub?", true))) {
reportMessage(
environment,
`Skipped daemon connection. Run: ${hubLoginResumeCommand("connect", origin)}; then paseo hub init`,
@@ -200,7 +206,20 @@ export async function continueHubGuidedSetup(
);
return;
}
await runHubGuidedSetup(environment, { origin, daemonId, deploy: true });
try {
await runHubGuidedSetup(environment, { origin, daemonId, deploy: true });
} catch (error) {
if (error instanceof HubInitCancelledError) {
if (environment.prompts === undefined) {
log.message(error.message);
outro("Connect an app to continue");
} else {
environment.prompts.message(error.message);
}
return;
}
throw error;
}
}
async function ensureLogin(
@@ -370,31 +389,61 @@ async function chooseProject(
return project;
}
async function chooseProvider(environment: HubGuidedSetupEnvironment): Promise<HubInitProvider> {
return requiredSelect(environment, {
message: "Trigger provider",
options: [
{ value: "github", label: "GitHub", hint: "issue or pull request comment" },
{ value: "slack", label: "Slack", hint: "channel mention" },
{ value: "discord", label: "Discord", hint: "channel mention" },
],
async function resolveStarterTriggerConnections(
resources: HubSetupResources,
cwd: string,
): Promise<HubStarterTriggerConnection[]> {
const remote = await readCommandValue("git", ["remote", "get-url", "origin"], cwd);
const repository = remote === undefined ? undefined : githubRepositoryFromRemote(remote);
const connections = availableStarterTriggerConnections(resources, repository);
if (connections.length === 0) {
throw new HubInitCancelledError(
"No Hub app connection is ready for this workflow.\nConnect GitHub, Slack, or Discord in Hub → Apps, then run `paseo hub init` again.",
);
}
return connections;
}
function reportStarterTriggerConnections(
environment: HubGuidedSetupEnvironment,
connections: readonly HubStarterTriggerConnection[],
): void {
const details = `${connections.map(({ label }) => label).join("\n")}\n\nOnly configured connections are shown. To add another, open Hub → Apps, then run \`paseo hub init\` again.`;
if (environment.prompts === undefined) {
note(details, "Hub app connections ready for this workflow");
return;
}
environment.prompts.message(`Hub app connections ready for this workflow:\n${details}`);
}
async function chooseStarterTriggerConnection(
environment: HubGuidedSetupEnvironment,
connections: readonly HubStarterTriggerConnection[],
): Promise<HubStarterTriggerConnection> {
if (connections.length === 1) {
const connection = connections[0]!;
reportMessage(environment, `Using ${connection.label}`);
return connection;
}
const id = await requiredSelect(environment, {
message: "Trigger connection",
options: connections.map((connection) => ({ value: connection.id, label: connection.label })),
});
const connection = connections.find((candidate) => candidate.id === id);
if (connection === undefined) {
throw new HubCommandError(
"HUB_PROVIDER_CONNECTION_INVALID",
"The selected Hub app connection is no longer available. Run paseo hub init again.",
);
}
return connection;
}
async function chooseStarterAgentRuntime(
environment: HubGuidedSetupEnvironment,
cwd: string,
): Promise<HubStarterAgentRuntime> {
const snapshot = await withHubDaemon(environment.daemon, undefined, (daemon) =>
daemon.getProvidersSnapshot({ cwd }),
);
const providers = availableStarterAgentProviders(snapshot.entries);
if (providers.length === 0) {
throw new HubCommandError(
"HUB_AGENT_RUNTIME_REQUIRED",
"No usable agent runtime is available from this daemon. Configure an enabled provider with a selectable model, then run paseo hub init again.",
);
}
const providers = await waitForStarterAgentProviders(environment, cwd);
const provider = await chooseStarterAgentProvider(environment, providers);
const model = await chooseStarterAgentModel(environment, provider);
const mode = await chooseStarterAgentMode(environment, provider);
@@ -408,18 +457,45 @@ async function chooseStarterAgentRuntime(
return runtime;
}
async function waitForStarterAgentProviders(
environment: HubGuidedSetupEnvironment,
cwd: string,
): Promise<readonly HubStarterAgentProvider[]> {
return withSpinner("Discovering agent runtimes", async (reporter) =>
withHubDaemon(environment.daemon, undefined, async (daemon) => {
const deadline = Date.now() + PROVIDER_READY_TIMEOUT_MS;
while (true) {
const snapshot = await daemon.getProvidersSnapshot({ cwd });
const state = starterAgentProviderSnapshotState(snapshot.entries);
if (state.kind === "ready") return state.providers;
if (state.kind === "unavailable") {
throw new HubCommandError(
"HUB_AGENT_RUNTIME_REQUIRED",
"No usable agent runtime is available from this daemon. Configure an enabled provider with a selectable model, then run paseo hub init again.",
);
}
if (Date.now() >= deadline) {
throw new HubCommandError(
"HUB_AGENT_RUNTIME_TIMEOUT",
"Agent runtime discovery did not finish within 60 seconds. Check the daemon's provider configuration, then run paseo hub init again.",
);
}
reporter.progress("Waiting for agent runtime discovery");
await delay(DAEMON_READY_POLL_MS);
}
}),
);
}
async function chooseStarterAgentProvider(
environment: HubGuidedSetupEnvironment,
providers: readonly HubStarterAgentProvider[],
): Promise<HubStarterAgentProvider> {
const suggested = suggestedStarterAgentChoice(providers);
const selected = await requiredSelect(environment, {
message: "Starter agent provider",
...(suggested === undefined ? {} : { initialValue: suggested.id }),
options: providers.map((provider) => ({
value: provider.id,
label: provider.label,
...(provider.suggested ? { hint: "suggested" } : {}),
})),
});
const provider = providers.find((candidate) => candidate.id === selected);
@@ -471,57 +547,30 @@ function invalidStarterAgentSelection(): HubCommandError {
);
}
async function collectProviderFilters(
provider: HubInitProvider,
resources: HubSetupResources,
cwd: string,
async function collectProviderIdentity(
trigger: HubStarterTriggerConnection,
environment: HubGuidedSetupEnvironment,
): Promise<Readonly<Record<string, string>>> {
if (provider === "github") {
const [login, originRemote] = await Promise.all([
readGhValue(["api", "user", "--jq", ".login"]),
readCommandValue("git", ["remote", "get-url", "origin"], cwd),
]);
const repo = originRemote === undefined ? undefined : githubRepositoryFromRemote(originRemote);
if (repo === undefined) {
throw new HubCommandError(
"HUB_GITHUB_REPOSITORY_UNDETECTED",
"Could not detect a GitHub repository from the origin remote.",
);
}
if (!resources.github.some(({ repositories }) => repositories.includes(repo))) {
const connected = resources.github.flatMap(({ repositories }) => repositories);
throw new HubCommandError(
"HUB_GITHUB_REPOSITORY_NOT_CONNECTED",
`${repo} is not connected to this Hub organization. Connected repositories: ${connected.join(", ") || "none"}.`,
);
}
if (trigger.provider === "github") {
const login = await readGhValue(["api", "user", "--jq", ".login"]);
return {
...trigger.filters,
user: await requiredText(environment, {
message: "Your GitHub username (only this user can trigger the bot)",
initialValue: login,
}),
repo,
};
}
if (provider === "slack") {
if (trigger.provider === "slack") {
return {
workspace: await chooseConnection(
environment,
"Slack workspace",
resources.slack.map(({ teamId, teamName }) => ({ id: teamId, label: teamName })),
),
...trigger.filters,
user: await requiredText(environment, {
message: "Your Slack member ID (only this user can trigger the bot)",
}),
};
}
return {
guild: await chooseConnection(
environment,
"Discord server",
resources.discord.map(({ guildId, guildName }) => ({ id: guildId, label: guildName })),
),
...trigger.filters,
user: await requiredText(environment, {
message: "Your Discord user ID (only this user can trigger the bot)",
}),
@@ -579,24 +628,6 @@ async function loadDaemonResource(
return daemon;
}
async function chooseConnection(
environment: HubGuidedSetupEnvironment,
message: string,
connections: readonly { id: string; label: string }[],
): Promise<string> {
if (connections.length === 0) {
throw new HubCommandError(
"HUB_PROVIDER_CONNECTION_REQUIRED",
`No ${message.toLowerCase()} is connected in this Hub organization. Connect one and try again.`,
);
}
if (connections.length === 1) return connections[0]!.id;
return requiredSelect(environment, {
message,
options: connections.map(({ id, label }) => ({ value: id, label })),
});
}
async function writeScaffold(
cwd: string,
scaffold: ReturnType<typeof createHubInitScaffold>,
+1
View File
@@ -42,6 +42,7 @@ export async function runHubLogin(
reportHubProgress(dependencies.reporter, options, `Logging in to ${origin}`);
const credential = await dependencies.flow.authorize(origin);
dependencies.credentials.save({ origin, credential });
reportHubProgress(dependencies.reporter, options, "Logged in");
if (
!options.json &&
dependencies.isInteractive?.() &&
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
availableStarterAgentProviders,
selectedStarterAgentRuntime,
starterAgentProviderSnapshotState,
suggestedStarterAgentChoice,
} from "./starter-agent-runtime.js";
@@ -30,7 +31,6 @@ describe("starter agent runtime choices", () => {
{
id: "codex",
label: "Codex",
suggested: true,
models: [
{ id: "gpt-5", label: "GPT-5", suggested: true },
{ id: "gpt-5-mini", label: "GPT-5 mini", suggested: false },
@@ -59,7 +59,6 @@ describe("starter agent runtime choices", () => {
{
id: "claude",
label: "Claude",
suggested: false,
models: [{ id: "sonnet", label: "Sonnet", suggested: true }],
modes: [{ id: "auto", label: "Auto", suggested: false }],
},
@@ -81,7 +80,6 @@ describe("starter agent runtime choices", () => {
{
id: "claude",
label: "claude",
suggested: false,
models: [{ id: "sonnet", label: "Sonnet", suggested: true }],
modes: [{ id: "auto", label: "Auto", suggested: false }],
},
@@ -118,4 +116,16 @@ describe("starter agent runtime choices", () => {
});
expect(suggestedStarterAgentChoice(provider!.modes)).toBeUndefined();
});
it("distinguishes discovery in progress from a terminally unusable snapshot", () => {
expect(starterAgentProviderSnapshotState([])).toEqual({ kind: "loading" });
expect(
starterAgentProviderSnapshotState([{ provider: "claude", status: "loading", enabled: true }]),
).toEqual({ kind: "loading" });
expect(
starterAgentProviderSnapshotState([
{ provider: "claude", status: "unavailable", enabled: true },
]),
).toEqual({ kind: "unavailable" });
});
});
@@ -11,7 +11,6 @@ export interface HubStarterAgentProvider {
label: string;
models: readonly HubStarterAgentModel[];
modes: readonly HubStarterAgentMode[];
suggested: boolean;
}
export interface HubStarterAgentModel {
@@ -26,6 +25,11 @@ export interface HubStarterAgentMode {
suggested: boolean;
}
export type HubStarterAgentProviderSnapshotState =
| { kind: "ready"; providers: readonly HubStarterAgentProvider[] }
| { kind: "loading" }
| { kind: "unavailable" };
export function availableStarterAgentProviders(
entries: readonly ProviderSnapshotEntry[],
): HubStarterAgentProvider[] {
@@ -47,14 +51,23 @@ export function availableStarterAgentProviders(
label: entry.label ?? entry.provider,
models,
modes,
suggested:
models.some((model) => model.suggested) &&
(modes.length === 0 || modes.some((mode) => mode.suggested)),
},
];
});
}
export function starterAgentProviderSnapshotState(
entries: readonly ProviderSnapshotEntry[],
): HubStarterAgentProviderSnapshotState {
const providers = availableStarterAgentProviders(entries);
if (providers.length > 0) return { kind: "ready", providers };
if (entries.length === 0) return { kind: "loading" };
if (entries.some((entry) => entry.enabled && entry.status === "loading")) {
return { kind: "loading" };
}
return { kind: "unavailable" };
}
export function suggestedStarterAgentChoice<T extends { suggested: boolean }>(
choices: readonly T[],
): T | undefined {
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { availableStarterTriggerConnections } from "./starter-trigger.js";
describe("starter trigger connections", () => {
it("returns only concrete connections that can back the generated workflow", () => {
expect(
availableStarterTriggerConnections(
{
github: [
{
slug: "github-getpaseo",
accountLogin: "getpaseo",
accountType: "Organization",
repositories: ["getpaseo/paseo"],
},
],
slack: [{ teamId: "T123", teamName: "Paseo" }],
discord: [{ guildId: "456", guildName: "Paseo Discord" }],
},
"getpaseo/paseo",
),
).toEqual([
{
id: "github:getpaseo/paseo",
label: "GitHub — getpaseo/paseo",
provider: "github",
filters: { repo: "getpaseo/paseo" },
},
{
id: "slack:T123",
label: "Slack — Paseo",
provider: "slack",
filters: { workspace: "T123" },
},
{
id: "discord:456",
label: "Discord — Paseo Discord",
provider: "discord",
filters: { guild: "456" },
},
]);
});
it("does not offer GitHub when the current repository is not connected", () => {
expect(
availableStarterTriggerConnections(
{
github: [
{
slug: "github-getpaseo",
accountLogin: "getpaseo",
accountType: "Organization",
repositories: ["getpaseo/hub"],
},
],
slack: [],
discord: [],
},
"getpaseo/paseo",
),
).toEqual([]);
});
});
@@ -0,0 +1,40 @@
import type { HubSetupResources } from "./hub-client/index.js";
import type { HubInitProvider } from "./init-plan.js";
export interface HubStarterTriggerConnection {
id: string;
label: string;
provider: HubInitProvider;
filters: Readonly<Record<string, string>>;
}
export function availableStarterTriggerConnections(
resources: HubSetupResources,
githubRepository?: string,
): HubStarterTriggerConnection[] {
return [
...(githubRepository !== undefined &&
resources.github.some(({ repositories }) => repositories.includes(githubRepository))
? [
{
id: `github:${githubRepository}`,
label: `GitHub — ${githubRepository}`,
provider: "github" as const,
filters: { repo: githubRepository },
},
]
: []),
...resources.slack.map(({ teamId, teamName }) => ({
id: `slack:${teamId}`,
label: `Slack — ${teamName}`,
provider: "slack" as const,
filters: { workspace: teamId },
})),
...resources.discord.map(({ guildId, guildName }) => ({
id: `discord:${guildId}`,
label: `Discord — ${guildName}`,
provider: "discord" as const,
filters: { guild: guildId },
})),
];
}