@cdo-ai/cli 0.1.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/bin/cdo.mjs ADDED
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ import { login } from "../lib/login.mjs";
3
+ import { openBrowser } from "../lib/browser.mjs";
4
+ import {
5
+ apiOriginForEnvironment,
6
+ clearProjectContext,
7
+ ensureConfigV2,
8
+ findProfile,
9
+ listProfiles,
10
+ loadConfig,
11
+ resolveContext,
12
+ saveProjectContext,
13
+ setDefaultProfile,
14
+ validateEnterpriseCode,
15
+ } from "../lib/config.mjs";
16
+ import { callApi } from "../lib/api.mjs";
17
+ import { diagnose } from "../lib/doctor.mjs";
18
+ import { installOrUpdateSkill } from "../lib/skills.mjs";
19
+ import { gitAuth } from "../lib/git-auth.mjs";
20
+ import { VERSION } from "../lib/version.mjs";
21
+
22
+ const HELP = `用法:cdo <命令> [选项]
23
+ 命令:
24
+ login --env <dev|staging|prod> --enterprise <企业码> [--set-default] [--api-url <url>]
25
+ auth list --json | use <profile> | status|key [--profile <profile>] [--env <环境>]
26
+ context use <profile> | clear | show
27
+ doctor --agent <codex|claude> [--json] [--profile <profile>] [--env <环境>]
28
+ api <METHOD> <相对路径> [--profile <profile>] [--env <环境>]
29
+ git auth [--json] [--profile <profile>] [--env <环境>]
30
+ skills install|update cdo-sys-local --agent <codex|claude> [--source-env <环境>]
31
+ `;
32
+
33
+ function optionValue(args, name) {
34
+ const index = args.indexOf(name);
35
+ if (index === -1) return null;
36
+ const value = args[index + 1];
37
+ if (!value || value.startsWith("--")) throw new Error(`${name} 缺少值`);
38
+ return value;
39
+ }
40
+
41
+ function hasOption(args, name) {
42
+ return args.includes(name);
43
+ }
44
+
45
+ function validateOptions(args, { values = [], flags = [], positional = 0 } = {}) {
46
+ const valueNames = new Set(values);
47
+ const flagNames = new Set(flags);
48
+ const seen = new Set();
49
+ const positionals = [];
50
+ for (let index = 0; index < args.length; index += 1) {
51
+ const argument = args[index];
52
+ if (valueNames.has(argument)) {
53
+ if (seen.has(argument)) throw new Error(`重复选项:${argument}`);
54
+ seen.add(argument);
55
+ const value = args[index + 1];
56
+ if (!value || value.startsWith("--")) throw new Error(`${argument} 缺少值`);
57
+ index += 1;
58
+ } else if (flagNames.has(argument)) {
59
+ if (seen.has(argument)) throw new Error(`重复选项:${argument}`);
60
+ seen.add(argument);
61
+ } else if (argument.startsWith("-")) {
62
+ throw new Error(`未知选项:${argument}`);
63
+ } else {
64
+ positionals.push(argument);
65
+ }
66
+ }
67
+ if (positionals.length !== positional) throw new Error("命令参数数量无效");
68
+ }
69
+
70
+ function validateCommandLine(args) {
71
+ const [top, second, third] = args;
72
+ const identityValues = ["--profile", "--env"];
73
+ if (top === "login") {
74
+ return validateOptions(args.slice(1), {
75
+ values: ["--env", "--enterprise", "--api-url"], flags: ["--set-default"],
76
+ });
77
+ }
78
+ if (top === "auth" && second === "list") {
79
+ return validateOptions(args.slice(2), { flags: ["--json"] });
80
+ }
81
+ if (top === "auth" && second === "use") return validateOptions(args.slice(2), { positional: 1 });
82
+ if (top === "auth" && ["status", "key"].includes(second)) {
83
+ return validateOptions(args.slice(2), { values: identityValues });
84
+ }
85
+ if (top === "context" && second === "use") return validateOptions(args.slice(2), { positional: 1 });
86
+ if (top === "context" && second === "clear") return validateOptions(args.slice(2));
87
+ if (top === "context" && second === "show") {
88
+ return validateOptions(args.slice(2), { values: identityValues });
89
+ }
90
+ if (top === "doctor") {
91
+ return validateOptions(args.slice(1), { values: [...identityValues, "--agent"], flags: ["--json"] });
92
+ }
93
+ if (top === "api") {
94
+ if (!second || !third || second.startsWith("-") || third.startsWith("-")) {
95
+ throw new Error("用法:cdo api <METHOD> <相对路径>");
96
+ }
97
+ return validateOptions(args.slice(3), {
98
+ values: [...identityValues, "--data", "--data-file", "--output"],
99
+ });
100
+ }
101
+ if (top === "git" && second === "auth") {
102
+ return validateOptions(args.slice(2), { values: identityValues, flags: ["--json"] });
103
+ }
104
+ if (top === "skills" && ["install", "update"].includes(second) && third === "cdo-sys-local") {
105
+ return validateOptions(args.slice(3), { values: ["--agent", "--source-env"] });
106
+ }
107
+ throw new Error(`未知或当前阶段不可用的命令:${[top, second].filter(Boolean).join(" ")}`);
108
+ }
109
+
110
+ const rawArgs = process.argv.slice(2);
111
+ const [command, subcommand] = rawArgs;
112
+
113
+ async function main() {
114
+ if (rawArgs.length === 1 && ["--version", "-V"].includes(command)) {
115
+ process.stdout.write(`${VERSION}\n`);
116
+ return;
117
+ }
118
+ if (!command || rawArgs.includes("--help") || rawArgs.includes("-h")) {
119
+ process.stdout.write(HELP);
120
+ return;
121
+ }
122
+ validateCommandLine(rawArgs);
123
+ const requestedEnvironment = optionValue(rawArgs, "--env");
124
+ const requestedProfile = optionValue(rawArgs, "--profile");
125
+ // System Skill repair is host/source scoped and must remain usable when identity config is unreadable.
126
+ if (command === "skills" && ["install", "update"].includes(subcommand)) {
127
+ if (rawArgs[2] !== "cdo-sys-local") throw new Error("只支持平台系统 Skill:cdo-sys-local");
128
+ const agent = optionValue(rawArgs, "--agent");
129
+ if (!agent) throw new Error("系统 Skill 安装更新必须显式提供 --agent");
130
+ const sourceEnvironment = optionValue(rawArgs, "--source-env");
131
+ const result = await installOrUpdateSkill({
132
+ action: subcommand, agent,
133
+ sourceEnvironment: sourceEnvironment || (subcommand === "install" ? "prod" : null),
134
+ sourceApiBaseUrl: sourceEnvironment || subcommand === "install"
135
+ ? apiOriginForEnvironment(sourceEnvironment || "prod") : null,
136
+ });
137
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
138
+ return;
139
+ }
140
+ await ensureConfigV2();
141
+ if (command === "login") {
142
+ const enterprise = optionValue(rawArgs, "--enterprise");
143
+ if (!enterprise) throw new Error("cdo login 必须显式提供 --enterprise");
144
+ await login({
145
+ environmentName: requestedEnvironment || process.env.CDO_ENV || "prod",
146
+ enterpriseCode: validateEnterpriseCode(enterprise),
147
+ apiBaseUrl: optionValue(rawArgs, "--api-url"),
148
+ setDefault: hasOption(rawArgs, "--set-default"),
149
+ openBrowser,
150
+ });
151
+ return;
152
+ }
153
+ if (command === "auth" && subcommand === "list") {
154
+ const config = await loadConfig();
155
+ const profiles = listProfiles(config).map(({ personal_api_key: _secret, ...profile }) => ({
156
+ ...profile,
157
+ default: profile.id === config.default_profile_id,
158
+ key_available: Boolean(_secret),
159
+ }));
160
+ process.stdout.write(`${JSON.stringify(profiles, null, 2)}\n`);
161
+ return;
162
+ }
163
+ if (command === "auth" && subcommand === "use") {
164
+ const selector = rawArgs[2];
165
+ if (!selector || selector.startsWith("--")) throw new Error("用法:cdo auth use <profile>");
166
+ const profile = await setDefaultProfile(selector);
167
+ process.stdout.write(`用户默认身份:${profile.alias} (${profile.id})\n`);
168
+ try {
169
+ const effective = await resolveContext({ requireKey: false });
170
+ process.stdout.write(
171
+ `当前有效身份:${effective.profile.alias || effective.profileId || "直接配置"}`
172
+ + `${effective.profileId ? ` (${effective.profileId})` : ""} | 来源:${effective.selectionSource}\n`,
173
+ );
174
+ } catch (error) {
175
+ process.stderr.write(`警告:用户默认身份已更新,但当前有效身份无法解析:${error.message || error}\n`);
176
+ }
177
+ return;
178
+ }
179
+ if (command === "auth" && subcommand === "key") {
180
+ const context = await resolveContext({ requestedEnvironment, requestedProfile });
181
+ process.stdout.write(`${context.profile.personal_api_key}\n`);
182
+ return;
183
+ }
184
+ if (command === "auth" && subcommand === "status") {
185
+ const context = await resolveContext({ requestedEnvironment, requestedProfile, requireKey: false });
186
+ const data = {
187
+ environment: context.environmentName,
188
+ api_base_url: context.profile.api_base_url || null,
189
+ enterprise: context.profile.enterprise || null,
190
+ user: context.profile.user || null,
191
+ key_available: Boolean(context.profile.personal_api_key),
192
+ source: context.profile.source,
193
+ profile_id: context.profileId || null,
194
+ profile_alias: context.profile.alias || null,
195
+ user_default_profile_id: context.userDefaultProfileId,
196
+ selection_source: context.selectionSource,
197
+ };
198
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
199
+ return;
200
+ }
201
+ if (command === "context" && subcommand === "use") {
202
+ const selector = rawArgs[2];
203
+ if (!selector || selector.startsWith("--")) throw new Error("用法:cdo context use <profile>");
204
+ const config = await loadConfig();
205
+ const profile = findProfile(config, selector);
206
+ await saveProjectContext(profile.id);
207
+ process.stdout.write(`当前目录默认身份:${profile.alias} (${profile.id})\n`);
208
+ return;
209
+ }
210
+ if (command === "context" && subcommand === "clear") {
211
+ await clearProjectContext();
212
+ process.stdout.write("已清除当前目录默认身份\n");
213
+ return;
214
+ }
215
+ if (command === "context" && subcommand === "show") {
216
+ const context = await resolveContext({ requestedEnvironment, requestedProfile, requireKey: false });
217
+ process.stdout.write(`${JSON.stringify({
218
+ profile_id: context.profileId || null,
219
+ profile_alias: context.profile.alias || null,
220
+ environment: context.environmentName,
221
+ selection_source: context.selectionSource,
222
+ user_default_profile_id: context.userDefaultProfileId,
223
+ project_profile_id: context.projectProfileId || null,
224
+ }, null, 2)}\n`);
225
+ return;
226
+ }
227
+ if (command === "doctor") {
228
+ const agent = optionValue(rawArgs, "--agent");
229
+ if (!agent) throw new Error("cdo doctor 必须显式提供 --agent");
230
+ if (!["codex", "claude"].includes(agent)) throw new Error("--agent 必须是 codex 或 claude");
231
+ const context = await resolveContext({ requestedEnvironment, requestedProfile, requireKey: false });
232
+ const result = await diagnose({ context, agent });
233
+ if (hasOption(rawArgs, "--json")) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
234
+ else {
235
+ process.stdout.write([
236
+ `环境:${result.environment}`,
237
+ `CDO API:${result.api.reachable ? result.api.status : "不可达"}`,
238
+ `用户 Key:${result.key_available ? "已配置" : "未配置"}`,
239
+ `身份:${result.api.authenticated ? "服务端已验证" : "未验证"}`,
240
+ `系统 Skill:${result.system_skill.status}`,
241
+ `企业 Git:${result.git_auth.status}`,
242
+ `Git:${result.git.available ? result.git.version : "未安装"}`,
243
+ `Tea:${result.tea.available ? result.tea.version : "未安装"}`,
244
+ ].join("\n") + "\n");
245
+ }
246
+ return;
247
+ }
248
+ if (command === "api") {
249
+ if (!subcommand || !rawArgs[2] || subcommand.startsWith("--") || rawArgs[2].startsWith("--")) {
250
+ throw new Error("用法:cdo api <METHOD> <相对路径>");
251
+ }
252
+ const context = await resolveContext({ requestedEnvironment, requestedProfile });
253
+ await callApi({
254
+ profile: context.profile,
255
+ method: subcommand,
256
+ relativePath: rawArgs[2],
257
+ data: optionValue(rawArgs, "--data"),
258
+ dataFile: optionValue(rawArgs, "--data-file"),
259
+ outputFile: optionValue(rawArgs, "--output"),
260
+ });
261
+ return;
262
+ }
263
+ if (command === "git" && subcommand === "auth") {
264
+ const context = await resolveContext({ requestedEnvironment, requestedProfile });
265
+ const result = await gitAuth({ context, openBrowser });
266
+ process.stdout.write(hasOption(rawArgs, "--json")
267
+ ? `${JSON.stringify(result, null, 2)}\n`
268
+ : `企业 Git 认证完成:${result.enterprise_name}\nTea 配置:${result.tea_login}\n`);
269
+ return;
270
+ }
271
+ throw new Error(`未知或当前阶段不可用的命令:${[command, subcommand].filter(Boolean).join(" ")}`);
272
+ }
273
+
274
+ main().catch((error) => {
275
+ process.stderr.write(`${error.message || error}\n`);
276
+ process.exitCode = 1;
277
+ });
package/lib/api.mjs ADDED
@@ -0,0 +1,64 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+
3
+ import { VERSION } from "./version.mjs";
4
+
5
+ function errorMessage(response, body) {
6
+ return body?.error?.message || body?.message || `CDO API request failed (${response.status})`;
7
+ }
8
+
9
+ export function resolveApiUrl(apiBaseUrl, relativePath) {
10
+ if (typeof relativePath !== "string" || !relativePath.startsWith("/") || relativePath.startsWith("//")) {
11
+ throw new Error("API 路径必须是以 / 开头的相对路径");
12
+ }
13
+ const base = new URL(apiBaseUrl);
14
+ const url = new URL(relativePath, base);
15
+ if (url.origin !== base.origin) throw new Error("API 路径不得指向其他服务");
16
+ return url;
17
+ }
18
+
19
+ export async function callApi({
20
+ profile,
21
+ method,
22
+ relativePath,
23
+ data = null,
24
+ dataFile = null,
25
+ outputFile = null,
26
+ fetchImplementation = fetch,
27
+ stdout = process.stdout,
28
+ }) {
29
+ if (data !== null && dataFile !== null) throw new Error("--data 与 --data-file 不能同时使用");
30
+ const normalizedMethod = String(method || "").toUpperCase();
31
+ if (!/^[A-Z]+$/.test(normalizedMethod)) throw new Error("METHOD 无效");
32
+ const headers = { Authorization: `ApiKey ${profile.personal_api_key}`, Accept: "application/json", "X-CDO-Client": "cdo-cli", "X-CDO-Version": VERSION };
33
+ let body;
34
+ if (data !== null || dataFile !== null) {
35
+ const source = dataFile !== null ? await readFile(dataFile, "utf8") : data;
36
+ try {
37
+ body = JSON.stringify(JSON.parse(source));
38
+ } catch {
39
+ throw new Error("请求数据必须是有效 JSON");
40
+ }
41
+ headers["Content-Type"] = "application/json";
42
+ }
43
+ const response = await fetchImplementation(resolveApiUrl(profile.api_base_url, relativePath), {
44
+ method: normalizedMethod,
45
+ headers,
46
+ body,
47
+ redirect: "error",
48
+ signal: AbortSignal.timeout(30_000),
49
+ });
50
+ const bytes = Buffer.from(await response.arrayBuffer());
51
+ let parsed = null;
52
+ if ((response.headers.get("content-type") || "").includes("json")) {
53
+ try { parsed = JSON.parse(bytes.toString("utf8")); } catch { /* report the raw invalid response */ }
54
+ }
55
+ if (!response.ok) throw new Error(errorMessage(response, parsed));
56
+ if (outputFile) {
57
+ await writeFile(outputFile, bytes);
58
+ } else if (parsed !== null) {
59
+ stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
60
+ } else {
61
+ stdout.write(bytes);
62
+ }
63
+ return { status: response.status, bytes: bytes.length };
64
+ }
@@ -0,0 +1,14 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ /** Build an OS URL opener command without routing an untrusted URL through a shell. */
4
+ export function browserCommand(url, platform = process.platform) {
5
+ if (platform === "darwin") return { program: "open", args: [url] };
6
+ if (platform === "win32") return { program: "explorer.exe", args: [url] };
7
+ return { program: "xdg-open", args: [url] };
8
+ }
9
+
10
+ /** Open a URL in the user's browser while preserving it as one literal process argument. */
11
+ export function openBrowser(url, platform = process.platform, spawnImplementation = spawn) {
12
+ const command = browserCommand(url, platform);
13
+ spawnImplementation(command.program, command.args, { detached: true, shell: false, stdio: "ignore" }).unref();
14
+ }