Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/commands/integration/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
BACK,
cancel,
note,
outro,
promptConfirmOrBack,
promptSelectOrBack,
promptPasswordOrBack,
} from '../../utils/prompt';
Expand Down Expand Up @@ -77,6 +79,16 @@ export function typeOptionsForCategory(category: string | undefined): typeof TYP
return TYPE_OPTIONS.filter((o) => o.category === category);
}

// The code-agent picker opens with an opt-in question because connecting one
// changes where autofixes run; --type means the user already decided.
export function shouldOfferCodeAgent(
category: string | undefined,
typeFromFlag: boolean,
interactive: boolean
): boolean {
return category === 'code-agent' && !typeFromFlag && interactive;
}

// The category is validated even when --type wins, so a typo always errors
// instead of being silently ignored.
export function resolveTypeOptions(category: string | undefined, typeFromFlag: boolean): typeof TYPE_OPTIONS {
Expand Down Expand Up @@ -602,6 +614,7 @@ export const integrationConnectCommand: Command = {
'polylane integration connect --type github',
'polylane integration connect --type github --reconnect',
'polylane integration connect --category observability',
'polylane integration connect --category code-agent',
'polylane integration connect --type datadog --site us5.datadoghq.com --api-key ... --app-key ...',
'polylane integration connect --type honeycomb --region us --api-key ...',
'polylane integration connect --type axiom --region us-east-1 --api-token ...',
Expand All @@ -611,12 +624,30 @@ export const integrationConnectCommand: Command = {
'polylane integration connect --type mcp --url https://mcp.example.com/sse --name "My MCP" --oauth',
],
async execute(config: Config, _flags, args: Record<string, unknown>): Promise<void> {
const workspaceId = await requireWorkspace(config);
const noBrowser = getArgBoolean(args, 'noBrowser') === true;
const api = new PolylaneAPI(config);
const typeFromFlag = getArgString(args, 'type') !== undefined;
const category = getArgString(args, 'category');
// --type always wins: the category filter only narrows the picker.
const typeOptions = resolveTypeOptions(getArgString(args, 'category'), typeFromFlag);
const typeOptions = resolveTypeOptions(category, typeFromFlag);

if (shouldOfferCodeAgent(category, typeFromFlag, isInteractive(config.nonInteractive))) {
note(
'Optional. Polylane runs autofixes on its own executor; connect your coding agent to run them in your account instead.',
'Cloud coding agent'
);
const wants = await promptConfirmOrBack(
{ nonInteractive: config.nonInteractive },
'Connect a cloud coding agent?',
true
);
if (wants !== true) {
outro('Skipped. Connect later with `polylane integration connect --category code-agent`.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this decline-path outro prints even with --quiet. Consistent with other clack output in the repo, so cosmetic at most.


Generated by Claude Code

return;
}
Comment on lines +643 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (non-blocking): Ctrl+C at the offer prompt is indistinguishable from an explicit "No" — promptConfirmOrBack maps the clack cancel to BACK (src/utils/prompt.ts:119), and wants !== true folds it into the decline path, printing the cheerful "Skipped. Connect later…" outro. Exit 0 on interrupt matches repo convention (cancel at the picker also exits 0 via cancel('Nothing connected.')), so only the message choice is debatable — an interrupt reading as a deliberate skip is worth a distinct, cancel()-styled message on BACK. Fine to ship as-is or as a follow-up.


Generated by Claude Code

}

const workspaceId = await requireWorkspace(config);
const api = new PolylaneAPI(config);

// Type selection restarts whenever the user backs out of the first step of
// the chosen flow, so nothing is committed until a flow completes.
Expand Down
22 changes: 22 additions & 0 deletions test/integration-connect-offer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { shouldOfferCodeAgent } from '../src/commands/integration/connect';

describe('shouldOfferCodeAgent', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: nice coverage — these four tests exercise the pure gate (shouldOfferCodeAgent) across all its axes. The interactive prompt flow itself (note → confirm → skip/proceed) is untested, though the repo has no pty/e2e harness, so that matches existing convention. Noting for completeness only.


Generated by Claude Code

it('offers on an interactive code-agent picker', () => {
assert.equal(shouldOfferCodeAgent('code-agent', false, true), true);
});

it('never offers outside the code-agent category', () => {
assert.equal(shouldOfferCodeAgent(undefined, false, true), false);
assert.equal(shouldOfferCodeAgent('observability', false, true), false);
});

it('never offers when --type already decided', () => {
assert.equal(shouldOfferCodeAgent('code-agent', true, true), false);
});

it('never offers non-interactively', () => {
assert.equal(shouldOfferCodeAgent('code-agent', false, false), false);
});
});
Loading