diff --git a/src/commands/linear.ts b/src/commands/linear.ts index 7280947a..f7d94e04 100644 --- a/src/commands/linear.ts +++ b/src/commands/linear.ts @@ -183,6 +183,16 @@ async function renderHelp(input: { }); return; } + await display.panel({ + title: "Linear routing model", + tone: "info", + lines: [ + "Profiles are global Linear auth identities.", + "Repos can bind a default Linear project route plus optional linked projects.", + "When you omit --profile, Hack prefers the repo binding first and then the global default profile.", + "Use `hack linear status` to see the active profile, default project, team, linked projects, and recovery steps.", + ], + }); await display.table({ columns: ["Command", "Summary"], rows: input.commands.map((cmd) => [`hack linear ${cmd.name}`, cmd.summary]), diff --git a/src/control-plane/extensions/linear/auth.ts b/src/control-plane/extensions/linear/auth.ts index b3c34cc1..ebc45d4f 100644 --- a/src/control-plane/extensions/linear/auth.ts +++ b/src/control-plane/extensions/linear/auth.ts @@ -262,12 +262,24 @@ export function resolveLinearAuthSettingsResult(input: { availableProfileIds.length > 0 ? availableProfileIds.join(", ") : "(none configured)"; + const fixLabel = (() => { + if (selected.selectedProfileSource === "command_flags") { + return `Use one of the available profiles with \`--profile \`, or run \`hack linear connect --profile ${selected.selectedProfileId}\` to create it.`; + } + if (selected.selectedProfileSource === "project_routing") { + return `Update the repo route with \`hack linear setup --profile \` or connect the missing profile with \`hack linear connect --profile ${selected.selectedProfileId}\`.`; + } + if (selected.selectedProfileSource === "global_default") { + return `Switch the global default with \`hack linear use --profile \` or connect the missing profile with \`hack linear connect --profile ${selected.selectedProfileId}\`.`; + } + return `Connect a profile with \`hack linear connect --profile ${selected.selectedProfileId}\` or choose one with \`hack linear use --profile \`.`; + })(); return { ok: false, selectedProfileId: selected.selectedProfileId, selectedProfileSource: selected.selectedProfileSource, availableProfileIds, - error: `Linear profile "${selected.selectedProfileId}" (${sourceLabel}) was not found. Available profiles: ${availableLabel}.`, + error: `Linear profile "${selected.selectedProfileId}" (${sourceLabel}) was not found. Available profiles: ${availableLabel}. ${fixLabel}`, }; } diff --git a/src/control-plane/extensions/linear/commands.ts b/src/control-plane/extensions/linear/commands.ts index e8066ef6..a1d9bbdf 100644 --- a/src/control-plane/extensions/linear/commands.ts +++ b/src/control-plane/extensions/linear/commands.ts @@ -2187,9 +2187,16 @@ async function renderLinearStatusPayload(input: { readonly payload: LinearStatusCommandPayload; readonly logger: ExtensionCommandContext["logger"]; }): Promise { + const bindingSummary = describeLinearBindingState({ + binding: input.payload.projectBinding, + }); const lines = [ `Active profile: ${input.payload.summary.activeProfile}`, `Connection: ${input.payload.summary.connectionLabel}`, + `Repo binding profile: ${bindingSummary.profile}`, + `Default project: ${bindingSummary.project}`, + `Default team: ${bindingSummary.team}`, + `Linked projects: ${bindingSummary.linkedProjects}`, `Route: ${input.payload.summary.routingSummary}`, ...(input.payload.summary.linkedProjectsLabel ? [input.payload.summary.linkedProjectsLabel] @@ -5230,8 +5237,7 @@ async function resolveLinearTargetForTicketSync(input: { if (!teamId) { return { ok: false, - error: - "Cannot resolve Linear team for ticket sync. Pass --team-id or bind a project with `hack x linear project-bind`.", + error: `Cannot resolve Linear team for ticket sync. Active route project: ${projectId ?? "(none)"}. Pass --team-id, or bind a default project/team with \`hack linear project-bind --project-id --team-id \`.`, }; } @@ -7010,6 +7016,33 @@ function buildLinearRoutingSummary(input: { })}.`; } +function describeLinearBindingState(input: { + readonly binding: + | ResolvedLinearProjectBinding + | LinearStatusCommandPayload["projectBinding"]; +}): { + readonly profile: string; + readonly project: string; + readonly team: string; + readonly linkedProjects: string; +} { + const defaultProject = input.binding.projectId + ? formatLinearProjectTarget({ + projectId: input.binding.projectId, + ...(input.binding.projectName + ? { projectName: input.binding.projectName } + : {}), + ...(input.binding.teamId ? { teamId: input.binding.teamId } : {}), + }) + : "(none)"; + return { + profile: readOptionalString(input.binding.profileId) ?? "(none)", + project: defaultProject, + team: readOptionalString(input.binding.teamId) ?? "(none)", + linkedProjects: String(input.binding.additionalProjects.length), + }; +} + function buildLinkedProjectsLabel(input: { readonly binding: ResolvedLinearProjectBinding; }): string | null { @@ -7572,7 +7605,7 @@ function describeAmbiguousProjectArtifactTarget(input: { input.projectName ? `project name "${input.projectName}"` : null, input.teamId ? `team id "${input.teamId}"` : null, ].filter((part): part is string => Boolean(part)); - return `Multiple ${input.source} Linear projects match ${parts.join(" and ")}. Pass --project-id to disambiguate.`; + return `Multiple ${input.source} Linear projects match ${parts.join(" and ")}. Pass --project-id to disambiguate, then confirm the repo route with \`hack linear status\`.`; } function describeMissingProjectArtifactTarget(input: { @@ -7583,7 +7616,7 @@ function describeMissingProjectArtifactTarget(input: { input.projectName ? `project name "${input.projectName}"` : null, input.teamId ? `team id "${input.teamId}"` : null, ].filter((part): part is string => Boolean(part)); - return `No Linear project matches ${parts.join(" and ")}. Pass --project-id or update the project binding.`; + return `No Linear project matches ${parts.join(" and ")}. Pass --project-id, run \`hack linear projects\` to inspect available projects, or update the repo binding with \`hack linear project-bind --project-id \`.`; } async function resolveProjectBindingDetails(input: {