@oxygen-agent/cli 1.1011.405 → 1.1011.440
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 +1 -1
- package/dist/command-manifest.js +8 -4
- package/dist/connectors-commands.js +3 -0
- package/dist/contract-flags.js +22 -9
- package/dist/customer-operation-commands.d.ts +5 -0
- package/dist/customer-operation-commands.js +101 -0
- package/dist/database-query-flags.d.ts +2 -0
- package/dist/database-query-flags.js +6 -0
- package/dist/help.d.ts +3 -1
- package/dist/help.js +33 -0
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +41 -1
- package/dist/index.js +470 -174
- package/dist/knowledge-repository-commands.js +12 -14
- package/dist/mailbox-ramp-notice.d.ts +11 -0
- package/dist/mailbox-ramp-notice.js +53 -0
- package/dist/settings-commands.js +5 -4
- package/dist/skills.d.ts +5 -4
- package/dist/skills.js +106 -79
- package/dist/tool-catalog-detail-command.d.ts +7 -0
- package/dist/tool-catalog-detail-command.js +19 -0
- package/dist/tool-catalog-search-window.d.ts +6 -0
- package/dist/tool-catalog-search-window.js +12 -0
- package/dist/transcript.js +1 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +2 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +359 -34
- package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +67 -7
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +12 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +1 -2
- package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -2
- package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +3 -6
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OPERATION_SPECS } from "@oxygen/shared/contracts/operations";
|
|
1
2
|
import { CLI_EXIT_CODE_TABLE, OXYGEN_VERSION, inferUserCapabilityRoute, serializeCapabilityRoute, } from "@oxygen/shared";
|
|
2
3
|
const DEFAULT_COMMAND_SEARCH_LIMIT = 10;
|
|
3
4
|
const MAX_COMMAND_SEARCH_LIMIT = 25;
|
|
@@ -341,6 +342,7 @@ function toManifestEntry(command, path) {
|
|
|
341
342
|
const leafVerb = path[path.length - 1] ?? "";
|
|
342
343
|
const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
|
|
343
344
|
const commandName = path.join(" ");
|
|
345
|
+
const operations = OPERATION_SPECS.filter((spec) => spec.cli.path.join(" ") === commandName);
|
|
344
346
|
return {
|
|
345
347
|
name: commandName,
|
|
346
348
|
group: path[0] ?? "",
|
|
@@ -355,7 +357,9 @@ function toManifestEntry(command, path) {
|
|
|
355
357
|
description: option.description,
|
|
356
358
|
required: option.mandatory,
|
|
357
359
|
})),
|
|
358
|
-
|
|
360
|
+
// Some native commands read without flags and write with an explicit grant.
|
|
361
|
+
// Their metadata must include every available action's authorization posture.
|
|
362
|
+
spends_credits: operations.length ? operations.some((spec) => spec.spends) : (!ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
|
|
359
363
|
(flagStrings.some((flags) => flags.includes("--approved") ||
|
|
360
364
|
flags.includes("--max-credits") ||
|
|
361
365
|
// Legacy Copilot compatibility plus any future explicit budget flag.
|
|
@@ -368,11 +372,11 @@ function toManifestEntry(command, path) {
|
|
|
368
372
|
// cap flag exists for either, so the flag heuristic alone would misreport
|
|
369
373
|
// them as free.
|
|
370
374
|
(path[0] === "copilot" &&
|
|
371
|
-
(leafVerb === "send" || leafVerb === "approve"))),
|
|
372
|
-
mutates: !READ_ONLY_COMMANDS.has(commandName)
|
|
375
|
+
(leafVerb === "send" || leafVerb === "approve")))),
|
|
376
|
+
mutates: operations.length ? operations.some((spec) => spec.mutates) : (!READ_ONLY_COMMANDS.has(commandName)
|
|
373
377
|
&& (MUTATING_COMMANDS.has(commandName)
|
|
374
378
|
|| MUTATING_VERBS.has(leafVerb)
|
|
375
|
-
|| MUTATING_VERBS.has(verbPrefix)),
|
|
379
|
+
|| MUTATING_VERBS.has(verbPrefix))),
|
|
376
380
|
preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
|
|
377
381
|
PREVIEW_BY_DEFAULT_COMMANDS.has(commandName),
|
|
378
382
|
json_supported: flagStrings.some((flags) => flags.includes("--json")),
|
|
@@ -49,6 +49,9 @@ export function buildConnectorUpdateBody(options) {
|
|
|
49
49
|
if (!("default_workspace" in body) && !("credit_ceiling" in body)) {
|
|
50
50
|
throw new OxygenError("invalid_request", "Pass --default-workspace <workspace> (or --clear-default-workspace) to set where the app works, or --credit-ceiling <credits> (or --clear-credit-ceiling) to set what it may spend in 24 hours.", { exitCode: 1 });
|
|
51
51
|
}
|
|
52
|
+
if ("approve_above_default" in body && !("credit_ceiling" in body)) {
|
|
53
|
+
throw new OxygenError("invalid_request", "--approve-above-default only goes with --credit-ceiling (or --clear-credit-ceiling).", { exitCode: 1 });
|
|
54
|
+
}
|
|
52
55
|
return body;
|
|
53
56
|
}
|
|
54
57
|
export function registerConnectorsCommands(program, handle) {
|
package/dist/contract-flags.js
CHANGED
|
@@ -24,7 +24,7 @@ function attribute(flag) {
|
|
|
24
24
|
return flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
|
|
25
25
|
}
|
|
26
26
|
function isObject(property) {
|
|
27
|
-
return "type" in property && property.type === "object"
|
|
27
|
+
return "type" in property && property.type === "object";
|
|
28
28
|
}
|
|
29
29
|
function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = "") {
|
|
30
30
|
const specs = [];
|
|
@@ -37,11 +37,14 @@ function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = ""
|
|
|
37
37
|
const flag = entry.flag ?? `${parentFlag}${kebab(name)}`;
|
|
38
38
|
if (isObject(property)) {
|
|
39
39
|
const fields = Object.values(property.properties ?? {});
|
|
40
|
-
const splat = entry.flatten || (fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
|
|
40
|
+
const splat = entry.flatten || (fields.length > 0 && property.additionalProperties !== true && fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
|
|
41
41
|
if (splat)
|
|
42
42
|
specs.push(...contractFlagSpecs(property.properties ?? {}, overlay, path, entry.flatten ? "" : `${flag}-`));
|
|
43
43
|
else
|
|
44
|
-
specs.push({ kind: "json", path, flag: `${flag}-json`, entry });
|
|
44
|
+
specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
|
|
45
|
+
}
|
|
46
|
+
else if (!("type" in property) || (property.type === "array" && !("type" in property.items && ["string", "number", "integer", "boolean"].includes(property.items.type)))) {
|
|
47
|
+
specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
|
|
45
48
|
}
|
|
46
49
|
else if ("type" in property && property.type === "boolean") {
|
|
47
50
|
specs.push(entry.pair ? { kind: "pair", path, on: entry.pair.on, off: entry.pair.off, entry } : { kind: "boolean", path, flag, entry });
|
|
@@ -97,7 +100,7 @@ export function addContractFlags(command, contract, overlay = {}) {
|
|
|
97
100
|
return command;
|
|
98
101
|
}
|
|
99
102
|
function invalid(flag, value, message, code = "invalid_number") {
|
|
100
|
-
return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode:
|
|
103
|
+
return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 2 });
|
|
101
104
|
}
|
|
102
105
|
function parseScalar(property, raw, flag) {
|
|
103
106
|
if (!("type" in property))
|
|
@@ -121,6 +124,13 @@ function parseScalar(property, raw, flag) {
|
|
|
121
124
|
if (property.type === "string" && property.enum && !property.enum.includes(raw)) {
|
|
122
125
|
throw invalid(flag, raw, `--${flag} must be one of: ${property.enum.join(", ")}.`, "invalid_option");
|
|
123
126
|
}
|
|
127
|
+
if (property.type === "boolean") {
|
|
128
|
+
if (raw === "true")
|
|
129
|
+
return true;
|
|
130
|
+
if (raw === "false")
|
|
131
|
+
return false;
|
|
132
|
+
throw invalid(flag, raw, `--${flag} values must be true or false.`, "invalid_option");
|
|
133
|
+
}
|
|
124
134
|
return raw;
|
|
125
135
|
}
|
|
126
136
|
function readValue(spec, raw) {
|
|
@@ -147,8 +157,8 @@ function setPath(body, path, value) {
|
|
|
147
157
|
function stringOption(options, flag, aliases = []) {
|
|
148
158
|
for (const name of [flag, ...aliases]) {
|
|
149
159
|
const value = options[attribute(name)];
|
|
150
|
-
if (typeof value === "string"
|
|
151
|
-
return value
|
|
160
|
+
if (typeof value === "string")
|
|
161
|
+
return value;
|
|
152
162
|
}
|
|
153
163
|
return undefined;
|
|
154
164
|
}
|
|
@@ -177,11 +187,14 @@ export function readContractFlags(options, contract, overlay = {}) {
|
|
|
177
187
|
parsed = JSON.parse(raw);
|
|
178
188
|
}
|
|
179
189
|
catch {
|
|
180
|
-
throw invalid(spec.flag, raw, `--${spec.flag} must be
|
|
190
|
+
throw invalid(spec.flag, raw, `--${spec.flag} must be valid JSON.`, "invalid_json");
|
|
181
191
|
}
|
|
182
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
192
|
+
if ("type" in spec.property && spec.property.type === "object" && (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))) {
|
|
183
193
|
throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
|
|
184
194
|
}
|
|
195
|
+
if ("type" in spec.property && spec.property.type === "array" && !Array.isArray(parsed)) {
|
|
196
|
+
throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON array.`, "invalid_json");
|
|
197
|
+
}
|
|
185
198
|
setPath(body, spec.path, parsed);
|
|
186
199
|
given.add(spec.flag);
|
|
187
200
|
}
|
|
@@ -198,7 +211,7 @@ export function readContractFlags(options, contract, overlay = {}) {
|
|
|
198
211
|
}
|
|
199
212
|
else if (options[attribute(spec.flag)] === true) {
|
|
200
213
|
if (given.has(spec.target)) {
|
|
201
|
-
throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode:
|
|
214
|
+
throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 2 });
|
|
202
215
|
}
|
|
203
216
|
setPath(body, spec.path, null);
|
|
204
217
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/** Register each domain's selected operation as a native command with exact typed flags. */
|
|
3
|
+
export declare function registerCustomerOperationCommands(program: Command, handle: (command: string, options: {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}, action: () => Promise<unknown>) => Promise<void>): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, resolve } from "node:path";
|
|
3
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
4
|
+
import { CLI_OPERATIONS, operationFormData, operationInputContract, operationRequest } from "@oxygen/shared/contracts/operations";
|
|
5
|
+
import { unwrapNullable, validateContract } from "@oxygen/shared/contracts/schema";
|
|
6
|
+
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
7
|
+
import { requestOxygen } from "./http-client.js";
|
|
8
|
+
/** Register each domain's selected operation as a native command with exact typed flags. */
|
|
9
|
+
export function registerCustomerOperationCommands(program, handle) {
|
|
10
|
+
for (const spec of CLI_OPERATIONS) {
|
|
11
|
+
let parent = program;
|
|
12
|
+
const words = [...spec.cli.path];
|
|
13
|
+
const leaf = words.pop();
|
|
14
|
+
for (const word of words) {
|
|
15
|
+
let child = parent.commands.find((command) => command.name() === word);
|
|
16
|
+
if (!child)
|
|
17
|
+
child = parent.command(word).description(`${word} workspace operations.`);
|
|
18
|
+
parent = child;
|
|
19
|
+
}
|
|
20
|
+
// Never hide an existing owner behind a second Commander command.
|
|
21
|
+
if (parent.commands.some((command) => command.name() === leaf)) {
|
|
22
|
+
throw new Error(`Duplicate native operation command ${spec.cli.path.join(" ")}`);
|
|
23
|
+
}
|
|
24
|
+
const contract = operationInputContract(spec);
|
|
25
|
+
const command = addContractFlags(parent.command(leaf).description(spec.cli.summary), contract)
|
|
26
|
+
.option("--json", "Print a JSON envelope.");
|
|
27
|
+
if (spec.binary)
|
|
28
|
+
command.requiredOption("--output <path>", "Write the exact downloaded bytes to this local file.");
|
|
29
|
+
if (spec.multipart)
|
|
30
|
+
command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path.");
|
|
31
|
+
for (const name of contract.required ?? []) {
|
|
32
|
+
const property = contract.properties[name];
|
|
33
|
+
const flag = `--${name.replace(/_/g, "-")}`;
|
|
34
|
+
const option = command.options.find((candidate) => candidate.long === flag || candidate.long === `${flag}-json`);
|
|
35
|
+
const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField].includes(name);
|
|
36
|
+
if (multipartAlternative) {
|
|
37
|
+
if (option)
|
|
38
|
+
option.description = `${option.description} Required unless supplied by --file.`.trim();
|
|
39
|
+
}
|
|
40
|
+
else if (unwrapNullable(property).nullable) {
|
|
41
|
+
if (option)
|
|
42
|
+
option.description = `${option.description} Required: use this flag or --clear-${name.replace(/_/g, "-")}.`.trim();
|
|
43
|
+
}
|
|
44
|
+
else if (option) {
|
|
45
|
+
// The negative boolean spelling uses the same attribute, so an explicit
|
|
46
|
+
// false satisfies this requirement just as true does.
|
|
47
|
+
option.makeOptionMandatory();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
|
|
51
|
+
const input = readContractFlags(options, contract);
|
|
52
|
+
if (spec.multipart && options.file !== undefined) {
|
|
53
|
+
if (input[spec.multipart.contentField] !== undefined)
|
|
54
|
+
throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
|
|
55
|
+
const filePath = String(options.file);
|
|
56
|
+
input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
|
|
57
|
+
input[spec.multipart.nameField] ??= basename(filePath);
|
|
58
|
+
input[spec.multipart.typeField] ??= uploadContentType(filePath);
|
|
59
|
+
}
|
|
60
|
+
const validation = validateContract(contract, input, { unknown: "reject", overMax: "reject" });
|
|
61
|
+
if (!validation.ok)
|
|
62
|
+
throw new OxygenError("invalid_request", validation.issues.map((issue) => issue.message).join(" "), { exitCode: 2 });
|
|
63
|
+
const request = operationRequest(spec, input);
|
|
64
|
+
if (spec.binary) {
|
|
65
|
+
if (spec.binary.stream)
|
|
66
|
+
return requestOxygen(request.path, { method: request.method, binaryOutputPath: resolve(String(options.output)) });
|
|
67
|
+
const bytes = await requestOxygen(request.path, { method: request.method, binaryMaxBytes: spec.binary.maxBytes });
|
|
68
|
+
const output = resolve(String(options.output));
|
|
69
|
+
try {
|
|
70
|
+
await writeFile(output, bytes, { flag: "wx" });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
74
|
+
throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return { path: output, bytes: bytes.byteLength };
|
|
79
|
+
}
|
|
80
|
+
if (spec.multipart) {
|
|
81
|
+
let formData;
|
|
82
|
+
try {
|
|
83
|
+
formData = operationFormData(spec, request.body ?? {});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new OxygenError("invalid_request", error instanceof Error ? error.message : "Invalid upload.", { exitCode: 2 });
|
|
87
|
+
}
|
|
88
|
+
return requestOxygen(request.path, { method: request.method, formData });
|
|
89
|
+
}
|
|
90
|
+
return requestOxygen(request.path, { method: request.method, ...(request.body ? { body: request.body } : {}) });
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function uploadContentType(filePath) {
|
|
95
|
+
const types = {
|
|
96
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
97
|
+
".csv": "text/csv", ".json": "application/json", ".jsonl": "application/x-ndjson",
|
|
98
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
99
|
+
};
|
|
100
|
+
return types[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
101
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readPositiveInt } from "./cli-values.js";
|
|
2
|
+
export const DATABASE_QUERY_FLAG_OVERLAY = {
|
|
3
|
+
sql: { omit: true }, // Keep the existing required --sql option.
|
|
4
|
+
max_rows: { valueName: "<n>", parse: readPositiveInt }, // Preserve legacy coercion/defaults.
|
|
5
|
+
max_chars: { valueName: "<n>" },
|
|
6
|
+
};
|
package/dist/help.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { CommanderError, type Command } from "commander";
|
|
2
2
|
import { type CliFailure } from "@oxygen/shared/cli-result";
|
|
3
3
|
export declare function applyOxygenHelp(program: Command, binaryName: string): void;
|
|
4
4
|
/**
|
|
@@ -12,6 +12,8 @@ export declare function applyOxygenHelp(program: Command, binaryName: string): v
|
|
|
12
12
|
* `installCommanderExitOverride` recurses.
|
|
13
13
|
*/
|
|
14
14
|
export declare function enableCommandSuggestions(command: Command): void;
|
|
15
|
+
/** A declared flag following a value option is a missing value, not literal data. */
|
|
16
|
+
export declare function validateRequiredOptionValues(program: Command, selected: Command): void;
|
|
15
17
|
/**
|
|
16
18
|
* The extra line printed after `error: unknown command '<x>'`.
|
|
17
19
|
*
|
package/dist/help.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommanderError } from "commander";
|
|
1
2
|
import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
|
|
2
3
|
import { exitCodeForErrorCode, failure } from "@oxygen/shared/cli-result";
|
|
3
4
|
// Grouped `--help` for the ~70 top-level command groups: commander's default
|
|
@@ -181,6 +182,38 @@ export function enableCommandSuggestions(command) {
|
|
|
181
182
|
for (const child of command.commands)
|
|
182
183
|
enableCommandSuggestions(child);
|
|
183
184
|
}
|
|
185
|
+
/** A declared flag following a value option is a missing value, not literal data. */
|
|
186
|
+
export function validateRequiredOptionValues(program, selected) {
|
|
187
|
+
const options = new Map();
|
|
188
|
+
for (let current = selected; current; current = current.parent) {
|
|
189
|
+
for (const option of current.options) {
|
|
190
|
+
if (option.long && !options.has(option.long))
|
|
191
|
+
options.set(option.long, option);
|
|
192
|
+
if (option.short && !options.has(option.short))
|
|
193
|
+
options.set(option.short, option);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Installed Commander writes this exact property before command dispatch;
|
|
197
|
+
// its public typings omit it. Validate only that property at this seam.
|
|
198
|
+
const rawArgs = Reflect.get(program, "rawArgs");
|
|
199
|
+
if (!Array.isArray(rawArgs) || !rawArgs.every((value) => typeof value === "string")) {
|
|
200
|
+
throw new CommanderError(2, "commander.invalidArgument", "Cannot validate the command's original option arguments.");
|
|
201
|
+
}
|
|
202
|
+
const argv = rawArgs;
|
|
203
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
204
|
+
const token = argv[index];
|
|
205
|
+
if (token === "--")
|
|
206
|
+
break;
|
|
207
|
+
const option = options.get(token);
|
|
208
|
+
if (!option?.required)
|
|
209
|
+
continue;
|
|
210
|
+
const next = argv[index + 1];
|
|
211
|
+
if (next === undefined || next === "--" || options.has(next.split("=")[0])) {
|
|
212
|
+
throw new CommanderError(2, "commander.optionMissingArgument", `option '${option.flags}' argument missing. To use an option name as literal text, pass ${option.long ?? option.short}=<value>.`);
|
|
213
|
+
}
|
|
214
|
+
index += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
184
217
|
/**
|
|
185
218
|
* Verbs a user reaches for that this CLI spells differently.
|
|
186
219
|
*
|
package/dist/http-client.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type CliEndpoint } from "./runtime.js";
|
|
|
3
3
|
type RequestOptions = {
|
|
4
4
|
/** Internal bounded file download; never follows a caller-supplied URL. */
|
|
5
5
|
binaryMaxBytes?: number;
|
|
6
|
+
/** Stream a native authenticated download to an exclusively created file. */
|
|
7
|
+
binaryOutputPath?: string;
|
|
6
8
|
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
7
9
|
body?: Record<string, unknown>;
|
|
8
10
|
formData?: FormData;
|
package/dist/http-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { open, unlink } from "node:fs/promises";
|
|
3
4
|
import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
|
|
4
5
|
import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
|
|
5
6
|
import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
|
|
@@ -86,7 +87,7 @@ path, options = {}) {
|
|
|
86
87
|
const requestInit = {
|
|
87
88
|
method: options.method ?? "GET",
|
|
88
89
|
headers,
|
|
89
|
-
...(options.binaryMaxBytes === undefined ? {} : { redirect: "error" }),
|
|
90
|
+
...(options.binaryMaxBytes === undefined && options.binaryOutputPath === undefined ? {} : { redirect: "error" }),
|
|
90
91
|
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
|
91
92
|
...(options.formData ? { body: options.formData } : {}),
|
|
92
93
|
};
|
|
@@ -97,6 +98,9 @@ path, options = {}) {
|
|
|
97
98
|
requestInit.signal = timeout.signal;
|
|
98
99
|
try {
|
|
99
100
|
response = await fetchImpl(`${apiUrl}${path}`, requestInit);
|
|
101
|
+
if (response.ok && options.binaryOutputPath !== undefined) {
|
|
102
|
+
return await streamBinaryResponse(response, options.binaryOutputPath);
|
|
103
|
+
}
|
|
100
104
|
if (response.ok && options.binaryMaxBytes !== undefined) {
|
|
101
105
|
return await readBoundedBinaryResponse(response, options.binaryMaxBytes);
|
|
102
106
|
}
|
|
@@ -195,6 +199,42 @@ async function readBoundedBinaryResponse(response, maxBytes) {
|
|
|
195
199
|
}
|
|
196
200
|
return bytes;
|
|
197
201
|
}
|
|
202
|
+
async function streamBinaryResponse(response, output) {
|
|
203
|
+
let file;
|
|
204
|
+
try {
|
|
205
|
+
file = await open(output, "wx");
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
await response.body?.cancel();
|
|
209
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
210
|
+
throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
const reader = response.body?.getReader();
|
|
215
|
+
let bytes = 0;
|
|
216
|
+
try {
|
|
217
|
+
if (!reader)
|
|
218
|
+
throw new OxygenError("invalid_response", "The download had no body.");
|
|
219
|
+
for (;;) {
|
|
220
|
+
const chunk = await reader.read();
|
|
221
|
+
if (chunk.done)
|
|
222
|
+
break;
|
|
223
|
+
await file.writeFile(chunk.value);
|
|
224
|
+
bytes += chunk.value.byteLength;
|
|
225
|
+
}
|
|
226
|
+
await file.close();
|
|
227
|
+
return { path: output, bytes };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await file.close().catch(() => { });
|
|
231
|
+
await unlink(output).catch(() => { });
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await reader?.cancel().catch(() => { });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
198
238
|
export async function ensureFreshCliForApiUrl(apiUrl, options = {}) {
|
|
199
239
|
const checkKey = `${apiUrl}|${OXYGEN_VERSION}`;
|
|
200
240
|
if (cliCompatibilityCheckKeys.has(checkKey))
|