refactor(cli): remove Vertex AI auth step and add gcloud retry (#84)

* refactor(cli): remove Vertex AI auth step and add gcloud project listing retry

The Vertex AI auth step only offered one option (use existing ADC credentials)
making it a redundant click. Remove it and go straight to project selection.

When gcloud project listing fails (e.g. expired credentials), show a
diagnostic message and offer a "Retry" option instead of silently falling
back to an empty list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(cli): show gcloud project listing errors inline in the prompt

Move the gcloud failure message into the select prompt instead of writing
it to stdout separately, and highlight it with yellow ANSI coloring so
users notice the remediation steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luca Martial 2026-05-13 19:49:49 -04:00 committed by GitHub
parent b00c1a11a9
commit 5b4ba73e64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 191 additions and 106 deletions

View file

@ -283,9 +283,9 @@ describe('setup Anthropic model step', () => {
expect(io.stdout()).toContain('LLM ready: yes (claude-sonnet-4-6)');
});
it('uses existing Vertex AI credentials without offering to run gcloud auth', async () => {
it('uses existing Vertex AI credentials without an extra auth choice', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['vertex', 'existing', 'local-gcp-project', 'claude-sonnet-4-6'] });
const prompts = makePromptAdapter({ selectValues: ['vertex', 'local-gcp-project', 'claude-sonnet-4-6'] });
const readGcloudProject = vi.fn(async () => 'local-gcp-project');
const listGcloudProjects = vi.fn(async () => [
{ projectId: 'local-gcp-project', name: 'Local project' },
@ -306,13 +306,9 @@ describe('setup Anthropic model step', () => {
);
expect(result.status).toBe('ready');
expect(prompts.select).toHaveBeenCalledWith(
expect(prompts.select).not.toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('How should KTX authenticate with Google Vertex AI?'),
options: [
{ value: 'existing', label: 'Use existing gcloud/Application Default Credentials' },
{ value: 'back', label: 'Back' },
],
}),
);
expect(readGcloudProject).toHaveBeenCalled();
@ -358,9 +354,45 @@ describe('setup Anthropic model step', () => {
});
});
it('skips the Vertex AI auth choice when Application Default Credentials are the only option', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['vertex', 'local-gcp-project', 'claude-sonnet-4-6'] });
const healthCheck = vi.fn(async () => ({ ok: true as const }));
const result = await runKtxSetupAnthropicModelStep(
{ projectDir: tempDir, inputMode: 'auto', skipLlm: false },
io.io,
{
prompts,
env: {},
readGcloudProject: vi.fn(async () => 'local-gcp-project'),
listGcloudProjects: vi.fn(async () => [{ projectId: 'local-gcp-project', name: 'Local project' }]),
healthCheck,
},
);
expect(result.status).toBe('ready');
expect(prompts.select).not.toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('How should KTX authenticate with Google Vertex AI?'),
}),
);
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Which Google Cloud project should KTX use for Vertex AI?'),
}),
);
expect(healthCheck).toHaveBeenCalledWith(
expect.objectContaining({
backend: 'vertex',
vertex: { project: 'local-gcp-project', location: 'us-east5' },
}),
);
});
it('lets users choose a different visible gcloud project for Vertex AI', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['vertex', 'existing', 'other-gcp-project', 'claude-sonnet-4-6'] });
const prompts = makePromptAdapter({ selectValues: ['vertex', 'other-gcp-project', 'claude-sonnet-4-6'] });
const healthCheck = vi.fn(async () => ({ ok: true as const }));
const result = await runKtxSetupAnthropicModelStep(
@ -395,7 +427,7 @@ describe('setup Anthropic model step', () => {
it('allows manual Vertex AI project entry when gcloud project listing is empty', async () => {
const io = makeIo();
const prompts = makePromptAdapter({
selectValues: ['vertex', 'existing', 'manual', 'claude-sonnet-4-6'],
selectValues: ['vertex', 'manual', 'claude-sonnet-4-6'],
textValues: ['manual-gcp-project'],
});
const healthCheck = vi.fn(async () => ({ ok: true as const }));
@ -434,8 +466,66 @@ describe('setup Anthropic model step', () => {
);
});
it('lets users retry Vertex AI project listing after gcloud auth fails', async () => {
const io = makeIo();
const prompts = makePromptAdapter({ selectValues: ['vertex', 'retry', 'other-gcp-project', 'claude-sonnet-4-6'] });
const listGcloudProjects = vi
.fn()
.mockRejectedValueOnce(new Error('Reauthentication failed. cannot prompt during non-interactive execution.'))
.mockResolvedValueOnce([
{ projectId: 'local-gcp-project', name: 'Local project' },
{ projectId: 'other-gcp-project', name: 'Other project' },
]);
const healthCheck = vi.fn(async () => ({ ok: true as const }));
const result = await runKtxSetupAnthropicModelStep(
{ projectDir: tempDir, inputMode: 'auto', skipLlm: false },
io.io,
{
prompts,
env: {},
readGcloudProject: vi.fn(async () => 'local-gcp-project'),
listGcloudProjects,
healthCheck,
},
);
expect(result.status).toBe('ready');
expect(listGcloudProjects).toHaveBeenCalledTimes(2);
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Could not list Google Cloud projects with gcloud'),
options: expect.arrayContaining([{ value: 'retry', label: 'Retry loading Google Cloud projects' }]),
}),
);
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
`${String.fromCharCode(0x1b)}[33mCould not list Google Cloud projects with gcloud`,
),
}),
);
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('gcloud auth login --update-adc'),
}),
);
expect(prompts.select).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
`${String.fromCharCode(0x1b)}[33mRun \`gcloud auth login --update-adc\``,
),
}),
);
expect(healthCheck).toHaveBeenCalledWith(
expect.objectContaining({
vertex: { project: 'other-gcp-project', location: 'us-east5' },
}),
);
});
it('returns from Vertex AI project selection Back to provider selection', async () => {
const prompts = makePromptAdapter({ selectValues: ['vertex', 'existing', 'back', 'back'] });
const prompts = makePromptAdapter({ selectValues: ['vertex', 'back', 'back'] });
const result = await runKtxSetupAnthropicModelStep(
{ projectDir: tempDir, inputMode: 'auto', skipLlm: false },
@ -450,7 +540,7 @@ describe('setup Anthropic model step', () => {
expect(result.status).toBe('back');
expect(prompts.select).toHaveBeenNthCalledWith(
4,
3,
expect.objectContaining({
message: expect.stringContaining('Which LLM provider should KTX use?'),
}),