@luckypiggy/zhihu-ai-cli 0.1.0-test.1
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/README.md +92 -0
- package/assets/codex-skills/zhihu-aiops/SKILL.md +39 -0
- package/assets/codex-skills/zhihu-aiops/agents/openai.yaml +4 -0
- package/assets/codex-skills/zhihu-aiops/references/commands.md +33 -0
- package/assets/codex-skills/zhihu-aiops/references/errors.md +14 -0
- package/assets/codex-skills/zhihu-aiops/references/safety.md +9 -0
- package/bin/zhihu.js +5 -0
- package/dist/auth-service.js +204 -0
- package/dist/browser.js +49 -0
- package/dist/cli.js +1092 -0
- package/dist/codex-skill.js +573 -0
- package/dist/config.js +350 -0
- package/dist/contracts.js +15 -0
- package/dist/credentials.js +198 -0
- package/dist/http.js +1875 -0
- package/dist/install.js +435 -0
- package/dist/logger.js +28 -0
- package/dist/output.js +48 -0
- package/dist/pkce.js +15 -0
- package/dist/profile-lock.js +126 -0
- package/dist/redaction.js +42 -0
- package/dist/token-manager.js +320 -0
- package/package.json +50 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { CLI_VERSION, MIN_NODE_MAJOR, } from "./contracts.js";
|
|
6
|
+
import { ConfigError, DEFAULT_PROFILE, DEFAULT_PROFILE_NAME, checkProfile, configRoot, guardProfileConnectionChange, initializeProfile, ProfileConnectionGuardError, readConfigSnapshot, readProfileRaw, validateProfile, } from "./config.js";
|
|
7
|
+
import { withProfileLock } from "./profile-lock.js";
|
|
8
|
+
import { emitError, emitSuccess } from "./output.js";
|
|
9
|
+
import { redactString, redactValue } from "./redaction.js";
|
|
10
|
+
import { AuthService } from "./auth-service.js";
|
|
11
|
+
import { SecureCredentialStore } from "./credentials.js";
|
|
12
|
+
import { IntegrationHttpClient, HttpError, validateIntegrationInfo, } from "./http.js";
|
|
13
|
+
import { CliAuthError, TokenManager, mapFailure } from "./token-manager.js";
|
|
14
|
+
import { CodexSkillError, inspectCodexSkill, installCodexSkill, uninstallCodexSkill, } from "./codex-skill.js";
|
|
15
|
+
import { InstallError, installWizard } from "./install.js";
|
|
16
|
+
class CliFailure extends Error {
|
|
17
|
+
shape;
|
|
18
|
+
constructor(shape) {
|
|
19
|
+
super(shape.message);
|
|
20
|
+
this.name = "CliFailure";
|
|
21
|
+
this.shape = shape;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function parseArguments(argv) {
|
|
25
|
+
const command = [];
|
|
26
|
+
const flags = new Map();
|
|
27
|
+
const duplicates = new Set();
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
let json = false;
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const argument = argv[index];
|
|
32
|
+
if (argument === "--help" || argument === "--version") {
|
|
33
|
+
if (seen.has(argument))
|
|
34
|
+
duplicates.add(argument);
|
|
35
|
+
seen.add(argument);
|
|
36
|
+
command.push(argument);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (argument === "--json") {
|
|
40
|
+
if (seen.has("json"))
|
|
41
|
+
duplicates.add("json");
|
|
42
|
+
seen.add("json");
|
|
43
|
+
json = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (argument.startsWith("--")) {
|
|
47
|
+
const separator = argument.indexOf("=");
|
|
48
|
+
if (separator > 2) {
|
|
49
|
+
flags.set(argument.slice(2, separator), argument.slice(separator + 1));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const key = argument.slice(2);
|
|
53
|
+
const next = argv[index + 1];
|
|
54
|
+
if (next && !next.startsWith("--")) {
|
|
55
|
+
flags.set(key, next);
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
flags.set(key, true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const key = argument.slice(2).split("=", 1)[0];
|
|
63
|
+
if (seen.has(key))
|
|
64
|
+
duplicates.add(key);
|
|
65
|
+
seen.add(key);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
command.push(argument);
|
|
69
|
+
}
|
|
70
|
+
return { command, json, flags, duplicates };
|
|
71
|
+
}
|
|
72
|
+
function context(parsed) {
|
|
73
|
+
return { command: parsed.command.join(" ") || "help", json: parsed.json };
|
|
74
|
+
}
|
|
75
|
+
function invalid(message) {
|
|
76
|
+
throw new CliFailure({
|
|
77
|
+
code: "INVALID_ARGUMENT",
|
|
78
|
+
message,
|
|
79
|
+
retryable: false,
|
|
80
|
+
exit_code: 2,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function profilePartial(flags) {
|
|
84
|
+
const partial = {};
|
|
85
|
+
const baseUrl = flags.get("base-url");
|
|
86
|
+
if (baseUrl !== undefined) {
|
|
87
|
+
if (typeof baseUrl !== "string")
|
|
88
|
+
invalid("--base-url requires a URL");
|
|
89
|
+
partial.base_url = baseUrl;
|
|
90
|
+
}
|
|
91
|
+
const timeout = flags.get("timeout");
|
|
92
|
+
if (timeout !== undefined) {
|
|
93
|
+
if (typeof timeout !== "string" || !/^\d+$/.test(timeout))
|
|
94
|
+
invalid("--timeout requires seconds");
|
|
95
|
+
partial.timeout_seconds = Number(timeout);
|
|
96
|
+
}
|
|
97
|
+
const language = flags.get("language");
|
|
98
|
+
if (language !== undefined) {
|
|
99
|
+
if (language !== "zh" && language !== "en")
|
|
100
|
+
invalid("--language must be zh or en");
|
|
101
|
+
partial.language = language;
|
|
102
|
+
}
|
|
103
|
+
const caBundle = flags.get("ca-bundle");
|
|
104
|
+
if (caBundle !== undefined) {
|
|
105
|
+
if (typeof caBundle !== "string")
|
|
106
|
+
invalid("--ca-bundle requires a path");
|
|
107
|
+
partial.ca_bundle = caBundle;
|
|
108
|
+
}
|
|
109
|
+
const skillDir = flags.get("codex-skill-dir");
|
|
110
|
+
if (skillDir !== undefined) {
|
|
111
|
+
if (typeof skillDir !== "string")
|
|
112
|
+
invalid("--codex-skill-dir requires a path");
|
|
113
|
+
partial.codex_skill_dir = skillDir;
|
|
114
|
+
}
|
|
115
|
+
return partial;
|
|
116
|
+
}
|
|
117
|
+
function versionData() {
|
|
118
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
119
|
+
return {
|
|
120
|
+
cli_version: CLI_VERSION,
|
|
121
|
+
node_version: process.versions.node,
|
|
122
|
+
node_supported: major >= MIN_NODE_MAJOR,
|
|
123
|
+
package: "@luckypiggy/zhihu-ai-cli",
|
|
124
|
+
stage: "p1-readonly",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function doctorDetail(error, fallback) {
|
|
128
|
+
if (error instanceof HttpError || error instanceof CliAuthError || error instanceof ConfigError) {
|
|
129
|
+
return redactString(error.message);
|
|
130
|
+
}
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
function caStatus(path) {
|
|
134
|
+
if (!path)
|
|
135
|
+
return { configured: false, state: "not_configured" };
|
|
136
|
+
try {
|
|
137
|
+
const stat = lstatSync(path);
|
|
138
|
+
const valid = isAbsolute(path) && !stat.isSymbolicLink() && stat.isFile()
|
|
139
|
+
&& stat.size >= 1 && stat.size <= 4 * 1024 * 1024;
|
|
140
|
+
return { configured: true, state: valid ? "valid" : "invalid" };
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return { configured: true, state: "unavailable" };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export async function doctorData(environment = process.env, options = {}) {
|
|
147
|
+
const runtimeMajor = Number(process.versions.node.split(".")[0]);
|
|
148
|
+
let configCheck;
|
|
149
|
+
let configError;
|
|
150
|
+
let selectedProfileName = options.profile;
|
|
151
|
+
let profile = options.profileConfig;
|
|
152
|
+
let configPresent = false;
|
|
153
|
+
if (profile) {
|
|
154
|
+
try {
|
|
155
|
+
selectedProfileName = selectedProfileName ? selectedProfileName : DEFAULT_PROFILE_NAME;
|
|
156
|
+
profile = validateProfile(profile);
|
|
157
|
+
configPresent = true;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
configError = error instanceof ConfigError ? error.message : "Profile configuration is invalid";
|
|
161
|
+
profile = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
try {
|
|
166
|
+
configCheck = checkProfile(environment, options.profile);
|
|
167
|
+
selectedProfileName = configCheck.snapshot.selected_profile;
|
|
168
|
+
configPresent = existsSync(configCheck.snapshot.profile_path);
|
|
169
|
+
profile = configCheck.profile;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
configError = error instanceof ConfigError ? error.message : "Unable to read configuration";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const profileError = configCheck?.error;
|
|
176
|
+
const invalidDetail = configError || profileError;
|
|
177
|
+
const profileReady = configPresent && Boolean(profile) && !invalidDetail;
|
|
178
|
+
let keychain = { ok: false, state: "not_checked", detail: "No configured profile" };
|
|
179
|
+
let authentication = { ok: false, state: "not_checked", detail: "No configured profile" };
|
|
180
|
+
let capabilityCheck = { ok: false, state: "not_checked" };
|
|
181
|
+
let service = { ok: false, state: "not_checked", detail: "Configure a profile before checking the Pilot service" };
|
|
182
|
+
let network = { ok: false, state: "not_checked", detail: "Configure a profile before checking the Pilot service" };
|
|
183
|
+
let tlsCa = { protocol: "invalid", https: false, ca: { configured: false, state: "not_configured" } };
|
|
184
|
+
let credentials;
|
|
185
|
+
let storedCredential = null;
|
|
186
|
+
let client;
|
|
187
|
+
let serviceValidatedReady = false;
|
|
188
|
+
if (profileReady && profile && selectedProfileName) {
|
|
189
|
+
let parsedUrl;
|
|
190
|
+
try {
|
|
191
|
+
parsedUrl = new URL(profile.base_url);
|
|
192
|
+
}
|
|
193
|
+
catch { /* config validation reports this below */ }
|
|
194
|
+
tlsCa = { protocol: parsedUrl?.protocol ?? "invalid", https: parsedUrl?.protocol === "https:", ca: caStatus(profile.ca_bundle) };
|
|
195
|
+
try {
|
|
196
|
+
credentials = options.credentialStoreFactory
|
|
197
|
+
? options.credentialStoreFactory(selectedProfileName)
|
|
198
|
+
: new SecureCredentialStore(selectedProfileName);
|
|
199
|
+
await credentials.available();
|
|
200
|
+
keychain = { ok: true, state: "available" };
|
|
201
|
+
storedCredential = await credentials.get();
|
|
202
|
+
authentication = { ok: Boolean(storedCredential), state: storedCredential ? "configured" : "not_configured" };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
keychain = { ok: false, state: "unavailable", detail: doctorDetail(error, "System credential store is unavailable") };
|
|
206
|
+
authentication = { ok: false, state: "unavailable", detail: "System credential store is unavailable" };
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
client = options.clientFactory ? options.clientFactory(profile) : new IntegrationHttpClient(profile);
|
|
210
|
+
const info = validateIntegrationInfo(await client.info(), false);
|
|
211
|
+
const ready = info.status === "ready" && info.auth?.configured === true;
|
|
212
|
+
serviceValidatedReady = ready;
|
|
213
|
+
service = {
|
|
214
|
+
ok: ready,
|
|
215
|
+
state: ready ? "ready" : "configuration_required",
|
|
216
|
+
service: info.service,
|
|
217
|
+
protocol_version: info.protocol_version,
|
|
218
|
+
minimum_cli_version: info.minimum_cli_version,
|
|
219
|
+
compatible: true,
|
|
220
|
+
status: info.status,
|
|
221
|
+
capabilities: info.capabilities,
|
|
222
|
+
};
|
|
223
|
+
network = { ok: true, state: "reachable" };
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const incompatible = error instanceof HttpError && error.code === "VERSION_INCOMPATIBLE";
|
|
227
|
+
service = { ok: false, state: incompatible ? "incompatible" : "invalid", detail: doctorDetail(error, "Pilot service response is invalid") };
|
|
228
|
+
const transportFailure = error instanceof HttpError
|
|
229
|
+
&& (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT");
|
|
230
|
+
network = transportFailure
|
|
231
|
+
? { ok: false, state: "unreachable", detail: doctorDetail(error, "Unable to reach the configured Pilot service") }
|
|
232
|
+
: { ok: true, state: "reachable", detail: doctorDetail(error, "Pilot service returned an invalid response") };
|
|
233
|
+
}
|
|
234
|
+
if (!serviceValidatedReady) {
|
|
235
|
+
// Never send a stored credential to an origin whose strict /info
|
|
236
|
+
// identity, version, readiness, or capability contract failed.
|
|
237
|
+
capabilityCheck = { ok: false, state: "service_invalid", detail: "Capabilities were not checked because the Pilot service is not ready" };
|
|
238
|
+
}
|
|
239
|
+
else if (!credentials || !client) {
|
|
240
|
+
capabilityCheck = { ok: false, state: "unavailable", detail: "Authentication or service checks are unavailable" };
|
|
241
|
+
}
|
|
242
|
+
else if (!storedCredential) {
|
|
243
|
+
capabilityCheck = { ok: false, state: "not_logged_in" };
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
try {
|
|
247
|
+
const manager = new TokenManager(client, credentials, selectedProfileName);
|
|
248
|
+
const capabilities = await manager.callReadOnly((accessToken) => client?.capabilities(accessToken));
|
|
249
|
+
const data = capabilities;
|
|
250
|
+
capabilityCheck = { ok: true, state: "available", scopes: data.scopes, capabilities: data.capabilities };
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
capabilityCheck = {
|
|
254
|
+
ok: false,
|
|
255
|
+
state: error instanceof CliAuthError ? "unauthorized" : "unavailable",
|
|
256
|
+
detail: doctorDetail(error, "Capabilities are unavailable"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const codex = inspectCodexSkill({
|
|
262
|
+
environment,
|
|
263
|
+
...(selectedProfileName ? { profile: selectedProfileName } : {}),
|
|
264
|
+
});
|
|
265
|
+
const configCheckResult = {
|
|
266
|
+
ok: !invalidDetail && (profileReady || !configPresent),
|
|
267
|
+
state: invalidDetail ? "invalid" : profileReady ? "configured" : "not_configured",
|
|
268
|
+
path: configRoot(environment),
|
|
269
|
+
...(invalidDetail ? { detail: redactString(invalidDetail) } : {}),
|
|
270
|
+
...(selectedProfileName ? { profile: selectedProfileName } : {}),
|
|
271
|
+
};
|
|
272
|
+
const runtime = { ok: runtimeMajor >= MIN_NODE_MAJOR, node_version: process.versions.node, minimum_node_major: MIN_NODE_MAJOR };
|
|
273
|
+
const codexCheck = { ok: codex.state === "managed", state: codex.state, ...(codex.target ? { target: codex.target } : {}) };
|
|
274
|
+
return {
|
|
275
|
+
cli_version: CLI_VERSION,
|
|
276
|
+
stage: "p1-readonly",
|
|
277
|
+
checks: {
|
|
278
|
+
runtime,
|
|
279
|
+
config: configCheckResult,
|
|
280
|
+
tls_ca: tlsCa,
|
|
281
|
+
service,
|
|
282
|
+
authentication,
|
|
283
|
+
capabilities: capabilityCheck,
|
|
284
|
+
codex: codexCheck,
|
|
285
|
+
// Compatibility aliases retained for existing automation; the stable
|
|
286
|
+
// Stage5B contract is the seven checks above.
|
|
287
|
+
base_url: { ok: profileReady, state: profileReady ? "configured" : "not_configured" },
|
|
288
|
+
tls: tlsCa,
|
|
289
|
+
keychain,
|
|
290
|
+
network,
|
|
291
|
+
info: service,
|
|
292
|
+
codex_skill: codexCheck,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function selectedProfile(flags) {
|
|
297
|
+
const requested = flags.get("profile");
|
|
298
|
+
if (requested !== undefined && typeof requested !== "string")
|
|
299
|
+
invalid("--profile requires a name");
|
|
300
|
+
const result = checkProfile(process.env, requested);
|
|
301
|
+
if (!result.profile) {
|
|
302
|
+
throw new CliFailure({
|
|
303
|
+
code: "CONFIG_INVALID",
|
|
304
|
+
message: result.error || "Profile is not configured",
|
|
305
|
+
retryable: false,
|
|
306
|
+
exit_code: 2,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (!result.profile.base_url) {
|
|
310
|
+
throw new CliFailure({
|
|
311
|
+
code: "CONFIG_INVALID",
|
|
312
|
+
message: "Profile base_url is required for authentication",
|
|
313
|
+
retryable: false,
|
|
314
|
+
exit_code: 2,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return { name: result.snapshot.selected_profile, profile: result.profile };
|
|
318
|
+
}
|
|
319
|
+
function authService(flags) {
|
|
320
|
+
const selected = selectedProfile(flags);
|
|
321
|
+
let client;
|
|
322
|
+
try {
|
|
323
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
throw mapFailure(error);
|
|
327
|
+
}
|
|
328
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
329
|
+
return new AuthService(client, credentials, selected.name, {
|
|
330
|
+
onBrowserFailure: (authorizationUrl) => {
|
|
331
|
+
process.stderr.write(`Browser launch failed. Open this URL manually: ${authorizationUrl}\n`);
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async function authCommand(action, flags) {
|
|
336
|
+
if (action !== "login" && action !== "status" && action !== "logout")
|
|
337
|
+
invalid("Use auth login, auth status, or auth logout");
|
|
338
|
+
const service = authService(flags);
|
|
339
|
+
if (action === "login")
|
|
340
|
+
return service.login();
|
|
341
|
+
if (action === "status")
|
|
342
|
+
return service.status();
|
|
343
|
+
return service.logout();
|
|
344
|
+
}
|
|
345
|
+
async function capabilitiesCommand(action, flags) {
|
|
346
|
+
if (action !== "list")
|
|
347
|
+
invalid("Use capabilities list");
|
|
348
|
+
const selected = selectedProfile(flags);
|
|
349
|
+
let client;
|
|
350
|
+
try {
|
|
351
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw mapFailure(error);
|
|
355
|
+
}
|
|
356
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
357
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
358
|
+
return tokenManager.callReadOnly((accessToken) => client.capabilities(accessToken));
|
|
359
|
+
}
|
|
360
|
+
async function employeesCommand(action, flags) {
|
|
361
|
+
if (action !== "list")
|
|
362
|
+
invalid("Use employees list");
|
|
363
|
+
const selected = selectedProfile(flags);
|
|
364
|
+
let client;
|
|
365
|
+
try {
|
|
366
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
throw mapFailure(error);
|
|
370
|
+
}
|
|
371
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
372
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
373
|
+
return tokenManager.callReadOnly((accessToken) => client.employees(accessToken));
|
|
374
|
+
}
|
|
375
|
+
function assetFilter(value, field, maxLength) {
|
|
376
|
+
if (value === undefined)
|
|
377
|
+
return undefined;
|
|
378
|
+
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f]/.test(value)) {
|
|
379
|
+
invalid(`--${field} must be text up to ${maxLength} characters`);
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
function assetPage(value, field, max, fallback) {
|
|
384
|
+
if (value === undefined)
|
|
385
|
+
return fallback;
|
|
386
|
+
if (typeof value !== "string" || !/^\d+$/.test(value))
|
|
387
|
+
invalid(`--${field} must be an integer`);
|
|
388
|
+
const number = Number(value);
|
|
389
|
+
if (!Number.isSafeInteger(number) || number < 1 || number > max)
|
|
390
|
+
invalid(`--${field} is out of range`);
|
|
391
|
+
return number;
|
|
392
|
+
}
|
|
393
|
+
async function assetsCommand(action, flags) {
|
|
394
|
+
if (action !== "list")
|
|
395
|
+
invalid("Use assets list");
|
|
396
|
+
for (const key of flags.keys()) {
|
|
397
|
+
if (!["profile", "keyword", "model", "page", "page-size"].includes(key)) {
|
|
398
|
+
invalid(`Unknown assets option: --${key}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const keyword = assetFilter(flags.get("keyword"), "keyword", 256);
|
|
402
|
+
const model = assetFilter(flags.get("model"), "model", 128);
|
|
403
|
+
const page = assetPage(flags.get("page"), "page", 10000, 1);
|
|
404
|
+
const pageSize = assetPage(flags.get("page-size"), "page-size", 100, 20);
|
|
405
|
+
const selected = selectedProfile(flags);
|
|
406
|
+
let client;
|
|
407
|
+
try {
|
|
408
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
throw mapFailure(error);
|
|
412
|
+
}
|
|
413
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
414
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
415
|
+
return tokenManager.callReadOnly((accessToken) => client.assetsSearch({ ...(keyword !== undefined ? { keyword } : {}), ...(model !== undefined ? { model } : {}), page, page_size: pageSize }, accessToken));
|
|
416
|
+
}
|
|
417
|
+
function assetRequestId(value) {
|
|
418
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
419
|
+
invalid("--id must use letters, numbers, hyphens, or underscores");
|
|
420
|
+
}
|
|
421
|
+
if (/^\d+$/.test(value)) {
|
|
422
|
+
const normalized = value.replace(/^0+/, "") || "0";
|
|
423
|
+
let numeric;
|
|
424
|
+
try {
|
|
425
|
+
numeric = BigInt(normalized);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
invalid("--id is out of range");
|
|
429
|
+
}
|
|
430
|
+
if (numeric < 1n || numeric > 9223372036854775807n) {
|
|
431
|
+
invalid("--id is out of range");
|
|
432
|
+
}
|
|
433
|
+
return numeric.toString();
|
|
434
|
+
}
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
async function assetGetCommand(flags) {
|
|
438
|
+
for (const key of flags.keys()) {
|
|
439
|
+
if (key !== "profile" && key !== "id")
|
|
440
|
+
invalid(`Unknown assets get option: --${key}`);
|
|
441
|
+
}
|
|
442
|
+
const id = assetRequestId(flags.get("id"));
|
|
443
|
+
const selected = selectedProfile(flags);
|
|
444
|
+
let client;
|
|
445
|
+
try {
|
|
446
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
throw mapFailure(error);
|
|
450
|
+
}
|
|
451
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
452
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
453
|
+
return tokenManager.callReadOnly((accessToken) => client.assetGet(id, accessToken));
|
|
454
|
+
}
|
|
455
|
+
function alarmStatus(value) {
|
|
456
|
+
if (value === undefined)
|
|
457
|
+
return "all";
|
|
458
|
+
if (typeof value !== "string" || !["all", "firing", "resolved"].includes(value)) {
|
|
459
|
+
invalid("--status must be all, firing, or resolved");
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
function alarmRequestId(value) {
|
|
464
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
465
|
+
invalid("--id must use letters, numbers, hyphens, or underscores");
|
|
466
|
+
}
|
|
467
|
+
if (/^\d+$/.test(value)) {
|
|
468
|
+
const normalized = value.replace(/^0+/, "") || "0";
|
|
469
|
+
let numeric;
|
|
470
|
+
try {
|
|
471
|
+
numeric = BigInt(normalized);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
invalid("--id is out of range");
|
|
475
|
+
}
|
|
476
|
+
if (numeric < 1n || numeric > 9223372036854775807n) {
|
|
477
|
+
invalid("--id is out of range");
|
|
478
|
+
}
|
|
479
|
+
return numeric.toString();
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
async function alarmsListCommand(flags) {
|
|
484
|
+
for (const key of flags.keys()) {
|
|
485
|
+
if (!["profile", "status", "page", "page-size"].includes(key)) {
|
|
486
|
+
invalid(`Unknown alarms option: --${key}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const status = alarmStatus(flags.get("status"));
|
|
490
|
+
const page = assetPage(flags.get("page"), "page", 10000, 1);
|
|
491
|
+
const pageSize = assetPage(flags.get("page-size"), "page-size", 100, 20);
|
|
492
|
+
const selected = selectedProfile(flags);
|
|
493
|
+
let client;
|
|
494
|
+
try {
|
|
495
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
throw mapFailure(error);
|
|
499
|
+
}
|
|
500
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
501
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
502
|
+
return tokenManager.callReadOnly((accessToken) => client.alarmsSearch({ status, page, page_size: pageSize }, accessToken));
|
|
503
|
+
}
|
|
504
|
+
async function alarmsGetCommand(flags) {
|
|
505
|
+
for (const key of flags.keys()) {
|
|
506
|
+
if (key !== "profile" && key !== "id")
|
|
507
|
+
invalid(`Unknown alarms get option: --${key}`);
|
|
508
|
+
}
|
|
509
|
+
const id = alarmRequestId(flags.get("id"));
|
|
510
|
+
const selected = selectedProfile(flags);
|
|
511
|
+
let client;
|
|
512
|
+
try {
|
|
513
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
throw mapFailure(error);
|
|
517
|
+
}
|
|
518
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
519
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
520
|
+
return tokenManager.callReadOnly((accessToken) => client.alarmGet(id, accessToken));
|
|
521
|
+
}
|
|
522
|
+
async function alarmsCommand(action, flags) {
|
|
523
|
+
if (action === "list")
|
|
524
|
+
return alarmsListCommand(flags);
|
|
525
|
+
if (action === "get")
|
|
526
|
+
return alarmsGetCommand(flags);
|
|
527
|
+
invalid("Use alarms list or alarms get --id <id>");
|
|
528
|
+
}
|
|
529
|
+
function incidentAlarmId(value) {
|
|
530
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
|
|
531
|
+
invalid("--alarm-id must use letters, numbers, hyphens, or underscores");
|
|
532
|
+
}
|
|
533
|
+
if (/^\d+$/.test(value)) {
|
|
534
|
+
const normalized = value.replace(/^0+/, "") || "0";
|
|
535
|
+
let numeric;
|
|
536
|
+
try {
|
|
537
|
+
numeric = BigInt(normalized);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
invalid("--alarm-id is out of range");
|
|
541
|
+
}
|
|
542
|
+
if (numeric < 1n || numeric > 9223372036854775807n) {
|
|
543
|
+
invalid("--alarm-id is out of range");
|
|
544
|
+
}
|
|
545
|
+
return numeric.toString();
|
|
546
|
+
}
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
async function incidentsDiagnoseCommand(flags) {
|
|
550
|
+
for (const key of flags.keys()) {
|
|
551
|
+
if (key !== "profile" && key !== "alarm-id")
|
|
552
|
+
invalid(`Unknown incidents diagnose option: --${key}`);
|
|
553
|
+
}
|
|
554
|
+
const alarmId = incidentAlarmId(flags.get("alarm-id"));
|
|
555
|
+
const selected = selectedProfile(flags);
|
|
556
|
+
let client;
|
|
557
|
+
try {
|
|
558
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
throw mapFailure(error);
|
|
562
|
+
}
|
|
563
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
564
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
565
|
+
return tokenManager.callReadOnly((accessToken) => client.incidentsDiagnose(alarmId, accessToken));
|
|
566
|
+
}
|
|
567
|
+
function incidentHumanSummary(data) {
|
|
568
|
+
const target = data.target.association === "unavailable"
|
|
569
|
+
? "target unavailable"
|
|
570
|
+
: data.target.ip
|
|
571
|
+
? `target ${data.target.ip}`
|
|
572
|
+
: "target identified";
|
|
573
|
+
return `Alarm ${data.alarm.id}: ${data.conclusion.replaceAll("_", " ")} (${data.confidence} confidence); category ${data.category}; ${target}.`;
|
|
574
|
+
}
|
|
575
|
+
function inspectionModel(value) {
|
|
576
|
+
if (value === undefined)
|
|
577
|
+
return undefined;
|
|
578
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 128 || value.trim() !== value || /[\u0000-\u001f]/.test(value)) {
|
|
579
|
+
invalid("--model must be an exact model name, code, or id");
|
|
580
|
+
}
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
function inspectionHumanSummary(data) {
|
|
584
|
+
const summary = data.summary;
|
|
585
|
+
const coverage = `${summary.monitoring_covered}/${summary.assets_examined}`;
|
|
586
|
+
const attention = data.items.length;
|
|
587
|
+
const truncated = data.truncated ? "; results truncated" : "";
|
|
588
|
+
return `Inspection ${data.grade}: checked ${summary.assets_examined}/${summary.assets_total} assets; coverage ${coverage}; active alarms ${summary.active_alarms}; needs attention ${attention}${truncated}.`;
|
|
589
|
+
}
|
|
590
|
+
async function inspectionsRunCommand(flags) {
|
|
591
|
+
for (const key of flags.keys()) {
|
|
592
|
+
if (key !== "profile" && key !== "model" && key !== "window") {
|
|
593
|
+
invalid(`Unknown inspections option: --${key}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const model = inspectionModel(flags.get("model"));
|
|
597
|
+
const window = metricWindow(flags.get("window") ?? "60m");
|
|
598
|
+
const selected = selectedProfile(flags);
|
|
599
|
+
let client;
|
|
600
|
+
try {
|
|
601
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
throw mapFailure(error);
|
|
605
|
+
}
|
|
606
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
607
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
608
|
+
return tokenManager.callReadOnly((accessToken) => client.inspectionsRun({
|
|
609
|
+
...(model !== undefined ? { model } : {}),
|
|
610
|
+
window_minutes: window,
|
|
611
|
+
}, accessToken));
|
|
612
|
+
}
|
|
613
|
+
function metricWindow(value) {
|
|
614
|
+
if (typeof value !== "string" || !/^[0-9]+[mh]$/.test(value)) {
|
|
615
|
+
invalid("--window must use a duration such as 15m or 2h");
|
|
616
|
+
}
|
|
617
|
+
const amount = Number(value.slice(0, -1));
|
|
618
|
+
const minutes = value.endsWith("h") ? amount * 60 : amount;
|
|
619
|
+
if (!Number.isSafeInteger(minutes) || minutes < 5 || minutes > 1440) {
|
|
620
|
+
invalid("--window must be between 5m and 24h");
|
|
621
|
+
}
|
|
622
|
+
return minutes;
|
|
623
|
+
}
|
|
624
|
+
function metricPoints(value) {
|
|
625
|
+
if (typeof value !== "string" || !/^\d+$/.test(value))
|
|
626
|
+
invalid("--max-points must be an integer");
|
|
627
|
+
const points = Number(value);
|
|
628
|
+
if (!Number.isSafeInteger(points) || points < 1 || points > 120)
|
|
629
|
+
invalid("--max-points must be between 1 and 120");
|
|
630
|
+
return points;
|
|
631
|
+
}
|
|
632
|
+
function metricAsset(value) {
|
|
633
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 128 || /[\u0000-\u001f]/.test(value)) {
|
|
634
|
+
invalid("--asset is required and must be an IP address or asset id");
|
|
635
|
+
}
|
|
636
|
+
if (isIP(value) > 0)
|
|
637
|
+
return value;
|
|
638
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value))
|
|
639
|
+
invalid("--asset must be an IP address or asset id");
|
|
640
|
+
if (/^\d+$/.test(value)) {
|
|
641
|
+
try {
|
|
642
|
+
const numeric = BigInt(value);
|
|
643
|
+
if (numeric < 1n || numeric > 9223372036854775807n)
|
|
644
|
+
invalid("--asset id is out of range");
|
|
645
|
+
return numeric.toString();
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
invalid("--asset id is out of range");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
async function metricsCommand(action, flags) {
|
|
654
|
+
if (action !== "query")
|
|
655
|
+
invalid("Use metrics query --asset <id|ip> --metric <name>");
|
|
656
|
+
for (const key of flags.keys()) {
|
|
657
|
+
if (!["profile", "asset", "metric", "window", "max-points"].includes(key)) {
|
|
658
|
+
invalid(`Unknown metrics option: --${key}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const asset = metricAsset(flags.get("asset"));
|
|
662
|
+
const metric = flags.get("metric");
|
|
663
|
+
const allowed = new Set(["online", "cpu", "memory", "load", "disk", "io", "snmp"]);
|
|
664
|
+
if (typeof metric !== "string" || !allowed.has(metric))
|
|
665
|
+
invalid("--metric is invalid");
|
|
666
|
+
const window = metricWindow(flags.get("window") ?? "60m");
|
|
667
|
+
const maxPoints = metricPoints(flags.get("max-points") ?? "60");
|
|
668
|
+
const selected = selectedProfile(flags);
|
|
669
|
+
let client;
|
|
670
|
+
try {
|
|
671
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
throw mapFailure(error);
|
|
675
|
+
}
|
|
676
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
677
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
678
|
+
return tokenManager.callReadOnly((accessToken) => client.metricsQuery({
|
|
679
|
+
asset,
|
|
680
|
+
metric: metric,
|
|
681
|
+
window_minutes: window,
|
|
682
|
+
max_points: maxPoints,
|
|
683
|
+
}, accessToken));
|
|
684
|
+
}
|
|
685
|
+
function validateChatEmployeeId(value) {
|
|
686
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value))
|
|
687
|
+
invalid("employee-id must use letters, numbers, hyphens, or underscores");
|
|
688
|
+
}
|
|
689
|
+
function validateChatConversationId(value) {
|
|
690
|
+
if (!/^[A-Za-z0-9_-]{1,96}$/.test(value))
|
|
691
|
+
invalid("--conversation must use letters, numbers, hyphens, or underscores");
|
|
692
|
+
}
|
|
693
|
+
async function employeesChatCommand(employeeId, flags, json) {
|
|
694
|
+
if (!employeeId)
|
|
695
|
+
invalid("Use employees chat <employee-id> --message <text>");
|
|
696
|
+
validateChatEmployeeId(employeeId);
|
|
697
|
+
const message = flags.get("message");
|
|
698
|
+
if (typeof message !== "string" || message.trim().length === 0 || message.length > 10000 || message.includes("\u0000")) {
|
|
699
|
+
invalid("--message is required and must be between 1 and 10000 characters");
|
|
700
|
+
}
|
|
701
|
+
const conversation = flags.get("conversation");
|
|
702
|
+
if (conversation !== undefined) {
|
|
703
|
+
if (typeof conversation !== "string")
|
|
704
|
+
invalid("--conversation requires an id");
|
|
705
|
+
validateChatConversationId(conversation);
|
|
706
|
+
}
|
|
707
|
+
for (const key of flags.keys()) {
|
|
708
|
+
if (key !== "profile" && key !== "message" && key !== "conversation")
|
|
709
|
+
invalid(`Unknown employees chat option: --${key}`);
|
|
710
|
+
}
|
|
711
|
+
const selected = selectedProfile(flags);
|
|
712
|
+
let client;
|
|
713
|
+
try {
|
|
714
|
+
client = new IntegrationHttpClient(selected.profile);
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
throw mapFailure(error);
|
|
718
|
+
}
|
|
719
|
+
const credentials = new SecureCredentialStore(selected.name);
|
|
720
|
+
const tokenManager = new TokenManager(client, credentials, selected.name);
|
|
721
|
+
const controller = new AbortController();
|
|
722
|
+
const onSigint = () => controller.abort();
|
|
723
|
+
let humanOutputStarted = false;
|
|
724
|
+
process.once("SIGINT", onSigint);
|
|
725
|
+
try {
|
|
726
|
+
const data = await tokenManager.callStreaming((accessToken, markEvent) => client.employeeChat(employeeId, message.trim(), conversation, accessToken, {
|
|
727
|
+
signal: controller.signal,
|
|
728
|
+
onEvent: (event) => {
|
|
729
|
+
markEvent();
|
|
730
|
+
if (!json && event.type === "delta") {
|
|
731
|
+
humanOutputStarted = true;
|
|
732
|
+
process.stdout.write(event.text);
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
}));
|
|
736
|
+
if (!json) {
|
|
737
|
+
if (humanOutputStarted)
|
|
738
|
+
process.stdout.write("\n");
|
|
739
|
+
process.stderr.write(`conversation_id: ${data.conversation_id}\n`);
|
|
740
|
+
}
|
|
741
|
+
return { data, streamed: !json };
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
if (!json && humanOutputStarted)
|
|
745
|
+
process.stdout.write("\n");
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
finally {
|
|
749
|
+
process.removeListener("SIGINT", onSigint);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function showConfig(flags) {
|
|
753
|
+
const requested = flags.get("profile");
|
|
754
|
+
if (requested !== undefined && typeof requested !== "string")
|
|
755
|
+
invalid("--profile requires a name");
|
|
756
|
+
const snapshot = readConfigSnapshot(process.env, requested);
|
|
757
|
+
return {
|
|
758
|
+
redacted: true,
|
|
759
|
+
selected_profile: snapshot.selected_profile,
|
|
760
|
+
config: redactValue(snapshot.root),
|
|
761
|
+
profile: redactValue(snapshot.profile),
|
|
762
|
+
paths: {
|
|
763
|
+
config: snapshot.root_path,
|
|
764
|
+
profile: snapshot.profile_path,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
export async function configInit(flags, options = {}) {
|
|
769
|
+
const allowed = new Set([
|
|
770
|
+
"profile",
|
|
771
|
+
"base-url",
|
|
772
|
+
"timeout",
|
|
773
|
+
"language",
|
|
774
|
+
"ca-bundle",
|
|
775
|
+
"codex-skill-dir",
|
|
776
|
+
"force",
|
|
777
|
+
]);
|
|
778
|
+
for (const key of flags.keys()) {
|
|
779
|
+
if (!allowed.has(key))
|
|
780
|
+
invalid(`Unknown config init option: --${key}`);
|
|
781
|
+
}
|
|
782
|
+
const forceValue = flags.get("force");
|
|
783
|
+
if (forceValue !== undefined && forceValue !== true)
|
|
784
|
+
invalid("--force does not accept a value");
|
|
785
|
+
const requested = flags.get("profile") ?? DEFAULT_PROFILE_NAME;
|
|
786
|
+
if (typeof requested !== "string")
|
|
787
|
+
invalid("--profile requires a name");
|
|
788
|
+
const force = flags.get("force") === true;
|
|
789
|
+
const environment = options.environment ?? process.env;
|
|
790
|
+
const credentialProbeFactory = options.credentialStoreFactory
|
|
791
|
+
?? ((name) => new SecureCredentialStore(name));
|
|
792
|
+
let profile;
|
|
793
|
+
try {
|
|
794
|
+
// Keep lock ordering identical to install: global root first, then the
|
|
795
|
+
// target profile. Existing configuration is strictly validated before a
|
|
796
|
+
// forced origin/CA change can write either file.
|
|
797
|
+
profile = await withProfileLock("install-root", async () => withProfileLock(requested, async () => {
|
|
798
|
+
const previousRaw = readProfileRaw(requested, environment);
|
|
799
|
+
const next = validateProfile({ ...DEFAULT_PROFILE, ...profilePartial(flags) });
|
|
800
|
+
if (previousRaw !== undefined && force) {
|
|
801
|
+
const previous = validateProfile(previousRaw);
|
|
802
|
+
await guardProfileConnectionChange(requested, previous, next, credentialProbeFactory);
|
|
803
|
+
}
|
|
804
|
+
return initializeProfile(requested, profilePartial(flags), environment, force);
|
|
805
|
+
}, { environment }), { environment });
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
if (error instanceof ProfileConnectionGuardError) {
|
|
809
|
+
throw new CliFailure({
|
|
810
|
+
code: error.code,
|
|
811
|
+
message: error.message,
|
|
812
|
+
retryable: false,
|
|
813
|
+
exit_code: error.exitCode,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
profile: requested,
|
|
820
|
+
config_root: configRoot(environment),
|
|
821
|
+
values: profile,
|
|
822
|
+
redacted: true,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function codexSkillCommand(action, flags) {
|
|
826
|
+
for (const key of flags.keys()) {
|
|
827
|
+
if (key !== "profile" && key !== "codex-skill-dir" && key !== "force") {
|
|
828
|
+
invalid(`Unknown ${action} option: --${key}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const profile = flags.get("profile");
|
|
832
|
+
if (profile !== undefined && typeof profile !== "string")
|
|
833
|
+
invalid("--profile requires a name");
|
|
834
|
+
const codexSkillDir = flags.get("codex-skill-dir");
|
|
835
|
+
if (codexSkillDir !== undefined && typeof codexSkillDir !== "string")
|
|
836
|
+
invalid("--codex-skill-dir requires a path");
|
|
837
|
+
const force = flags.get("force");
|
|
838
|
+
if (force !== undefined && force !== true)
|
|
839
|
+
invalid("--force does not accept a value");
|
|
840
|
+
const options = {
|
|
841
|
+
...(profile === undefined ? {} : { profile }),
|
|
842
|
+
...(codexSkillDir === undefined ? {} : { codexSkillDir }),
|
|
843
|
+
...(force === undefined ? {} : { force: true }),
|
|
844
|
+
environment: process.env,
|
|
845
|
+
};
|
|
846
|
+
try {
|
|
847
|
+
const result = action === "install" ? installCodexSkill(options) : uninstallCodexSkill(options);
|
|
848
|
+
return { ...result };
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
if (error instanceof CodexSkillError) {
|
|
852
|
+
throw new CliFailure({
|
|
853
|
+
code: error.code,
|
|
854
|
+
message: error.message,
|
|
855
|
+
retryable: false,
|
|
856
|
+
exit_code: error.exitCode,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function installCommand(flags, json) {
|
|
863
|
+
const allowed = new Set([
|
|
864
|
+
"profile", "base-url", "auth-mode", "timeout", "language", "ca-file", "codex-skill-dir",
|
|
865
|
+
"skip-login", "skip-codex", "force",
|
|
866
|
+
]);
|
|
867
|
+
for (const key of flags.keys()) {
|
|
868
|
+
if (!allowed.has(key))
|
|
869
|
+
invalid(`Unknown install option: --${key}`);
|
|
870
|
+
}
|
|
871
|
+
for (const key of ["skip-login", "skip-codex", "force"]) {
|
|
872
|
+
const value = flags.get(key);
|
|
873
|
+
if (value !== undefined && value !== true)
|
|
874
|
+
invalid(`--${key} does not accept a value`);
|
|
875
|
+
}
|
|
876
|
+
const stringOption = (key) => {
|
|
877
|
+
const value = flags.get(key);
|
|
878
|
+
if (value === undefined)
|
|
879
|
+
return undefined;
|
|
880
|
+
if (typeof value !== "string")
|
|
881
|
+
invalid(`--${key} requires a value`);
|
|
882
|
+
return value;
|
|
883
|
+
};
|
|
884
|
+
try {
|
|
885
|
+
const result = await installWizard({
|
|
886
|
+
profile: stringOption("profile"),
|
|
887
|
+
baseUrl: stringOption("base-url"),
|
|
888
|
+
authMode: stringOption("auth-mode"),
|
|
889
|
+
timeout: stringOption("timeout"),
|
|
890
|
+
language: stringOption("language"),
|
|
891
|
+
caFile: stringOption("ca-file"),
|
|
892
|
+
codexSkillDir: stringOption("codex-skill-dir"),
|
|
893
|
+
skipLogin: flags.get("skip-login") === true,
|
|
894
|
+
skipCodex: flags.get("skip-codex") === true,
|
|
895
|
+
force: flags.get("force") === true,
|
|
896
|
+
json,
|
|
897
|
+
doctor: (environment, profileName, profile) => doctorData(environment, {
|
|
898
|
+
...(profileName ? { profile: profileName } : {}),
|
|
899
|
+
...(profile ? { profileConfig: profile } : {}),
|
|
900
|
+
}),
|
|
901
|
+
});
|
|
902
|
+
return { ...result };
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
if (error instanceof InstallError) {
|
|
906
|
+
throw new CliFailure({
|
|
907
|
+
code: error.code,
|
|
908
|
+
message: error.message,
|
|
909
|
+
retryable: error.retryable,
|
|
910
|
+
exit_code: error.exitCode,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function helpData() {
|
|
917
|
+
return {
|
|
918
|
+
commands: [
|
|
919
|
+
"version",
|
|
920
|
+
"doctor",
|
|
921
|
+
"config init",
|
|
922
|
+
"config show --redact",
|
|
923
|
+
"auth login",
|
|
924
|
+
"auth status",
|
|
925
|
+
"auth logout",
|
|
926
|
+
"capabilities list",
|
|
927
|
+
"employees list",
|
|
928
|
+
"employees chat <employee-id> --message <text>",
|
|
929
|
+
"assets list [--keyword <text>] [--model <name|code|id>] [--page N] [--page-size N]",
|
|
930
|
+
"assets get --id <id>",
|
|
931
|
+
"metrics query --asset <id|ip> --metric <online|cpu|memory|load|disk|io|snmp> [--window 5m..24h] [--max-points N]",
|
|
932
|
+
"alarms list [--status all|firing|resolved] [--page N] [--page-size N]",
|
|
933
|
+
"alarms get --id <id>",
|
|
934
|
+
"incidents diagnose --alarm-id <id>",
|
|
935
|
+
"inspections run [--model <name|code|id>] [--window 5m..24h]",
|
|
936
|
+
"install [--profile <name>] [--base-url <https-url>] [--auth-mode browser] [--timeout N] [--language zh-CN|en] [--ca-file <absolute-file>] [--codex-skill-dir <absolute-path>] [--skip-login] [--skip-codex] [--force]",
|
|
937
|
+
"install codex [--profile <name>] [--codex-skill-dir <absolute-path>] [--force]",
|
|
938
|
+
"uninstall codex [--profile <name>] [--codex-skill-dir <absolute-path>] [--force]",
|
|
939
|
+
],
|
|
940
|
+
json: "Append --json for the stable success/error envelope",
|
|
941
|
+
note: "Authentication plus read-only employee, asset, metric, alarm, incident diagnosis, inspection queries, local installation, and the Codex Skill installer are available; install also accepts ZHIHU_PROFILE, ZHIHU_BASE_URL, ZHIHU_AUTH_MODE=browser, ZHIHU_CA_BUNDLE, and ZHIHU_TIMEOUT; write operations, MCP, customer URL delivery, and public npm publication remain unavailable",
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
async function run(parsed) {
|
|
945
|
+
if (parsed.duplicates.size > 0) {
|
|
946
|
+
invalid("Duplicate options are not allowed");
|
|
947
|
+
}
|
|
948
|
+
const [first, second] = parsed.command;
|
|
949
|
+
if (!first || first === "help" || first === "--help")
|
|
950
|
+
return { data: helpData(), exit_code: 0 };
|
|
951
|
+
if (first === "version" || (first === "--version" && !second))
|
|
952
|
+
return { data: versionData(), exit_code: 0 };
|
|
953
|
+
if (first === "doctor") {
|
|
954
|
+
for (const key of parsed.flags.keys()) {
|
|
955
|
+
if (key !== "profile")
|
|
956
|
+
invalid("doctor only accepts --profile");
|
|
957
|
+
}
|
|
958
|
+
const requested = parsed.flags.get("profile");
|
|
959
|
+
if (requested !== undefined && typeof requested !== "string")
|
|
960
|
+
invalid("--profile requires a name");
|
|
961
|
+
return { data: await doctorData(process.env, requested === undefined ? {} : { profile: requested }), exit_code: 0 };
|
|
962
|
+
}
|
|
963
|
+
if (first === "config" && second === "show")
|
|
964
|
+
return { data: showConfig(parsed.flags), exit_code: 0 };
|
|
965
|
+
if (first === "config" && second === "init")
|
|
966
|
+
return { data: await configInit(parsed.flags), exit_code: 0 };
|
|
967
|
+
if (first === "install") {
|
|
968
|
+
if (!second && parsed.command.length === 1) {
|
|
969
|
+
return { data: await installCommand(parsed.flags, parsed.json), exit_code: 0 };
|
|
970
|
+
}
|
|
971
|
+
if (second !== "codex" || parsed.command.length !== 2)
|
|
972
|
+
invalid("Use install or install codex");
|
|
973
|
+
return { data: codexSkillCommand("install", parsed.flags), exit_code: 0 };
|
|
974
|
+
}
|
|
975
|
+
if (first === "uninstall") {
|
|
976
|
+
if (second !== "codex" || parsed.command.length !== 2)
|
|
977
|
+
invalid("Use uninstall codex");
|
|
978
|
+
return { data: codexSkillCommand("uninstall", parsed.flags), exit_code: 0 };
|
|
979
|
+
}
|
|
980
|
+
if (first === "auth")
|
|
981
|
+
return { data: await authCommand(second, parsed.flags), exit_code: 0 };
|
|
982
|
+
if (first === "capabilities") {
|
|
983
|
+
if (parsed.command.length !== 2)
|
|
984
|
+
invalid("Use capabilities list");
|
|
985
|
+
for (const key of parsed.flags.keys()) {
|
|
986
|
+
if (key !== "profile")
|
|
987
|
+
invalid(`Unknown capabilities option: --${key}`);
|
|
988
|
+
}
|
|
989
|
+
return { data: await capabilitiesCommand(second, parsed.flags), exit_code: 0 };
|
|
990
|
+
}
|
|
991
|
+
if (first === "employees") {
|
|
992
|
+
if (second === "list") {
|
|
993
|
+
if (parsed.command.length !== 2)
|
|
994
|
+
invalid("Use employees list");
|
|
995
|
+
for (const key of parsed.flags.keys()) {
|
|
996
|
+
if (key !== "profile")
|
|
997
|
+
invalid(`Unknown employees option: --${key}`);
|
|
998
|
+
}
|
|
999
|
+
return { data: await employeesCommand(second, parsed.flags), exit_code: 0 };
|
|
1000
|
+
}
|
|
1001
|
+
if (second === "chat") {
|
|
1002
|
+
if (parsed.command.length !== 3)
|
|
1003
|
+
invalid("Use employees chat <employee-id> --message <text>");
|
|
1004
|
+
const result = await employeesChatCommand(parsed.command[2], parsed.flags, parsed.json);
|
|
1005
|
+
return { data: result.data, exit_code: 0, streamed: result.streamed };
|
|
1006
|
+
}
|
|
1007
|
+
invalid("Use employees list or employees chat <employee-id> --message <text>");
|
|
1008
|
+
}
|
|
1009
|
+
if (first === "assets") {
|
|
1010
|
+
if (second === "list") {
|
|
1011
|
+
if (parsed.command.length !== 2)
|
|
1012
|
+
invalid("Use assets list");
|
|
1013
|
+
return { data: await assetsCommand(second, parsed.flags), exit_code: 0 };
|
|
1014
|
+
}
|
|
1015
|
+
if (second === "get") {
|
|
1016
|
+
if (parsed.command.length !== 2)
|
|
1017
|
+
invalid("Use assets get --id <id>");
|
|
1018
|
+
return { data: await assetGetCommand(parsed.flags), exit_code: 0 };
|
|
1019
|
+
}
|
|
1020
|
+
invalid("Use assets list or assets get --id <id>");
|
|
1021
|
+
}
|
|
1022
|
+
if (first === "metrics") {
|
|
1023
|
+
if (parsed.command.length !== 2)
|
|
1024
|
+
invalid("Use metrics query --asset <id|ip> --metric <name>");
|
|
1025
|
+
return { data: await metricsCommand(second, parsed.flags), exit_code: 0 };
|
|
1026
|
+
}
|
|
1027
|
+
if (first === "alarms") {
|
|
1028
|
+
if (parsed.command.length !== 2)
|
|
1029
|
+
invalid("Use alarms list or alarms get --id <id>");
|
|
1030
|
+
return { data: await alarmsCommand(second, parsed.flags), exit_code: 0 };
|
|
1031
|
+
}
|
|
1032
|
+
if (first === "incidents") {
|
|
1033
|
+
if (second !== "diagnose" || parsed.command.length !== 2) {
|
|
1034
|
+
invalid("Use incidents diagnose --alarm-id <id>");
|
|
1035
|
+
}
|
|
1036
|
+
const data = await incidentsDiagnoseCommand(parsed.flags);
|
|
1037
|
+
return { data, exit_code: 0, human: incidentHumanSummary(data) };
|
|
1038
|
+
}
|
|
1039
|
+
if (first === "inspections") {
|
|
1040
|
+
if (second !== "run" || parsed.command.length !== 2) {
|
|
1041
|
+
invalid("Use inspections run [--model <name|code|id>] [--window 5m..24h]");
|
|
1042
|
+
}
|
|
1043
|
+
const data = await inspectionsRunCommand(parsed.flags);
|
|
1044
|
+
return { data, exit_code: 0, human: inspectionHumanSummary(data) };
|
|
1045
|
+
}
|
|
1046
|
+
invalid(`Unknown command: ${parsed.command.join(" ")}`);
|
|
1047
|
+
}
|
|
1048
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
1049
|
+
const parsed = parseArguments(argv);
|
|
1050
|
+
const outputContext = context(parsed);
|
|
1051
|
+
try {
|
|
1052
|
+
const result = await run(parsed);
|
|
1053
|
+
if (!result.streamed) {
|
|
1054
|
+
if (!parsed.json && result.human)
|
|
1055
|
+
process.stdout.write(`${result.human}\n`);
|
|
1056
|
+
else
|
|
1057
|
+
emitSuccess(outputContext, result.data);
|
|
1058
|
+
}
|
|
1059
|
+
return result.exit_code;
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
if (error instanceof CliFailure)
|
|
1063
|
+
return emitError(outputContext, error.shape);
|
|
1064
|
+
if (error instanceof ConfigError) {
|
|
1065
|
+
return emitError(outputContext, {
|
|
1066
|
+
code: error.code,
|
|
1067
|
+
message: error.message,
|
|
1068
|
+
retryable: false,
|
|
1069
|
+
exit_code: 2,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (error instanceof CliAuthError) {
|
|
1073
|
+
return emitError(outputContext, {
|
|
1074
|
+
code: error.code,
|
|
1075
|
+
message: error.message,
|
|
1076
|
+
retryable: error.retryable,
|
|
1077
|
+
exit_code: error.exitCode,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
return emitError(outputContext, {
|
|
1081
|
+
code: "PROTOCOL_ERROR",
|
|
1082
|
+
message: "Unable to process the command",
|
|
1083
|
+
retryable: false,
|
|
1084
|
+
exit_code: 8,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1089
|
+
const invokedFile = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
1090
|
+
if (currentFile === invokedFile) {
|
|
1091
|
+
process.exitCode = await main();
|
|
1092
|
+
}
|