diff --git a/packages/web/bin/cli-output.js b/packages/web/bin/cli-output.js index 92a9e845..14b9c7cc 100644 --- a/packages/web/bin/cli-output.js +++ b/packages/web/bin/cli-output.js @@ -15,6 +15,8 @@ import { select, text, password, + spinner, + progress, cancel, isCancel, } from '@clack/prompts'; diff --git a/packages/web/bin/cli-output.test.js b/packages/web/bin/cli-output.test.js new file mode 100644 index 00000000..3c2b42e7 --- /dev/null +++ b/packages/web/bin/cli-output.test.js @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; + +async function withInteractiveTty(fn) { + const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY'); + const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY'); + + Object.defineProperty(process.stdout, 'isTTY', { configurable: true, value: true }); + Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: true }); + + try { + return await fn(); + } finally { + if (stdoutDescriptor) { + Object.defineProperty(process.stdout, 'isTTY', stdoutDescriptor); + } else { + delete process.stdout.isTTY; + } + + if (stdinDescriptor) { + Object.defineProperty(process.stdin, 'isTTY', stdinDescriptor); + } else { + delete process.stdin.isTTY; + } + } +} + +describe('cli output', () => { + it('creates interactive clack spinner and progress helpers', async () => { + await withInteractiveTty(async () => { + const output = await import('./cli-output.js?interactive-test'); + + expect(output.createSpinner({})).toBeTruthy(); + await expect(output.createProgress({}, { max: 2 })).resolves.toBeTruthy(); + }); + }); +}); diff --git a/packages/web/bin/cli.test.js b/packages/web/bin/cli.test.js index bafef8ae..9faa2100 100644 --- a/packages/web/bin/cli.test.js +++ b/packages/web/bin/cli.test.js @@ -10,6 +10,11 @@ import { pathToFileURL } from 'url'; import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js'; import { requestJson } from './lib/cli-http.js'; import { inspectTunnelAttachability } from './lib/cli-lifecycle.js'; +import { DEFAULT_TUNNEL_PROVIDER_CAPABILITIES } from './lib/cli-tunnel-capabilities.js'; +import { + TUNNEL_PROVIDER_CLOUDFLARE, + TUNNEL_PROVIDER_NGROK, +} from '../server/lib/tunnels/types.js'; import { assertAuthenticatedNetworkExposure, commands, @@ -173,6 +178,13 @@ function spawnOpenChamberLikeHungServer(port) { } describe('cli args', () => { + it('loads fallback tunnel provider capabilities for CLI startup', () => { + expect(DEFAULT_TUNNEL_PROVIDER_CAPABILITIES.map((provider) => provider.provider)).toEqual([ + TUNNEL_PROVIDER_CLOUDFLARE, + TUNNEL_PROVIDER_NGROK, + ]); + }); + it('accepts legacy daemon flags as no-ops', () => { expect(parseArgs(['serve', '--daemon']).removedFlagErrors).toEqual([]); expect(parseArgs(['serve', '-d']).removedFlagErrors).toEqual([]); diff --git a/packages/web/server/lib/quota/providers/google/index.js b/packages/web/server/lib/quota/providers/google/index.js index e24b3ef7..c74497cf 100644 --- a/packages/web/server/lib/quota/providers/google/index.js +++ b/packages/web/server/lib/quota/providers/google/index.js @@ -11,12 +11,20 @@ import { fetchGoogleModels } from './api.js'; +export { resolveGoogleAuthSources } from './auth.js'; + +export const providerId = 'google'; +export const providerName = 'Google'; +export const aliases = ['google', 'google.oauth']; + +export const isConfigured = () => resolveGoogleAuthSources().length > 0; + export const fetchGoogleQuota = async () => { const authSources = resolveGoogleAuthSources(); if (!authSources.length) { return buildResult({ - providerId: 'google', - providerName: 'Google', + providerId, + providerName, ok: false, configured: false, error: 'Not configured' @@ -76,8 +84,8 @@ export const fetchGoogleQuota = async () => { if (!Object.keys(models).length) { return buildResult({ - providerId: 'google', - providerName: 'Google', + providerId, + providerName, ok: false, configured: true, error: sourceErrors[0] ?? 'Failed to fetch models' @@ -85,8 +93,8 @@ export const fetchGoogleQuota = async () => { } return buildResult({ - providerId: 'google', - providerName: 'Google', + providerId, + providerName, ok: true, configured: true, usage: { diff --git a/packages/web/server/lib/quota/providers/index.js b/packages/web/server/lib/quota/providers/index.js index 0b212d2e..c27cd46f 100644 --- a/packages/web/server/lib/quota/providers/index.js +++ b/packages/web/server/lib/quota/providers/index.js @@ -43,9 +43,9 @@ const registry = { fetchQuota: cursor.fetchQuota }, google: { - providerId: 'google', - providerName: 'Google', - isConfigured: () => google.resolveGoogleAuthSources().length > 0, + providerId: google.providerId, + providerName: google.providerName, + isConfigured: google.isConfigured, fetchQuota: google.fetchGoogleQuota }, 'zai-coding-plan': { diff --git a/packages/web/server/lib/quota/providers/index.test.js b/packages/web/server/lib/quota/providers/index.test.js new file mode 100644 index 00000000..b95cfb8a --- /dev/null +++ b/packages/web/server/lib/quota/providers/index.test.js @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import * as google from './google/index.js'; +import { listConfiguredQuotaProviders } from './index.js'; + +describe('quota provider registry', () => { + it('exposes google provider configuration helpers through the provider module', () => { + expect(google.providerId).toBe('google'); + expect(google.providerName).toBe('Google'); + expect(typeof google.isConfigured).toBe('function'); + expect(typeof google.resolveGoogleAuthSources).toBe('function'); + }); + + it('can list configured providers without missing provider exports', () => { + expect(() => listConfiguredQuotaProviders()).not.toThrow(); + }); +}); diff --git a/packages/web/server/lib/tunnels/providers/ngrok.js b/packages/web/server/lib/tunnels/providers/ngrok.js index a567a36b..76d94399 100644 --- a/packages/web/server/lib/tunnels/providers/ngrok.js +++ b/packages/web/server/lib/tunnels/providers/ngrok.js @@ -13,7 +13,7 @@ import { } from '../types.js'; import { getTunnelDependencyInstallInfo } from '../install-help.js'; -const ngrokTunnelProviderCapabilities = { +export const ngrokTunnelProviderCapabilities = { provider: TUNNEL_PROVIDER_NGROK, defaults: { mode: TUNNEL_MODE_QUICK,