@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.
@@ -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
- const platformHost = new URL(cfg.platformURL).hostname;
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 过滤:仅保留 agents 中包含指定 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
- * scope 参数规则与 `skills list` 一致:
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
- * 1. SKILL.md frontmatter 回填缺失的 description
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, opts);
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
- * 优先取 git remote 中的 host 拼成 https://{host};
58
- * 无 remote 或解析失败时兜底返回 https://cnb.cool。
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
- return host ? `https://${host}` : 'https://cnb.cool';
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.14.1",
3
+ "version": "2.15.0",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",