From 0249f0469a7746b4d5370ca11cfd654c0295abc5 Mon Sep 17 00:00:00 2001 From: xxx <616896861@qq.com> Date: Tue, 12 May 2026 18:53:07 -0700 Subject: [PATCH] chore: switch npm publish to OIDC trusted publisher and simplify codebase - ci(release): drop long-lived NPM_TOKEN secret; rely on npm Trusted Publisher (OIDC) via id-token: write + npm@latest + npm publish - fix(package): drop "./" prefix from bin entry to silence publish warning - refactor(installer): fetch checksums-sha256.txt directly into memory instead of mkdtemp + downloadFile + readFile + rm dance - fix(download): cancel ReadableStream reader on error to avoid leak - refactor(platform): remove identity mapArch helper and simplify isMuslWith API to a thin pattern-free probe - refactor(providers): replace `as ProviderSpec` cast with explicit not-found throw and simplify buildEnv adapters to point-free style - chore: drop file-header path comments --- .github/workflows/release.yml | 6 ++---- package.json | 2 +- src/core/download.test.ts | 1 - src/core/download.ts | 2 +- src/core/installer.test.ts | 1 - src/core/installer.ts | 29 ++++++++++++----------------- src/core/platform.test.ts | 11 ----------- src/core/platform.ts | 28 ++++++++-------------------- src/providers/configure.ts | 13 ++++++++----- src/providers/specs.ts | 14 +++++++------- src/utils/json-merge.ts | 4 +--- 11 files changed, 40 insertions(+), 71 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3376e4..c67a8cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,3 @@ -# .github/workflows/release.yml name: Release on: @@ -21,9 +20,8 @@ jobs: node-version: '22' cache: pnpm registry-url: 'https://registry.npmjs.org' + - run: npm install -g npm@latest - run: pnpm install --frozen-lockfile - run: pnpm test - run: pnpm build - - run: pnpm publish --provenance --no-git-checks --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: npm publish --provenance --access public diff --git a/package.json b/package.json index b9e5a8f..1cf2449 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "description": "Claude Code 中国大陆下载与配置工具", "type": "module", - "bin": { "ccc": "./dist/cli.js" }, + "bin": { "ccc": "dist/cli.js" }, "files": ["dist", "README.md", "LICENSE"], "engines": { "node": ">=18" }, "scripts": { diff --git a/src/core/download.test.ts b/src/core/download.test.ts index 3d44b71..f07e282 100644 --- a/src/core/download.test.ts +++ b/src/core/download.test.ts @@ -1,4 +1,3 @@ -// src/core/download.test.ts import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { type Server, createServer } from 'node:http'; diff --git a/src/core/download.ts b/src/core/download.ts index c769c8a..45c137b 100644 --- a/src/core/download.ts +++ b/src/core/download.ts @@ -1,4 +1,3 @@ -// src/core/download.ts import type { Hash } from 'node:crypto'; import { once } from 'node:events'; import { createWriteStream } from 'node:fs'; @@ -47,6 +46,7 @@ export async function downloadFile( } } catch (err) { file.destroy(); + await reader.cancel().catch(() => {}); throw err; } finally { bar?.stop(); diff --git a/src/core/installer.test.ts b/src/core/installer.test.ts index 6dc8911..f066faf 100644 --- a/src/core/installer.test.ts +++ b/src/core/installer.test.ts @@ -1,4 +1,3 @@ -// src/core/installer.test.ts import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { type Server, createServer } from 'node:http'; diff --git a/src/core/installer.ts b/src/core/installer.ts index 45b4351..fabd274 100644 --- a/src/core/installer.ts +++ b/src/core/installer.ts @@ -1,7 +1,5 @@ -// src/core/installer.ts import { createHash } from 'node:crypto'; -import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { chmod, mkdir, rename, rm, stat } from 'node:fs/promises'; import { join } from 'node:path'; import pc from 'picocolors'; import { UnsupportedPlatformError } from '../utils/errors.js'; @@ -18,6 +16,12 @@ export interface InstallOptions { showProgress?: boolean; } +async function fetchChecksumText(url: string): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.text(); +} + export async function install(opts: InstallOptions): Promise { const binDir = join(opts.stateDir, 'bin'); const destPath = join(binDir, 'claude'); @@ -26,9 +30,7 @@ export async function install(opts: InstallOptions): Promise { try { await stat(destPath); return destPath; - } catch { - // does not exist — continue - } + } catch {} } if (!isSupportedPlatform(opts.platform)) { @@ -47,16 +49,12 @@ export async function install(opts: InstallOptions): Promise { const checksumURL = `${base}/checksums-sha256.txt`; const binaryURL = `${base}/${platStr}/claude`; const assetName = buildAssetName(opts.platform, version); - - const tmpDir = await mkdtemp(join(tmpdir(), 'claude-code-cn-')); - const checksumPath = join(tmpDir, 'checksums-sha256.txt'); const tmpBinaryPath = `${destPath}.tmp`; - let checksumAvailable = true; + let checksumText: string | null = null; try { - await downloadFile(checksumURL, checksumPath, null, { showProgress: false }); + checksumText = await fetchChecksumText(checksumURL); } catch (err) { - checksumAvailable = false; process.stderr.write( pc.yellow(`warning: 无法下载 checksums-sha256.txt, 将跳过校验 (${(err as Error).message})\n`), ); @@ -70,10 +68,9 @@ export async function install(opts: InstallOptions): Promise { label: '下载中', }); - if (checksumAvailable) { + if (checksumText) { process.stdout.write('正在校验 SHA-256...\n'); - const data = await readFile(checksumPath, 'utf8'); - const expected = parseChecksumFile(data, assetName); + const expected = parseChecksumFile(checksumText, assetName); const actual = hash.digest('hex'); if (actual !== expected) { throw new Error(`SHA-256 不匹配\n expected: ${expected}\n got: ${actual}`); @@ -86,8 +83,6 @@ export async function install(opts: InstallOptions): Promise { } catch (err) { await rm(tmpBinaryPath, { force: true }); throw err; - } finally { - await rm(tmpDir, { recursive: true, force: true }); } return destPath; diff --git a/src/core/platform.test.ts b/src/core/platform.test.ts index 7bd770c..499f95d 100644 --- a/src/core/platform.test.ts +++ b/src/core/platform.test.ts @@ -4,20 +4,9 @@ import { buildAssetName, isMuslWith, isSupportedPlatform, - mapArch, platformString, } from './platform.js'; -describe('mapArch', () => { - it.each([ - ['x64', 'x64'], - ['arm64', 'arm64'], - ['ia32', 'ia32'], - ])('maps %s -> %s', (input, want) => { - expect(mapArch(input)).toBe(want); - }); -}); - describe('isMuslWith', () => { it('true when glob returns matches', () => { expect(isMuslWith(() => ['/lib/ld-musl-x86_64.so.1'])).toBe(true); diff --git a/src/core/platform.ts b/src/core/platform.ts index bc76d7e..ba4b2b3 100644 --- a/src/core/platform.ts +++ b/src/core/platform.ts @@ -1,45 +1,33 @@ import { readdirSync } from 'node:fs'; -export type SupportedOS = 'darwin' | 'linux'; -export type Arch = string; - export interface Platform { os: string; arch: string; variant: '' | 'musl'; } -export function mapArch(arch: string): string { - // Node 'x64' already matches the asset naming; keep this as a hook for future translations. - return arch; -} - -// Glob abstraction kept simple: caller supplies a function that returns matches -// for a pattern. The default scans /lib for ld-musl-*.so* without pulling in -// a glob library. Node 22's fs.glob is intentionally avoided for ≥18 compatibility. -export function isMuslWith(globFn: (pattern: string) => string[]): boolean { +export function isMuslWith(globFn: () => string[]): boolean { try { - return globFn('/lib/ld-musl-*.so*').length > 0; + return globFn().length > 0; } catch { return false; } } -function defaultGlob(pattern: string): string[] { - // Only the specific pattern '/lib/ld-musl-*.so*' is needed. - if (pattern !== '/lib/ld-musl-*.so*') return []; - const names = readdirSync('/lib'); +function scanMuslLoaders(): string[] { const re = /^ld-musl-.*\.so/; - return names.filter((n) => re.test(n)).map((n) => `/lib/${n}`); + return readdirSync('/lib') + .filter((n) => re.test(n)) + .map((n) => `/lib/${n}`); } export function isMusl(): boolean { - return isMuslWith(defaultGlob); + return isMuslWith(scanMuslLoaders); } export function detectPlatform(): Platform { const os = process.platform; - const arch = mapArch(process.arch); + const arch = process.arch; const variant: '' | 'musl' = os === 'linux' && isMusl() ? 'musl' : ''; return { os, arch, variant }; } diff --git a/src/providers/configure.ts b/src/providers/configure.ts index c185d9b..727a733 100644 --- a/src/providers/configure.ts +++ b/src/providers/configure.ts @@ -3,7 +3,7 @@ import { confirm, input, select } from '@inquirer/prompts'; import { InterruptedError } from '../utils/errors.js'; import { mergeJSONFile } from '../utils/json-merge.js'; import { PROVIDER_ENV_KEYS, type ProviderEnv } from './env-keys.js'; -import { PROVIDER_SPECS, type ProviderSpec } from './specs.js'; +import { PROVIDER_SPECS } from './specs.js'; export interface ConfigureOptions { settingsPath: string; @@ -40,21 +40,24 @@ export async function configureProvider( choices: PROVIDER_SPECS.map((s) => ({ name: s.name, value: s.name })), }), ); - const spec = PROVIDER_SPECS.find((s) => s.name === providerName) as ProviderSpec; + const spec = PROVIDER_SPECS.find((s) => s.name === providerName); + if (!spec) throw new Error(`未知 Provider: ${providerName}`); let baseURL = ''; if (spec.baseURLPrompt) { - baseURL = await ask(() => input({ message: spec.baseURLPrompt as string })); + const message = spec.baseURLPrompt; + baseURL = await ask(() => input({ message })); } const apiKey = await ask(() => input({ message: spec.keyPrompt })); let secondArg = ''; - if (spec.modelOptions && spec.modelOptions.length > 0) { + const modelOptions = spec.modelOptions; + if (modelOptions && modelOptions.length > 0) { secondArg = await ask(() => select({ message: '请选择模型', - choices: (spec.modelOptions as string[]).map((m) => ({ name: m, value: m })), + choices: modelOptions.map((m) => ({ name: m, value: m })), default: spec.modelDefault, }), ); diff --git a/src/providers/specs.ts b/src/providers/specs.ts index 5163cb8..7819ba1 100644 --- a/src/providers/specs.ts +++ b/src/providers/specs.ts @@ -27,31 +27,31 @@ export const PROVIDER_SPECS: readonly ProviderSpec[] = [ name: 'KimiCode', keyPrompt: '请输入 KimiCode API Key', needClaudeJSON: false, - buildEnv: (k) => kimiCodeEnv(k), + buildEnv: kimiCodeEnv, }, { name: 'Moonshot (Kimi)', keyPrompt: '请输入 Moonshot API Key', needClaudeJSON: false, - buildEnv: (k) => moonshotEnv(k), + buildEnv: moonshotEnv, }, { name: 'DeepSeek', keyPrompt: '请输入 DeepSeek API Key', needClaudeJSON: false, - buildEnv: (k) => deepseekEnv(k), + buildEnv: deepseekEnv, }, { name: 'Zhipu (GLM)', keyPrompt: '请输入 智谱 GLM API Key', needClaudeJSON: true, - buildEnv: (k) => glmEnv(k), + buildEnv: glmEnv, }, { name: 'MiniMax', keyPrompt: '请输入 MiniMax API Key', needClaudeJSON: true, - buildEnv: (k) => minimaxEnv(k), + buildEnv: minimaxEnv, }, { name: 'Alibaba Cloud (Qwen)', @@ -100,13 +100,13 @@ export const PROVIDER_SPECS: readonly ProviderSpec[] = [ keyPrompt: '请输入 小米 Mimo Token', baseURLPrompt: '请输入 小米 Mimo Base URL', needClaudeJSON: true, - buildEnv: (k, baseURL) => mimoEnv(k, baseURL), + buildEnv: mimoEnv, }, { name: 'Custom provider', keyPrompt: '请输入 自定义 Provider Token', baseURLPrompt: '请输入 自定义 Provider Base URL', needClaudeJSON: false, - buildEnv: (k, baseURL) => customEnv(k, baseURL), + buildEnv: customEnv, }, ]; diff --git a/src/utils/json-merge.ts b/src/utils/json-merge.ts index b1d6fe2..af67ceb 100644 --- a/src/utils/json-merge.ts +++ b/src/utils/json-merge.ts @@ -17,9 +17,7 @@ export async function mergeJSONFile( } catch { process.stderr.write(`warning: ${path} 不是合法 JSON, 将覆盖\n`); } - } catch { - // file missing — start fresh - } + } catch {} apply(existing); const out = `${JSON.stringify(existing, null, 2)}\n`; await writeFile(path, out, 'utf8');