@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/lib/config.mjs ADDED
@@ -0,0 +1,392 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { lstat, mkdir, open, readFile, realpath, rename, stat, unlink, writeFile, chmod } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, join, relative, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+
8
+ export const ENVIRONMENTS = Object.freeze(["dev", "staging", "prod"]);
9
+ const ROOT_DOMAINS = Object.freeze({ dev: "dev-agents.cdo.top", staging: "s-agents.cdo.top", prod: "agents.ttdd.work" });
10
+ const OFFICIAL_API_ORIGINS = Object.freeze({ dev: "https://dev-api.cdo.top", staging: "https://s-api.cdo.top", prod: "https://api.ttdd.work" });
11
+ const LOCK_TIMEOUT_MS = 5_000;
12
+ const LOCK_STALE_MS = 30_000;
13
+ const executeFile = promisify(execFile);
14
+
15
+ export function validateEnvironment(value, source = "--env") {
16
+ if (!ENVIRONMENTS.includes(value)) throw new Error(`${source} 必须是 dev、staging 或 prod`);
17
+ return value;
18
+ }
19
+
20
+ export function validateEnterpriseCode(value) {
21
+ if (typeof value !== "string" || value.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value)) {
22
+ throw new Error("--enterprise 必须是有效企业码(小写字母、数字和中间短横线)");
23
+ }
24
+ return value;
25
+ }
26
+
27
+ export function endpointForEnvironment(environmentName, enterpriseCode) {
28
+ return `https://${validateEnterpriseCode(enterpriseCode)}.${ROOT_DOMAINS[validateEnvironment(environmentName)]}`;
29
+ }
30
+
31
+ export function apiOriginForEnvironment(environmentName) {
32
+ return OFFICIAL_API_ORIGINS[validateEnvironment(environmentName)];
33
+ }
34
+
35
+ export function validateEndpointEnvironment(value, environmentName, source = "CDO API 地址") {
36
+ const selected = validateEnvironment(environmentName);
37
+ let endpoint;
38
+ try { endpoint = new URL(value); } catch { throw new Error(`${source} 无效`); }
39
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password
40
+ || endpoint.pathname !== "/" || endpoint.search || endpoint.hash) {
41
+ throw new Error(`${source} 必须是无凭据的 HTTP(S) origin`);
42
+ }
43
+ const hostname = endpoint.hostname.toLowerCase();
44
+ const loopback = ["localhost", "127.0.0.1", "[::1]", "::1"].includes(hostname);
45
+ const enterpriseDomainMatches = selected === "dev"
46
+ ? loopback || hostname.endsWith(".dev-agents.cdo.top")
47
+ : selected === "staging"
48
+ ? hostname.endsWith(".s-agents.cdo.top") || hostname.endsWith(".s-agents.ttdd.work")
49
+ : hostname.endsWith(".agents.ttdd.work") && !hostname.endsWith(".s-agents.ttdd.work");
50
+ if (!(enterpriseDomainMatches || endpoint.origin === OFFICIAL_API_ORIGINS[selected])) {
51
+ throw new Error(`${source}与所选 ${selected} 环境不匹配`);
52
+ }
53
+ if (!loopback && endpoint.protocol !== "https:") throw new Error(`${source}必须使用 HTTPS`);
54
+ return endpoint;
55
+ }
56
+
57
+ export function inferEndpointEnvironment(value, source = "CDO API 地址") {
58
+ const matches = ENVIRONMENTS.filter((name) => {
59
+ try { validateEndpointEnvironment(value, name, source); return true; } catch { return false; }
60
+ });
61
+ if (matches.length !== 1) throw new Error(`${source}无法唯一确定部署环境`);
62
+ return matches[0];
63
+ }
64
+
65
+ export function stableProfileId(environmentName, enterpriseId, userId) {
66
+ if (![environmentName, enterpriseId, userId].every((value) => typeof value === "string" && value)) {
67
+ throw new Error("profile 身份缺少部署、企业或员工 ID");
68
+ }
69
+ const digest = createHash("sha256").update(`${environmentName}\0${enterpriseId}\0${userId}`).digest("hex");
70
+ return `p_${digest.slice(0, 20)}`;
71
+ }
72
+
73
+ function defaultAlias(profile) {
74
+ return `${profile.environment}/${profile.enterprise?.abbr || profile.enterprise?.id || "unknown"}`;
75
+ }
76
+
77
+ export function configPath(environment = process.env) {
78
+ return environment.CDO_CONFIG_PATH || join(homedir(), ".config", "cdo", "config.json");
79
+ }
80
+
81
+ function emptyConfig() { return { schema_version: 2, profiles: {}, default_profile_id: null }; }
82
+
83
+ function uniqueAlias(profiles, requested, profileId, userId) {
84
+ const occupied = new Set(Object.values(profiles).filter((item) => item.id !== profileId).map((item) => item.alias));
85
+ if (!occupied.has(requested)) return requested;
86
+ const suffix = createHash("sha256").update(userId || profileId).digest("hex").slice(0, 6);
87
+ const candidate = `${requested}/${suffix}`;
88
+ if (occupied.has(candidate)) throw new Error(`profile 别名冲突:${candidate}`);
89
+ return candidate;
90
+ }
91
+
92
+ function normalizedProfile(id, value) {
93
+ if (!value || typeof value !== "object" || !ENVIRONMENTS.includes(value.environment)) return null;
94
+ return { ...value, id, alias: value.alias || defaultAlias(value) };
95
+ }
96
+
97
+ export function normalizeConfig(raw) {
98
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("CDO 配置格式无效,原文件未修改");
99
+ if (raw.schema_version === 2) {
100
+ if (!raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)
101
+ || !([null, undefined].includes(raw.default_profile_id) || typeof raw.default_profile_id === "string")) {
102
+ throw new Error("CDO v2 配置结构不完整,原文件未修改");
103
+ }
104
+ const profiles = {};
105
+ for (const [key, value] of Object.entries(raw.profiles)) {
106
+ if (value?.id && value.id !== key) throw new Error(`CDO v2 profile ID 不一致:${key}`);
107
+ const profile = normalizedProfile(key, value);
108
+ if (!profile?.id) throw new Error(`CDO v2 profile 无效:${key}`);
109
+ profiles[profile.id] = profile;
110
+ }
111
+ return { schema_version: 2, profiles, default_profile_id: raw.default_profile_id || null };
112
+ }
113
+ if (raw.schema_version !== 1) throw new Error(`不支持的 CDO 配置版本:${String(raw.schema_version)}`);
114
+ if (!raw.environments || typeof raw.environments !== "object" || Array.isArray(raw.environments)) {
115
+ throw new Error("CDO v1 配置结构不完整,原文件未修改");
116
+ }
117
+ const profiles = {};
118
+ for (const [environment, value] of Object.entries(raw.environments || {})) {
119
+ if (!ENVIRONMENTS.includes(environment) || !value || typeof value !== "object") continue;
120
+ let id;
121
+ try { id = stableProfileId(environment, value.enterprise?.id, value.user?.id); }
122
+ catch { id = `legacy_${createHash("sha256").update(environment).digest("hex").slice(0, 12)}`; }
123
+ const profile = { ...value, id, environment, alias: value.alias || defaultAlias({ ...value, environment }) };
124
+ if (!value.enterprise?.id || !value.user?.id) profile.migration_status = "identity_incomplete";
125
+ profiles[id] = profile;
126
+ }
127
+ const ids = Object.keys(profiles);
128
+ return { schema_version: 2, profiles, default_profile_id: ids.length === 1 ? ids[0] : null };
129
+ }
130
+
131
+ export async function loadConfig(environment = process.env) {
132
+ try { return normalizeConfig(JSON.parse(await readFile(configPath(environment), "utf8"))); }
133
+ catch (error) { if (error?.code === "ENOENT") return emptyConfig(); throw error; }
134
+ }
135
+
136
+ export async function ensureConfigV2(environment = process.env) {
137
+ try {
138
+ const raw = JSON.parse(await readFile(configPath(environment), "utf8"));
139
+ normalizeConfig(raw);
140
+ if (raw.schema_version === 2) return false;
141
+ } catch (error) {
142
+ if (error?.code === "ENOENT") return false;
143
+ throw error;
144
+ }
145
+ await updateConfig((config) => ({ config, value: true }), environment);
146
+ return true;
147
+ }
148
+
149
+ async function rejectSymlink(path) {
150
+ try { if ((await lstat(path)).isSymbolicLink()) throw new Error(`拒绝写入符号链接配置:${path}`); }
151
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
152
+ }
153
+
154
+ async function writeConfigAtomic(value, environment) {
155
+ const path = configPath(environment);
156
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
157
+ await rejectSymlink(path);
158
+ const temporary = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
159
+ const handle = await open(temporary, "wx", 0o600);
160
+ try { await handle.writeFile(`${JSON.stringify(normalizeConfig(value), null, 2)}\n`, "utf8"); await handle.sync(); }
161
+ finally { await handle.close(); }
162
+ try { await rename(temporary, path); await chmod(path, 0o600); }
163
+ catch (error) { await unlink(temporary).catch(() => {}); throw error; }
164
+ }
165
+
166
+ async function withConfigLock(environment, operation) {
167
+ const path = configPath(environment);
168
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
169
+ const lockPath = `${path}.lock`;
170
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
171
+ let handle;
172
+ while (!handle) {
173
+ try { handle = await open(lockPath, "wx", 0o600); await handle.writeFile(`${process.pid}\n`, "utf8"); }
174
+ catch (error) {
175
+ if (error?.code !== "EEXIST") throw error;
176
+ const age = await stat(lockPath).then((info) => Date.now() - info.mtimeMs).catch(() => 0);
177
+ if (age > LOCK_STALE_MS) { await unlink(lockPath).catch(() => {}); continue; }
178
+ if (Date.now() >= deadline) throw new Error("CDO 配置正被其他进程更新,请稍后重试");
179
+ await new Promise((done) => setTimeout(done, 20));
180
+ }
181
+ }
182
+ try { return await operation(); }
183
+ finally { await handle.close().catch(() => {}); await unlink(lockPath).catch(() => {}); }
184
+ }
185
+
186
+ export async function saveConfig(value, environment = process.env) {
187
+ return withConfigLock(environment, () => writeConfigAtomic(value, environment));
188
+ }
189
+
190
+ export async function updateConfig(updater, environment = process.env) {
191
+ return withConfigLock(environment, async () => {
192
+ const current = await loadConfig(environment);
193
+ const result = await updater(current);
194
+ await writeConfigAtomic(result?.config || current, environment);
195
+ return result?.value;
196
+ });
197
+ }
198
+
199
+ export async function upsertProfile(profile, { setDefault = false, environment = process.env } = {}) {
200
+ const id = stableProfileId(profile.environment, profile.enterprise?.id, profile.user?.id);
201
+ return updateConfig((config) => {
202
+ const previous = config.profiles[id] || {};
203
+ const alias = uniqueAlias(config.profiles, previous.alias || profile.alias || defaultAlias(profile), id, profile.user.id);
204
+ config.profiles[id] = { ...previous, ...profile, id, alias };
205
+ if (!config.default_profile_id || setDefault) config.default_profile_id = id;
206
+ return { config, value: config.profiles[id] };
207
+ }, environment);
208
+ }
209
+
210
+ export async function setDefaultProfile(selector, environment = process.env) {
211
+ return updateConfig((config) => {
212
+ const profile = findProfile(config, selector);
213
+ config.default_profile_id = profile.id;
214
+ return { config, value: profile };
215
+ }, environment);
216
+ }
217
+
218
+ export function listProfiles(config) {
219
+ return Object.values(config.profiles).sort((left, right) => left.alias.localeCompare(right.alias));
220
+ }
221
+
222
+ export function findProfile(config, selector) {
223
+ if (!selector) throw new Error("profile 不能为空");
224
+ if (config.profiles[selector]) return config.profiles[selector];
225
+ const matches = listProfiles(config).filter((profile) => profile.alias === selector);
226
+ if (matches.length === 1) return matches[0];
227
+ if (matches.length > 1) throw new Error(`profile 别名不唯一:${selector}`);
228
+ throw new Error(`profile 不存在:${selector}`);
229
+ }
230
+
231
+ export function projectContextPath(cwd = process.cwd(), environment = process.env) {
232
+ return environment.CDO_CONTEXT_PATH || join(resolve(cwd), ".cdo", "context.local.json");
233
+ }
234
+
235
+ async function gitProjectMetadata(cwd) {
236
+ try {
237
+ const directory = resolve(cwd);
238
+ const root = (await executeFile("git", ["-C", directory, "rev-parse", "--show-toplevel"])).stdout.trim();
239
+ const exclude = (await executeFile("git", ["-C", directory, "rev-parse", "--git-path", "info/exclude"])).stdout.trim();
240
+ return { root: resolve(root), exclude: resolve(root, exclude) };
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ async function resolvedProjectContextPath(cwd, environment, { search = false } = {}) {
247
+ if (environment.CDO_CONTEXT_PATH) return environment.CDO_CONTEXT_PATH;
248
+ const directory = resolve(cwd);
249
+ const metadata = await gitProjectMetadata(directory);
250
+ if (!metadata) return projectContextPath(directory, environment);
251
+ if (!search) return projectContextPath(metadata.root, environment);
252
+ let candidate = directory;
253
+ while (true) {
254
+ const path = projectContextPath(candidate, environment);
255
+ try {
256
+ await lstat(path);
257
+ return path;
258
+ } catch (error) {
259
+ if (error?.code !== "ENOENT") throw error;
260
+ }
261
+ if (candidate === metadata.root) return projectContextPath(metadata.root, environment);
262
+ const parent = dirname(candidate);
263
+ if (parent === candidate) return projectContextPath(metadata.root, environment);
264
+ candidate = parent;
265
+ }
266
+ }
267
+
268
+ async function ensureProjectContextIgnored(metadata) {
269
+ if (!metadata) return;
270
+ const rule = "/.cdo/context.local.json";
271
+ let current = "";
272
+ try { current = await readFile(metadata.exclude, "utf8"); }
273
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
274
+ if (current.split(/\r?\n/).includes(rule)) return;
275
+ await mkdir(dirname(metadata.exclude), { recursive: true, mode: 0o700 });
276
+ await rejectSymlink(metadata.exclude);
277
+ const separator = current && !current.endsWith("\n") ? "\n" : "";
278
+ const temporary = `${metadata.exclude}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
279
+ await writeFile(temporary, `${current}${separator}${rule}\n`, { mode: 0o600, flag: "wx" });
280
+ try { await rename(temporary, metadata.exclude); }
281
+ catch (error) { await unlink(temporary).catch(() => {}); throw error; }
282
+ }
283
+
284
+ async function rejectTrackedProjectContext(cwd, path, environment) {
285
+ const metadata = await gitProjectMetadata(cwd);
286
+ if (!metadata) return;
287
+ // macOS may spell the same temporary/worktree path through /var and /private/var.
288
+ const canonicalRoot = await realpath(metadata.root);
289
+ const canonicalDirectory = await realpath(dirname(path)).catch((error) => {
290
+ if (error?.code === "ENOENT") return resolve(canonicalRoot, relative(metadata.root, dirname(path)));
291
+ throw error;
292
+ });
293
+ const repositoryPath = relative(canonicalRoot, join(canonicalDirectory, basename(path)));
294
+ if (!repositoryPath || repositoryPath.startsWith("..")) return;
295
+ try {
296
+ await executeFile("git", ["-C", metadata.root, "ls-files", "--error-unmatch", "--", repositoryPath]);
297
+ } catch (error) {
298
+ if (error?.code === 1) return;
299
+ throw error;
300
+ }
301
+ throw new Error(`项目 CDO context 已被 Git 跟踪,拒绝读取或覆盖:${repositoryPath}`);
302
+ }
303
+
304
+ export async function loadProjectContext(cwd = process.cwd(), environment = process.env) {
305
+ try {
306
+ const path = await resolvedProjectContextPath(cwd, environment, { search: true });
307
+ await rejectTrackedProjectContext(cwd, path, environment);
308
+ const data = JSON.parse(await readFile(path, "utf8"));
309
+ return typeof data?.profile_id === "string" ? { profile_id: data.profile_id } : null;
310
+ } catch (error) {
311
+ if (error?.code === "ENOENT") return null;
312
+ throw new Error(`项目 CDO context 无效:${error.message}`);
313
+ }
314
+ }
315
+
316
+ export async function saveProjectContext(profileId, cwd = process.cwd(), environment = process.env) {
317
+ const metadata = environment.CDO_CONTEXT_PATH ? null : await gitProjectMetadata(cwd);
318
+ const path = environment.CDO_CONTEXT_PATH
319
+ || projectContextPath(metadata?.root || cwd, environment);
320
+ await rejectTrackedProjectContext(cwd, path, environment);
321
+ await ensureProjectContextIgnored(metadata);
322
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
323
+ await rejectSymlink(path);
324
+ const temporary = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
325
+ await writeFile(temporary, `${JSON.stringify({ schema_version: 1, profile_id: profileId }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
326
+ try { await rename(temporary, path); await chmod(path, 0o600); }
327
+ catch (error) { await unlink(temporary).catch(() => {}); throw error; }
328
+ return path;
329
+ }
330
+
331
+ export async function clearProjectContext(cwd = process.cwd(), environment = process.env) {
332
+ const path = await resolvedProjectContextPath(cwd, environment, { search: true });
333
+ await rejectTrackedProjectContext(cwd, path, environment);
334
+ await unlink(path).catch((error) => { if (error?.code !== "ENOENT") throw error; });
335
+ }
336
+
337
+ function candidateError(candidates, environmentName) {
338
+ const labels = candidates.map((profile) => `${profile.id} (${profile.alias})`).join(", ");
339
+ return new Error(candidates.length
340
+ ? `存在多个${environmentName ? ` ${environmentName}` : ""}身份,请显式选择 profile:${labels}`
341
+ : `没有可用${environmentName ? ` ${environmentName}` : ""}身份,请先运行 cdo login`);
342
+ }
343
+
344
+ /** Resolve one immutable command context using the LCLI-9 precedence contract. */
345
+ export async function resolveContext({ requestedProfile = null, requestedEnvironment = null, environment = process.env, cwd = process.cwd(), requireKey = true } = {}) {
346
+ if (requestedEnvironment) validateEnvironment(requestedEnvironment, "--env");
347
+ const processProfile = environment.CDO_PROFILE || null;
348
+ const explicitSelector = requestedProfile || processProfile;
349
+ const directUrl = environment.CDO_BASE_URL;
350
+ const directKey = environment.CDO_PERSONAL_API_KEY;
351
+ const config = await loadConfig(environment);
352
+ const userDefaultProfileId = config.default_profile_id || null;
353
+
354
+ if (explicitSelector) {
355
+ const profile = findProfile(config, explicitSelector);
356
+ if (requestedEnvironment && profile.environment !== requestedEnvironment) throw new Error(`profile ${profile.id} 与 --env ${requestedEnvironment} 不匹配`);
357
+ if (profile.api_base_url) validateEndpointEnvironment(profile.api_base_url, profile.environment);
358
+ if (requireKey && (!profile.api_base_url || !profile.personal_api_key)) throw new Error(`profile ${profile.id} 缺少可用用户 Key`);
359
+ const source = requestedProfile ? "argument" : "process_profile";
360
+ return { config, environmentName: profile.environment, profile: { ...profile, source }, profileId: profile.id, selectionSource: source, userDefaultProfileId };
361
+ }
362
+
363
+ if (Boolean(directUrl) !== Boolean(directKey)) throw new Error("CDO_BASE_URL 与 CDO_PERSONAL_API_KEY 必须同时设置并绑定同一环境");
364
+ const fallbackEnvironment = requestedEnvironment || (environment.CDO_ENV ? validateEnvironment(environment.CDO_ENV, "CDO_ENV") : null);
365
+ if (directUrl) {
366
+ const environmentName = fallbackEnvironment || inferEndpointEnvironment(directUrl, "CDO_BASE_URL");
367
+ validateEndpointEnvironment(directUrl, environmentName, "CDO_BASE_URL");
368
+ return { config, environmentName, profile: { api_base_url: directUrl, personal_api_key: directKey, environment: environmentName, source: "direct_environment" }, profileId: null, selectionSource: "direct_environment", userDefaultProfileId };
369
+ }
370
+
371
+ const project = await loadProjectContext(cwd, environment);
372
+ const candidates = listProfiles(config).filter((profile) => !fallbackEnvironment || profile.environment === fallbackEnvironment);
373
+ const selectIfEligible = (id, source) => {
374
+ if (!id) return null;
375
+ const profile = config.profiles[id];
376
+ if (!profile) throw new Error(`${source === "project" ? "项目" : "用户默认"} profile 已失效:${id}`);
377
+ return !fallbackEnvironment || profile.environment === fallbackEnvironment ? { profile, source } : null;
378
+ };
379
+ const selected = selectIfEligible(project?.profile_id, "project")
380
+ || selectIfEligible(config.default_profile_id, "user_default")
381
+ || (candidates.length === 1 ? { profile: candidates[0], source: "unique_candidate" } : null);
382
+ if (!selected) {
383
+ if (!requireKey && candidates.length === 0) {
384
+ const environmentName = fallbackEnvironment || "prod";
385
+ return { config, environmentName, profile: { environment: environmentName, source: "none" }, profileId: null, selectionSource: "none", userDefaultProfileId };
386
+ }
387
+ throw candidateError(candidates, fallbackEnvironment);
388
+ }
389
+ if (selected.profile.api_base_url) validateEndpointEnvironment(selected.profile.api_base_url, selected.profile.environment);
390
+ if (requireKey && (!selected.profile.api_base_url || !selected.profile.personal_api_key)) throw new Error(`profile ${selected.profile.id} 缺少可用用户 Key`);
391
+ return { config, environmentName: selected.profile.environment, profile: { ...selected.profile, source: selected.source }, profileId: selected.profile.id, selectionSource: selected.source, userDefaultProfileId, projectProfileId: project?.profile_id || null };
392
+ }
package/lib/doctor.mjs ADDED
@@ -0,0 +1,71 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { VERSION } from "./version.mjs";
4
+ import { inspectSystemSkill } from "./skills.mjs";
5
+ import { inspectGitAuth as inspectGitAuthLive } from "./git-auth.mjs";
6
+
7
+ function executableStatus(command, run = spawnSync) {
8
+ const result = run(command, ["--version"], { encoding: "utf8", timeout: 5000 });
9
+ return {
10
+ available: !result.error && result.status === 0,
11
+ version: !result.error && result.status === 0
12
+ ? String(result.stdout || result.stderr || "").trim().split("\n")[0]
13
+ : null,
14
+ };
15
+ }
16
+
17
+ export async function diagnose({
18
+ context,
19
+ environment = process.env,
20
+ fetchImplementation = fetch,
21
+ run = spawnSync,
22
+ agent = null,
23
+ inspectSkill = inspectSystemSkill,
24
+ inspectGitAuth = inspectGitAuthLive,
25
+ }) {
26
+ const profile = context.profile;
27
+ let identity = null;
28
+ let api = { reachable: false, authenticated: false, status: null };
29
+ if (profile.api_base_url && profile.personal_api_key) {
30
+ try {
31
+ const response = await fetchImplementation(new URL("/api/v1/auth/me", profile.api_base_url), {
32
+ headers: { Authorization: `ApiKey ${profile.personal_api_key}`, Accept: "application/json", "X-CDO-Client": "cdo-cli", "X-CDO-Version": VERSION },
33
+ redirect: "error",
34
+ signal: AbortSignal.timeout(10_000),
35
+ });
36
+ api = { reachable: true, authenticated: response.ok, status: response.status };
37
+ const body = await response.json().catch(() => null);
38
+ if (response.ok) identity = body?.data || null;
39
+ } catch (error) {
40
+ api = { reachable: false, authenticated: false, status: null, error: error?.code || error?.name || "request_failed" };
41
+ }
42
+ }
43
+ const liveUser = identity ? { id: identity.user_id || identity.id, name: identity.name || identity.display_name, role: identity.role, verified: true } : null;
44
+ const cachedUser = profile.user ? { ...profile.user, verified: false } : null;
45
+ const liveEnterpriseId = identity?.enterprise_id || null;
46
+ const enterprise = liveEnterpriseId
47
+ ? { id: liveEnterpriseId, cache_matches: profile.enterprise?.id === liveEnterpriseId, ...(profile.enterprise?.id === liveEnterpriseId ? profile.enterprise : {}) }
48
+ : profile.enterprise ? { ...profile.enterprise, verified: false } : null;
49
+ const systemSkill = agent
50
+ ? await inspectSkill({ agent, environment, fetchImplementation })
51
+ : { status: "not_checked", installed_version: null, latest_version: null };
52
+ const gitAuth = api.authenticated ? await inspectGitAuth({ context, fetchImplementation, run }) : {
53
+ status: "not_authenticated", tea_api_ready: false, git_ssh_ready: false,
54
+ };
55
+ return {
56
+ environment: context.environmentName,
57
+ profile_id: context.profileId || null,
58
+ selection_source: context.selectionSource,
59
+ user_default_profile_id: context.userDefaultProfileId || null,
60
+ api_base_url: profile.api_base_url || null,
61
+ enterprise,
62
+ user: liveUser || cachedUser,
63
+ key_available: Boolean(profile.personal_api_key),
64
+ api,
65
+ cli: { version: VERSION },
66
+ system_skill: systemSkill,
67
+ git_auth: gitAuth,
68
+ git: executableStatus("git", run),
69
+ tea: executableStatus("tea", run),
70
+ };
71
+ }