@cnbcool/cnb-api-generate 2.14.1 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/lib/login.ts
CHANGED
|
@@ -16,13 +16,14 @@ export function registerLoginCommand(program: Command): void {
|
|
|
16
16
|
.command('login')
|
|
17
17
|
.description('通过 OAuth2 设备授权流登录 CNB,获取并保存 access_token')
|
|
18
18
|
.option('--client-id <string>', 'OAuth2 client_id', process.env.OAUTH2_CLIENT_ID || 'cnb_cli')
|
|
19
|
+
.option('--host <string>', '平台域名(如 cnb.cool,可带端口如 cnb.cool:8080),优先级高于 git remote 与 CNB_API_ENDPOINT')
|
|
19
20
|
.option('--debug', '打印调试信息', false)
|
|
20
21
|
.helpOption('-h, --help', '显示帮助文档')
|
|
21
22
|
.action(async (opts) => {
|
|
22
23
|
const cfg: LoginConfig = {
|
|
23
24
|
clientID: opts.clientId,
|
|
24
25
|
// 从 git remote -v 动态识别平台域名,兼容不同部署(公开 / 内网 / 自定义域名)
|
|
25
|
-
platformURL: resolvePlatformUrl(),
|
|
26
|
+
platformURL: resolvePlatformUrl(opts.host),
|
|
26
27
|
debug: opts.debug,
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -79,7 +80,8 @@ export function registerLoginCommand(program: Command): void {
|
|
|
79
80
|
// 持久化 token
|
|
80
81
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
81
82
|
// 从 platformURL 推导 API 域名,例如 https://cnb.cool -> https://api.cnb.cool
|
|
82
|
-
|
|
83
|
+
// 使用 host(而非 hostname)以保留自定义端口,如 https://example.com:8080 -> https://api.example.com:8080
|
|
84
|
+
const platformHost = new URL(cfg.platformURL).host;
|
|
83
85
|
const loginHost = `https://api.${platformHost}`;
|
|
84
86
|
const store: TokenStore = {
|
|
85
87
|
access_token: tok.access_token,
|
|
@@ -90,22 +90,24 @@ export interface SkillsListOptions {
|
|
|
90
90
|
global?: boolean;
|
|
91
91
|
/** 仅列出项目 scope(对应 skills list -p) */
|
|
92
92
|
project?: boolean;
|
|
93
|
-
/** 按 agent
|
|
93
|
+
/** 按 agent 过滤:透传 -a 给底层 skills 命令(值会归一化为小写 key) */
|
|
94
94
|
agent?: string;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* 运行原始 `skills list [scope] --json` 并返回解析后的 JSON 数组。
|
|
98
|
+
* 运行原始 `skills list [scope] [--agent] --json` 并返回解析后的 JSON 数组。
|
|
99
99
|
*
|
|
100
|
-
*
|
|
100
|
+
* 参数规则与 `skills list` 一致:
|
|
101
101
|
* - 指定 `-g` → `-g`
|
|
102
102
|
* - 指定 `-p` → `-p`
|
|
103
103
|
* - 都不指定 → 不传 scope(由 skills 默认:项目优先,否则全局)
|
|
104
|
+
* - 指定 `-a/--agent` → 归一化为小写后透传(底层按 agent 目录 key 匹配)
|
|
104
105
|
*/
|
|
105
106
|
export function runSkillsListJson(opts: SkillsListOptions): SkillEntry[] {
|
|
106
107
|
const args = ['list'];
|
|
107
108
|
if (opts.global) args.push('-g');
|
|
108
109
|
if (opts.project) args.push('-p');
|
|
110
|
+
if (opts.agent) args.push('-a', opts.agent.toLowerCase());
|
|
109
111
|
args.push('--json');
|
|
110
112
|
|
|
111
113
|
let stdout: string;
|
|
@@ -132,24 +134,17 @@ export function runSkillsListJson(opts: SkillsListOptions): SkillEntry[] {
|
|
|
132
134
|
|
|
133
135
|
/**
|
|
134
136
|
* 对 `skills list --json` 输出做后处理:
|
|
135
|
-
*
|
|
136
|
-
* 2. 按 agent 过滤(若指定 -a)。
|
|
137
|
+
* 仅从 SKILL.md frontmatter 回填缺失的 description。
|
|
137
138
|
*
|
|
139
|
+
* agent 过滤已下沉到底层 `skills list -a`(大小写归一化后透传),此处不再过滤。
|
|
138
140
|
* 返回的是全新数组/对象,不修改原始输入。
|
|
139
141
|
*/
|
|
140
142
|
export function decorateSkillEntries(
|
|
141
143
|
entries: SkillEntry[],
|
|
142
|
-
opts: SkillsListOptions,
|
|
143
144
|
): SkillEntry[] {
|
|
144
145
|
const result: SkillEntry[] = [];
|
|
145
146
|
|
|
146
147
|
for (const entry of entries) {
|
|
147
|
-
// agent 过滤:保留 agents 中包含指定 agent 的条目
|
|
148
|
-
if (opts.agent) {
|
|
149
|
-
const agents = Array.isArray(entry.agents) ? entry.agents : [];
|
|
150
|
-
if (!agents.includes(opts.agent)) continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
148
|
const decorated: SkillEntry = { ...entry };
|
|
154
149
|
|
|
155
150
|
// 回填 description:缺失或为空时从 path 下的 SKILL.md frontmatter 解析
|
|
@@ -171,7 +166,7 @@ export function decorateSkillEntries(
|
|
|
171
166
|
*/
|
|
172
167
|
export function skillsList(opts: SkillsListOptions & { json?: boolean }): string {
|
|
173
168
|
const raw = runSkillsListJson(opts);
|
|
174
|
-
const decorated = decorateSkillEntries(raw
|
|
169
|
+
const decorated = decorateSkillEntries(raw);
|
|
175
170
|
|
|
176
171
|
// 与 `skills list --json` 保持一致的 JSON 输出(默认即 JSON,字段结构不变)
|
|
177
172
|
return JSON.stringify(decorated, null, 2);
|
|
@@ -52,13 +52,77 @@ function parseUrlHost(url: string): string | null {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 将 --host 传入的裸主机名规范化为平台 base URL。
|
|
57
|
+
* --host 仅接受裸域名(如 cnb.cool、example.com:8080),不兼容 https:// 这类协议头;
|
|
58
|
+
* 含协议头的输入视为非法,返回 null(交由后续优先级处理)。
|
|
59
|
+
* 内部统一补 https://,并去掉常见的 api. 前缀。
|
|
60
|
+
* @param raw 裸主机名,如 cnb.cool、example.com:8080、api.cnb.cool
|
|
61
|
+
* @returns 形如 https://cnb.cool,失败返回 null
|
|
62
|
+
*/
|
|
63
|
+
function normalizeHost(raw: string): string | null {
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
if (!trimmed) return null;
|
|
66
|
+
// 含协议头(如 https://)视为非法 --host 输入
|
|
67
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return null;
|
|
68
|
+
// 校验为合法主机名(可带端口),拒绝路径、空格等非法字符;
|
|
69
|
+
// 否则后续 new URL(`https://${host}`) 可能抛 Invalid URL,或产生带路径的异常平台地址,
|
|
70
|
+
// 导致 login_host 与 platformURL 脱节。
|
|
71
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:\d{1,5})?$/i.test(trimmed)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// 主机名大小写不敏感,统一转小写;去掉常见的 api. 前缀(api.cnb.cool -> cnb.cool)
|
|
75
|
+
// 使用 host 形式以保留自定义端口,如 example.com:8080
|
|
76
|
+
const host = trimmed.toLowerCase().replace(/^api\./, '');
|
|
77
|
+
return `https://${host}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 将 CNB_API_ENDPOINT(完整 API URL,如 https://api.cnb.cool)规范化为平台 base URL。
|
|
82
|
+
* 与 --host 不同,这里是完整 URL,需要按 URL 解析,并去掉常见的 api. 前缀、保留自定义端口。
|
|
83
|
+
* @param raw 完整 URL,如 https://api.cnb.cool、https://api.example.com:8443
|
|
84
|
+
* @returns 形如 https://cnb.cool,失败返回 null
|
|
85
|
+
*/
|
|
86
|
+
function normalizeEndpoint(raw: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
const u = new URL(raw.trim());
|
|
89
|
+
if (!u.host) return null;
|
|
90
|
+
// 去掉常见的 api. 前缀,统一得到平台域名(api.cnb.cool -> cnb.cool)
|
|
91
|
+
// 使用 host(而非 hostname)以保留自定义端口,如 example.com:8080
|
|
92
|
+
const host = u.host.replace(/^api\./, '');
|
|
93
|
+
return `https://${host}`;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
55
99
|
/**
|
|
56
100
|
* 解析当前平台的 base URL(形如 https://cnb.cool)。
|
|
57
|
-
*
|
|
58
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* 优先级(从高到低):
|
|
103
|
+
* 1. 显式传入的参数(login 的 --host,裸域名)
|
|
104
|
+
* 2. 当前仓库 git remote -v 解析出的 host
|
|
105
|
+
* 3. 环境变量 CNB_API_ENDPOINT(完整 URL)
|
|
106
|
+
* 4. 兜底 https://cnb.cool
|
|
107
|
+
*
|
|
108
|
+
* @param paramUrl 可选的显式 --host 裸主机名
|
|
59
109
|
* @returns 平台 base URL
|
|
60
110
|
*/
|
|
61
|
-
export function resolvePlatformUrl(): string {
|
|
111
|
+
export function resolvePlatformUrl(paramUrl?: string): string {
|
|
112
|
+
// 1. 显式参数(--host 裸主机名)优先
|
|
113
|
+
const fromParam = paramUrl?.trim() ? normalizeHost(paramUrl.trim()) : null;
|
|
114
|
+
if (fromParam) return fromParam;
|
|
115
|
+
|
|
116
|
+
// 2. 无参数时从 git remote 取
|
|
62
117
|
const host = getGitRemoteHosts()[0];
|
|
63
|
-
|
|
118
|
+
if (host) return `https://${host}`;
|
|
119
|
+
|
|
120
|
+
// 3. 都没有则尝试从 CNB_API_ENDPOINT(完整 URL)取
|
|
121
|
+
const fromEnv = process.env.CNB_API_ENDPOINT?.trim()
|
|
122
|
+
? normalizeEndpoint(process.env.CNB_API_ENDPOINT.trim())
|
|
123
|
+
: null;
|
|
124
|
+
if (fromEnv) return fromEnv;
|
|
125
|
+
|
|
126
|
+
// 4. 最后降级到 cnb.cool
|
|
127
|
+
return 'https://cnb.cool';
|
|
64
128
|
}
|