mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import { execa } from 'execa'
|
|
import * as React from 'react'
|
|
import { useEffect, useState } from 'react'
|
|
import { Select } from '../../components/CustomSelect/index.js'
|
|
import { Dialog } from '../../components/design-system/Dialog.js'
|
|
import { LoadingState } from '../../components/design-system/LoadingState.js'
|
|
import { Box, Text } from '../../ink.js'
|
|
import {
|
|
logEvent,
|
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
|
|
} from '../../services/analytics/index.js'
|
|
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
|
import { openBrowser } from '../../utils/browser.js'
|
|
import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js'
|
|
import {
|
|
createDefaultEnvironment,
|
|
getCodeWebUrl,
|
|
type ImportTokenError,
|
|
importGithubToken,
|
|
isSignedIn,
|
|
RedactedGithubToken,
|
|
} from './api.js'
|
|
|
|
type CheckResult =
|
|
| { status: 'not_signed_in' }
|
|
| { status: 'has_gh_token'; token: RedactedGithubToken }
|
|
| { status: 'gh_not_installed' }
|
|
| { status: 'gh_not_authenticated' }
|
|
|
|
async function checkLoginState(): Promise<CheckResult> {
|
|
if (!(await isSignedIn())) {
|
|
return { status: 'not_signed_in' }
|
|
}
|
|
|
|
const ghStatus = await getGhAuthStatus()
|
|
if (ghStatus === 'not_installed') {
|
|
return { status: 'gh_not_installed' }
|
|
}
|
|
if (ghStatus === 'not_authenticated') {
|
|
return { status: 'gh_not_authenticated' }
|
|
}
|
|
|
|
// ghStatus === 'authenticated'. getGhAuthStatus spawns with stdout:'ignore'
|
|
// (telemetry-safe); spawn once more with stdout:'pipe' to read the token.
|
|
const { stdout } = await execa('gh', ['auth', 'token'], {
|
|
stdout: 'pipe',
|
|
stderr: 'ignore',
|
|
timeout: 5000,
|
|
reject: false,
|
|
})
|
|
const trimmed = stdout.trim()
|
|
if (!trimmed) {
|
|
return { status: 'gh_not_authenticated' }
|
|
}
|
|
return { status: 'has_gh_token', token: new RedactedGithubToken(trimmed) }
|
|
}
|
|
|
|
function errorMessage(err: ImportTokenError, codeUrl: string): string {
|
|
switch (err.kind) {
|
|
case 'not_signed_in':
|
|
return `Login failed. Please visit ${codeUrl} and login using the GitHub App`
|
|
case 'invalid_token':
|
|
return 'GitHub rejected that token. Run `gh auth login` and try again.'
|
|
case 'server':
|
|
return `Server error (${err.status}). Try again in a moment.`
|
|
case 'network':
|
|
return "Couldn't reach the server. Check your connection."
|
|
}
|
|
}
|
|
|
|
type Step =
|
|
| { name: 'checking' }
|
|
| { name: 'confirm'; token: RedactedGithubToken }
|
|
| { name: 'uploading' }
|
|
|
|
function Web({ onDone }: { onDone: LocalJSXCommandOnDone }) {
|
|
const [step, setStep] = useState<Step>({ name: 'checking' })
|
|
|
|
useEffect(() => {
|
|
logEvent('tengu_remote_setup_started', {})
|
|
void checkLoginState().then(async result => {
|
|
switch (result.status) {
|
|
case 'not_signed_in':
|
|
logEvent('tengu_remote_setup_result', {
|
|
result: 'not_signed_in' as SafeString,
|
|
})
|
|
onDone('Not signed in to Claude. Run /login first.')
|
|
return
|
|
case 'gh_not_installed':
|
|
case 'gh_not_authenticated': {
|
|
const url = `${getCodeWebUrl()}/onboarding?step=alt-auth`
|
|
await openBrowser(url)
|
|
logEvent('tengu_remote_setup_result', {
|
|
result: result.status as SafeString,
|
|
})
|
|
onDone(
|
|
result.status === 'gh_not_installed'
|
|
? `GitHub CLI not found. Install it via https://cli.github.com/, then run \`gh auth login\`, or connect GitHub on the web: ${url}`
|
|
: `GitHub CLI not authenticated. Run \`gh auth login\` and try again, or connect GitHub on the web: ${url}`,
|
|
)
|
|
return
|
|
}
|
|
case 'has_gh_token':
|
|
setStep({ name: 'confirm', token: result.token })
|
|
}
|
|
})
|
|
// onDone is stable across renders; intentionally not in deps.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
const handleCancel = () => {
|
|
logEvent('tengu_remote_setup_result', {
|
|
result: 'cancelled' as SafeString,
|
|
})
|
|
onDone()
|
|
}
|
|
|
|
const handleConfirm = async (token: RedactedGithubToken) => {
|
|
setStep({ name: 'uploading' })
|
|
|
|
const result = await importGithubToken(token)
|
|
if (!result.ok) {
|
|
logEvent('tengu_remote_setup_result', {
|
|
result: 'import_failed' as SafeString,
|
|
error_kind: result.error.kind as SafeString,
|
|
})
|
|
onDone(errorMessage(result.error, getCodeWebUrl()))
|
|
return
|
|
}
|
|
|
|
// Token import succeeded. Environment creation is best-effort — if it
|
|
// fails, the web state machine routes to env-setup on landing, which is
|
|
// one extra click but still better than the OAuth dance.
|
|
await createDefaultEnvironment()
|
|
|
|
const url = getCodeWebUrl()
|
|
await openBrowser(url)
|
|
|
|
logEvent('tengu_remote_setup_result', {
|
|
result: 'success' as SafeString,
|
|
})
|
|
onDone(`Connected as ${result.result.github_username}. Opened ${url}`)
|
|
}
|
|
|
|
if (step.name === 'checking') {
|
|
return <LoadingState message="Checking login status…" />
|
|
}
|
|
|
|
if (step.name === 'uploading') {
|
|
return <LoadingState message="Connecting GitHub to Claude…" />
|
|
}
|
|
|
|
const token = step.token
|
|
return (
|
|
<Dialog
|
|
title="Connect Claude on the web to GitHub?"
|
|
onCancel={handleCancel}
|
|
hideInputGuide
|
|
>
|
|
<Box flexDirection="column">
|
|
<Text>
|
|
Claude on the web requires connecting to your GitHub account to clone
|
|
and push code on your behalf.
|
|
</Text>
|
|
<Text dimColor>
|
|
Your local credentials are used to authenticate with GitHub
|
|
</Text>
|
|
</Box>
|
|
<Select
|
|
options={[
|
|
{ label: 'Continue', value: 'send' },
|
|
{ label: 'Cancel', value: 'cancel' },
|
|
]}
|
|
onChange={value => {
|
|
if (value === 'send') {
|
|
void handleConfirm(token)
|
|
} else {
|
|
handleCancel()
|
|
}
|
|
}}
|
|
onCancel={handleCancel}
|
|
/>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export async function call(
|
|
onDone: LocalJSXCommandOnDone,
|
|
): Promise<React.ReactNode> {
|
|
return <Web onDone={onDone} />
|
|
}
|