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
101 changes: 101 additions & 0 deletions packages/ccdb-cli/src/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const common = `通用选项
--profile <环境> 默认 production,可选 local / test / pre / 自定义
--json 机器可读输出;登录进度为 JSON 行
--timeout <毫秒> 单次请求超时
-h, --help 查看帮助
--version 查看版本
`;

export function helpText(parts: string[]): string {
const topic = parts.slice(0, 2).join(' ');
const pages: Record<string, string> = {
'auth login': `登录 CCDB

用法
ccdb-cli auth login [选项]

登录选项
--method <方式> device(默认)/ pkce / api-key
--no-browser 不自动打开浏览器,手动访问授权链接

示例
ccdb-cli auth login --profile test

设备码和授权链接会显示在终端,由你在浏览器完成授权。
API Key 通过隐藏输入或 stdin 提供,不要放进命令参数。
环境 CCDB_API_KEY 优先于保存的凭证;OAuth 失败不会自动改用 Key。
`,
'auth status': `查看本地凭证状态

ccdb-cli auth status [--profile <环境>] [--json]

状态不代表远程权限有效;不会输出 Token 或完整 API Key。
`,
'auth logout': `退出登录

ccdb-cli auth logout [--profile <环境>] [--revoke]

默认只清理本地凭证。
--revoke 撤销整条应用授权,可能影响共用授权的其他工具。
不会删除环境 CCDB_API_KEY,也不会在服务端停用 API Key。
`,
'factor search': `搜索排放因子

用法
ccdb-cli factor search <关键词> [选项]

查询选项
--country <地区> 地区筛选,可重复
--year <年份> 年份筛选,可重复
--source-level <来源> 来源级别筛选,可重复
--limit <数量> 返回 1~10 条,默认 5
--language <语言> zh / en
--accounting-type <类> product / enterprise

示例
ccdb-cli factor search "电力" --profile test --limit 5

返回的是候选因子;推荐前请核对详情、单位及适用范围。
`,
'factor detail': `查看因子详情

ccdb-cli factor detail <factorId> [--language zh|en] [选项]

factorId 必须原样使用搜索结果中的字符串 ID,不使用示例 ID。
详情与搜索须保持相同 profile;受限值不是 0,不能用于计算。
`,
doctor: `检查服务配置

ccdb-cli doctor [--profile <环境>] [--json]

检查发现端点和本地凭证状态,不查询因子、不消耗查询配额。
诊断通过不代表业务权限有效。
`,
};
if (pages[topic]) return pages[topic] + '\n' + common;
return `ccdb-cli — CCDB 排放因子查询

用法
ccdb-cli <命令> [选项]

账号
auth login 登录(默认设备码授权)
auth status 查看本地凭证状态
auth logout 退出登录

因子
factor search <关键词> 搜索候选因子
factor detail <ID> 查看因子详情

诊断
doctor 检查服务配置

快速开始(测试环境)
ccdb-cli auth login --profile test
ccdb-cli factor search "电力" --profile test --limit 5

${common}
更多参数:ccdb-cli factor search --help
登录选项:ccdb-cli auth login --help
`;
}
25 changes: 25 additions & 0 deletions packages/ccdb-cli/src/output.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
/** Human login progress only; --json retains the original event objects. */
export function loginProgress(event: Record<string, unknown>): string {
const text = (value: unknown) =>
typeof value === 'string' ? value.replace(/[\x00-\x1f\x7f-\x9f]/g, '') : '';
if (event.event === 'credential_storage') return '凭证存储\n ' + text(event.message);
if (event.event === 'authorization_pending') {
const url = text(event.verificationUri || event.authorizationUri);
const code = text(event.userCode);
const expires =
typeof event.expiresIn === 'number' && event.expiresIn > 0
? '\n 有效期:' + Math.ceil(event.expiresIn / 60) + ' 分钟'
: '';
return (
'等待浏览器授权\n' +
(code ? ' 设备码:' + code + '\n' : '') +
' 授权链接:' +
url +
expires +
'\n\n 请在浏览器完成授权,终端会继续等待。'
);
}
if (event.event === 'browser_unavailable') return '浏览器未能自动打开,请手动访问上方授权链接。';
return '登录提示:' + (text(event.message) || '正在处理,请稍候。');
}

/** Localized fields remain untouched in JSON; only the terminal view selects a label. */
export function label(value: unknown, language = 'zh'): string {
if (value === null || value === undefined) return '—';
Expand Down
23 changes: 4 additions & 19 deletions packages/ccdb-cli/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,9 @@ import { parseArgs } from 'node:util';
import { CcdbClient, CcdbError, asError, exitCode, type Config } from 'ccdb-client';
import { appConfig } from './config.js';
import { interactiveLogin } from 'ccdb-client/auth/interactive';
import { humanOutput } from './output.js';
import { humanOutput, loginProgress } from './output.js';
import { helpText } from './help.js';

const HELP = `ccdb-cli 0.1.2 — CCDB 因子查询工具

ccdb-cli auth login [--method device|pkce|api-key] [--no-browser]
ccdb-cli auth status
ccdb-cli auth logout [--revoke]
ccdb-cli factor search <query> [--language zh|en] [--accounting-type product|enterprise]
[--country 中国] [--year 2025] [--source-level 国家排放因子] [--limit 5]
ccdb-cli factor detail <factorId> [--language zh|en]
ccdb-cli doctor

公共选项:--profile local|test|pre|production|自定义 --json --timeout <毫秒>
筛选项可重复传入;factorId 必须原样使用字符串。
API Key 使用 CCDB_API_KEY 环境变量或 api-key 登录的隐藏输入/stdin,不接受明文命令行参数。
auth logout --revoke 会撤销整条应用授权,可能影响共用该授权的 MCP/CLI/Skill。
--json 成功/错误为 JSON;登录进度为不含 Token 的 JSON 行。
`;
export async function runCli(
args: string[],
env: NodeJS.ProcessEnv = process.env,
Expand Down Expand Up @@ -52,7 +37,7 @@ export async function runCli(
},
});
if (v.help || (!p.length && !v.version)) {
process.stdout.write(HELP);
process.stdout.write(helpText(p));
return 0;
}
if (v.version) {
Expand Down Expand Up @@ -87,7 +72,7 @@ export async function runCli(
if (selected === 'auth login') {
const notify = (event: Record<string, unknown>) => {
if (v.json) process.stdout.write(JSON.stringify(event) + '\n');
else process.stderr.write(JSON.stringify(event, null, 2) + '\n');
else process.stderr.write(loginProgress(event) + '\n');
};
output = await interactiveLogin(
client.auth,
Expand Down
59 changes: 59 additions & 0 deletions tests/help.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { loginProgress } from '../packages/ccdb-cli/src/output.js';

test('root and command help work without valid configuration or starting services', () => {
for (const args of [
['--help'],
['auth', 'login', '--help'],
['factor', 'search', '-h'],
['factor', 'detail', '--help'],
]) {
const result = spawnSync(
process.execPath,
[resolve('packages/ccdb-cli/dist/main.mjs'), ...args],
{
encoding: 'utf8',
timeout: 5000,
env: {
...process.env,
CCDB_PROFILE: 'unregistered-help-fixture',
CCDB_API_BASE: 'invalid-url',
CCDB_MCP_PORT: 'invalid-port',
},
},
);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stderr, '');
assert.match(result.stdout, /ccdb-cli/);
assert.ok(result.stdout.split('\n').length > 5);
}
});

test('device and PKCE progress are readable without rendering arbitrary secret fields', () => {
const device = loginProgress({
event: 'authorization_pending',
verificationUri: 'https://example.com/device',
userCode: 'ABCD-EFGH',
expiresIn: 900,
accessToken: 'secret-fixture',
});
assert.match(device, /设备码:ABCD-EFGH/);
assert.match(device, /有效期:15 分钟/);
assert.match(device, /https:\/\/example.com\/device/);
assert.ok(!device.includes('secret-fixture'));
const pkce = loginProgress({
event: 'authorization_pending',
authorizationUri: 'https://example.com/authorize',
});
assert.match(pkce, /授权链接:https:\/\/example.com\/authorize/);
assert.ok(!pkce.includes('设备码'));
assert.match(loginProgress({ event: 'browser_unavailable' }), /手动访问/);
assert.match(
loginProgress({ event: 'credential_storage', message: '已使用加密文件存储' }),
/加密文件存储/,
);
assert.ok(!loginProgress({ event: 'unknown', message: '\u001bunsafe\u0007' }).includes('\u001b'));
});
Loading