@oxygen-agent/cli 1.64.5 → 1.99.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 +1 -1
- package/dist/browser-login.js +1 -1
- package/dist/credentials.d.ts +13 -5
- package/dist/credentials.js +114 -19
- package/dist/http-client.d.ts +5 -1
- package/dist/http-client.js +23 -2
- package/dist/index.js +1417 -306
- package/dist/skills.d.ts +8 -2
- package/dist/skills.js +67 -17
- package/dist/update.js +2 -5
- package/dist/windows-shim.d.ts +7 -0
- package/dist/windows-shim.js +21 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +156 -6
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +115 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +91 -0
- package/node_modules/@oxygen/workflows/dist/index.js +232 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// skipcq: JS-0271 — bin entry source; build chmod+x on dist/index.js
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { basename, dirname, extname, resolve } from "node:path";
|
|
@@ -14,7 +14,7 @@ import { inferRowsFileFormat, normalizeRowsForNewTable, normalizeRowsFormat, par
|
|
|
14
14
|
import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
|
|
15
15
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
16
16
|
import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
|
|
17
|
-
import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, resolveActiveProfile, saveCredentials, switchCredentialProfile, } from "./credentials.js";
|
|
17
|
+
import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
18
18
|
import { requestOxygen } from "./http-client.js";
|
|
19
19
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
20
20
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
@@ -112,6 +112,21 @@ function parseJsonArray(value) {
|
|
|
112
112
|
}
|
|
113
113
|
return parsed;
|
|
114
114
|
}
|
|
115
|
+
function readCustomIntegrationManifest(options) {
|
|
116
|
+
const manifestPath = readOption(options.manifest);
|
|
117
|
+
const manifestJson = readOption(options.manifestJson);
|
|
118
|
+
if (manifestPath && manifestJson) {
|
|
119
|
+
throw new OxygenError("invalid_request", "Pass either --manifest or --manifest-json, not both.", {
|
|
120
|
+
exitCode: 1,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (!manifestPath && !manifestJson) {
|
|
124
|
+
throw new OxygenError("invalid_request", "Pass --manifest or --manifest-json.", {
|
|
125
|
+
exitCode: 1,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return parseJsonObject(manifestJson ?? readFileSync(resolve(manifestPath ?? ""), "utf8"));
|
|
129
|
+
}
|
|
115
130
|
function readFileIfPresent(value) {
|
|
116
131
|
const candidate = resolve(value);
|
|
117
132
|
try {
|
|
@@ -161,13 +176,17 @@ export function createProgram() {
|
|
|
161
176
|
.name("oxygen")
|
|
162
177
|
.description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
|
|
163
178
|
.version(OXYGEN_VERSION)
|
|
164
|
-
.option("--profile <name>", "Use a stored CLI profile for this command.")
|
|
179
|
+
.option("--profile <name>", "Use a stored CLI profile for this command.")
|
|
180
|
+
.option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
|
|
165
181
|
program.hook("preAction", () => {
|
|
166
182
|
const options = program.opts();
|
|
167
183
|
if (options.profile) {
|
|
168
184
|
globalProfileFlag = options.profile;
|
|
169
185
|
process.env.OXYGEN_PROFILE = options.profile;
|
|
170
186
|
}
|
|
187
|
+
if (options.org) {
|
|
188
|
+
process.env.OXYGEN_ORG = options.org;
|
|
189
|
+
}
|
|
171
190
|
});
|
|
172
191
|
program
|
|
173
192
|
.command("login")
|
|
@@ -284,6 +303,22 @@ export function createProgram() {
|
|
|
284
303
|
.action(async (options) => {
|
|
285
304
|
await handleWhoamiAction(options);
|
|
286
305
|
});
|
|
306
|
+
program
|
|
307
|
+
.command("onboarding")
|
|
308
|
+
.description("Onboarding helpers.")
|
|
309
|
+
.addCommand(new Command("start")
|
|
310
|
+
.description("Start onboarding and get the skill to load.")
|
|
311
|
+
.option("--json", "Print a JSON envelope.")
|
|
312
|
+
.action(async (options) => {
|
|
313
|
+
await handleOnboardingStartAction(options);
|
|
314
|
+
}))
|
|
315
|
+
.addCommand(new Command("reset")
|
|
316
|
+
.description("Clear the onboarding_started_at marker so onboarding re-runs from scratch. Company context data is preserved.")
|
|
317
|
+
.option("--confirm", "Required: confirm the reset. Without this flag the command refuses to run.")
|
|
318
|
+
.option("--json", "Print a JSON envelope.")
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
await handleOnboardingResetAction(options);
|
|
321
|
+
}));
|
|
287
322
|
program
|
|
288
323
|
.command("status")
|
|
289
324
|
.description("Compare the local Oxygen CLI version against what's deployed in prod.")
|
|
@@ -308,16 +343,20 @@ export function createProgram() {
|
|
|
308
343
|
.option("--json", "Print a JSON envelope.")
|
|
309
344
|
.action(async (options) => {
|
|
310
345
|
await handleAsyncAction("orgs list", options, async () => requestOxygen("/api/cli/orgs"));
|
|
346
|
+
}))
|
|
347
|
+
.addCommand(new Command("use")
|
|
348
|
+
.description("Select the active organization for this CLI profile.")
|
|
349
|
+
.argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
|
|
350
|
+
.option("--json", "Print a JSON envelope.")
|
|
351
|
+
.action(async (organization, options) => {
|
|
352
|
+
await handleOrgUseAction(organization, options, "orgs use");
|
|
311
353
|
}))
|
|
312
354
|
.addCommand(new Command("select")
|
|
313
|
-
.description("
|
|
355
|
+
.description("Alias for orgs use.")
|
|
314
356
|
.argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
|
|
315
357
|
.option("--json", "Print a JSON envelope.")
|
|
316
358
|
.action(async (organization, options) => {
|
|
317
|
-
await
|
|
318
|
-
method: "POST",
|
|
319
|
-
body: { organization },
|
|
320
|
-
}));
|
|
359
|
+
await handleOrgUseAction(organization, options, "orgs select");
|
|
321
360
|
}));
|
|
322
361
|
program
|
|
323
362
|
.command("db")
|
|
@@ -557,11 +596,13 @@ export function createProgram() {
|
|
|
557
596
|
await handleAsyncAction("tables export-bundle", options, async () => exportTableBundle(table, options));
|
|
558
597
|
}))
|
|
559
598
|
.addCommand(new Command("import-bundle")
|
|
560
|
-
.description("Recreate a workspace table from an export-bundle file in this org. Restores columns (incl. enrichment/tool definitions) and inserts every row.")
|
|
599
|
+
.description("Recreate a workspace table from an export-bundle file in this org. Restores columns (incl. enrichment/tool definitions) and inserts every row. Pass --key to make the import idempotent (re-runnable), and --into to resume a failed import into the table it already created.")
|
|
561
600
|
.requiredOption("--file <path>", "Bundle JSON file produced by `tables export-bundle`.")
|
|
562
|
-
.option("--name <name>", "Override the table display name. Defaults to the bundle's table name.")
|
|
563
|
-
.option("--project <project>", "Project id or slug for the new table. Defaults to General.")
|
|
564
|
-
.option("--
|
|
601
|
+
.option("--name <name>", "Override the table display name. Defaults to the bundle's table name. Ignored with --into.")
|
|
602
|
+
.option("--project <project>", "Project id or slug for the new table. Defaults to General. Ignored with --into.")
|
|
603
|
+
.option("--into <table>", "Resume into an existing table (id or slug) instead of creating a new one. Requires --key. Use the table id printed by the failed run.")
|
|
604
|
+
.option("--key <column>", "Upsert key column. When set, rows are matched by this key instead of blindly inserted, so re-running the import is idempotent (no duplicates). Required with --into.")
|
|
605
|
+
.option("--batch-size <n>", "Rows per write request. Defaults to 500; paid orgs may use up to 5000.")
|
|
565
606
|
.option("--json", "Print a JSON envelope.")
|
|
566
607
|
.action(async (options) => {
|
|
567
608
|
await handleAsyncAction("tables import-bundle", options, async () => importTableBundle(options));
|
|
@@ -805,7 +846,238 @@ export function createProgram() {
|
|
|
805
846
|
}));
|
|
806
847
|
})));
|
|
807
848
|
program
|
|
808
|
-
.command("
|
|
849
|
+
.command("blueprints")
|
|
850
|
+
.description("Portable Oxygen blueprints: bundle a workflow + tables + columns + prompts as shareable JSON.")
|
|
851
|
+
.addCommand(new Command("list")
|
|
852
|
+
.description("List Oxygen blueprints visible in this workspace (seeds + saved).")
|
|
853
|
+
.argument("[query]", "Search text.")
|
|
854
|
+
.option("--tag <tag>", "Filter by tag.")
|
|
855
|
+
.option("--include-seeds", "Include the shipped seed blueprints (default true).")
|
|
856
|
+
.option("--json", "Print a JSON envelope.")
|
|
857
|
+
.action(async (query, options) => {
|
|
858
|
+
await handleAsyncAction("blueprints list", options, () => {
|
|
859
|
+
const params = new URLSearchParams();
|
|
860
|
+
if (query)
|
|
861
|
+
params.set("query", query);
|
|
862
|
+
const tag = readOption(options.tag);
|
|
863
|
+
if (tag)
|
|
864
|
+
params.set("tag", tag);
|
|
865
|
+
if (options.includeSeeds === false)
|
|
866
|
+
params.set("include_seeds", "false");
|
|
867
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
868
|
+
return requestOxygen(`/api/cli/blueprints${qs}`);
|
|
869
|
+
});
|
|
870
|
+
}))
|
|
871
|
+
.addCommand(new Command("describe")
|
|
872
|
+
.description("Describe one blueprint (seed or saved) by slug.")
|
|
873
|
+
.argument("<slug>", "Blueprint slug.")
|
|
874
|
+
.option("--json", "Print a JSON envelope.")
|
|
875
|
+
.action(async (slug, options) => {
|
|
876
|
+
await handleAsyncAction("blueprints describe", options, () => requestOxygen("/api/cli/blueprints/get", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
body: { slug },
|
|
879
|
+
}));
|
|
880
|
+
}))
|
|
881
|
+
.addCommand(new Command("export")
|
|
882
|
+
.description("Build a portable blueprint JSON from an existing workflow.")
|
|
883
|
+
.requiredOption("--workflow <id>", "Workflow id or slug to export.")
|
|
884
|
+
.option("--tables <ids>", "Comma-separated list of table ids/slugs to bundle.")
|
|
885
|
+
.option("--prompts <slugs>", "Comma-separated list of prompt template slugs to bundle.")
|
|
886
|
+
.option("--blueprint-id <slug>", "Override the output blueprint slug.")
|
|
887
|
+
.option("--blueprint-name <name>", "Override the output blueprint name.")
|
|
888
|
+
.option("--blueprint-summary <text>", "Override the output blueprint summary.")
|
|
889
|
+
.option("--blueprint-tags <tags>", "Comma-separated tags for the exported blueprint.")
|
|
890
|
+
.option("--out <path>", "Write the exported envelope to this file path.")
|
|
891
|
+
.option("--json", "Print a JSON envelope.")
|
|
892
|
+
.action(async (options) => {
|
|
893
|
+
await handleAsyncAction("blueprints export", options, async () => {
|
|
894
|
+
const body = { workflow_id: options.workflow };
|
|
895
|
+
const tables = readOption(options.tables);
|
|
896
|
+
if (tables)
|
|
897
|
+
body.table_ids = splitCsv(tables);
|
|
898
|
+
const prompts = readOption(options.prompts);
|
|
899
|
+
if (prompts)
|
|
900
|
+
body.prompt_slugs = splitCsv(prompts);
|
|
901
|
+
const blueprintId = readOption(options.blueprintId);
|
|
902
|
+
if (blueprintId)
|
|
903
|
+
body.blueprint_id = blueprintId;
|
|
904
|
+
const blueprintName = readOption(options.blueprintName);
|
|
905
|
+
if (blueprintName)
|
|
906
|
+
body.blueprint_name = blueprintName;
|
|
907
|
+
const blueprintSummary = readOption(options.blueprintSummary);
|
|
908
|
+
if (blueprintSummary)
|
|
909
|
+
body.blueprint_summary = blueprintSummary;
|
|
910
|
+
const blueprintTags = readOption(options.blueprintTags);
|
|
911
|
+
if (blueprintTags)
|
|
912
|
+
body.blueprint_tags = splitCsv(blueprintTags);
|
|
913
|
+
const result = await requestOxygen("/api/cli/blueprints/export", {
|
|
914
|
+
method: "POST",
|
|
915
|
+
body,
|
|
916
|
+
});
|
|
917
|
+
const outPath = readOption(options.out);
|
|
918
|
+
if (outPath) {
|
|
919
|
+
const fs = await import("node:fs/promises");
|
|
920
|
+
const blueprint = result.blueprint;
|
|
921
|
+
await fs.writeFile(outPath, JSON.stringify(blueprint, null, 2), "utf8");
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
});
|
|
925
|
+
}))
|
|
926
|
+
.addCommand(new Command("preflight")
|
|
927
|
+
.description("Preflight a blueprint (slug, local file, or shared URL) against this workspace.")
|
|
928
|
+
.argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
|
|
929
|
+
.option("--file <path>", "Read a blueprint envelope from a local JSON file.")
|
|
930
|
+
.option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
|
|
931
|
+
.option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
|
|
932
|
+
.option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
|
|
933
|
+
.option("--json", "Print a JSON envelope.")
|
|
934
|
+
.action(async (slug, options) => {
|
|
935
|
+
await handleAsyncAction("blueprints preflight", options, async () => {
|
|
936
|
+
const body = await buildBlueprintRequestBody(slug, options);
|
|
937
|
+
return requestOxygen("/api/cli/blueprints/preflight", { method: "POST", body });
|
|
938
|
+
});
|
|
939
|
+
}))
|
|
940
|
+
.addCommand(new Command("apply")
|
|
941
|
+
.description("Apply a blueprint to this workspace. Creates tables, columns, prompts, and a disabled workflow.")
|
|
942
|
+
.argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
|
|
943
|
+
.option("--file <path>", "Read a blueprint envelope from a local JSON file.")
|
|
944
|
+
.option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
|
|
945
|
+
.option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
|
|
946
|
+
.option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
|
|
947
|
+
.option("--workflow-id <id>", "Override the resulting workflow id.")
|
|
948
|
+
.option("--workflow-name <name>", "Override the resulting workflow name.")
|
|
949
|
+
.option("--json", "Print a JSON envelope.")
|
|
950
|
+
.action(async (slug, options) => {
|
|
951
|
+
await handleAsyncAction("blueprints apply", options, async () => {
|
|
952
|
+
const body = await buildBlueprintRequestBody(slug, options);
|
|
953
|
+
const workflowId = readOption(options.workflowId);
|
|
954
|
+
if (workflowId)
|
|
955
|
+
body.workflow_id = workflowId;
|
|
956
|
+
const workflowName = readOption(options.workflowName);
|
|
957
|
+
if (workflowName)
|
|
958
|
+
body.workflow_name = workflowName;
|
|
959
|
+
return requestOxygen("/api/cli/blueprints/apply", { method: "POST", body });
|
|
960
|
+
});
|
|
961
|
+
}))
|
|
962
|
+
.addCommand(new Command("save")
|
|
963
|
+
.description("Persist a blueprint envelope into this workspace's gallery.")
|
|
964
|
+
.option("--file <path>", "Read a blueprint envelope from a local JSON file.")
|
|
965
|
+
.option("--slug <slug>", "Override the stored blueprint slug.")
|
|
966
|
+
.option("--name <name>", "Override the stored blueprint name.")
|
|
967
|
+
.option("--summary <text>", "Override the stored blueprint summary.")
|
|
968
|
+
.option("--json", "Print a JSON envelope.")
|
|
969
|
+
.action(async (options) => {
|
|
970
|
+
await handleAsyncAction("blueprints save", options, async () => {
|
|
971
|
+
const filePath = readOption(options.file);
|
|
972
|
+
if (!filePath)
|
|
973
|
+
throw new Error("--file is required for blueprints save");
|
|
974
|
+
const fs = await import("node:fs/promises");
|
|
975
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
976
|
+
const envelope = JSON.parse(raw);
|
|
977
|
+
const body = { envelope };
|
|
978
|
+
const slug = readOption(options.slug);
|
|
979
|
+
if (slug)
|
|
980
|
+
body.slug = slug;
|
|
981
|
+
const name = readOption(options.name);
|
|
982
|
+
if (name)
|
|
983
|
+
body.name = name;
|
|
984
|
+
const summary = readOption(options.summary);
|
|
985
|
+
if (summary)
|
|
986
|
+
body.summary = summary;
|
|
987
|
+
return requestOxygen("/api/cli/blueprints/save", { method: "POST", body });
|
|
988
|
+
});
|
|
989
|
+
}))
|
|
990
|
+
.addCommand(new Command("archive")
|
|
991
|
+
.description("Archive a saved blueprint (seed blueprints cannot be archived).")
|
|
992
|
+
.argument("<slug>", "Blueprint slug or id.")
|
|
993
|
+
.option("--json", "Print a JSON envelope.")
|
|
994
|
+
.action(async (slug, options) => {
|
|
995
|
+
await handleAsyncAction("blueprints archive", options, () => requestOxygen("/api/cli/blueprints/archive", { method: "POST", body: { slug } }));
|
|
996
|
+
}))
|
|
997
|
+
.addCommand(new Command("share")
|
|
998
|
+
.description("Create a public share URL for a saved blueprint (oxygen-agent.com/b/<code>).")
|
|
999
|
+
.argument("<slug>", "Blueprint slug or id to share.")
|
|
1000
|
+
.option("--expires-in-days <days>", "Optional expiry (default: no expiry).")
|
|
1001
|
+
.option("--disabled", "Create the share in disabled state.")
|
|
1002
|
+
.option("--json", "Print a JSON envelope.")
|
|
1003
|
+
.action(async (slug, options) => {
|
|
1004
|
+
await handleAsyncAction("blueprints share", options, () => {
|
|
1005
|
+
const body = { slug };
|
|
1006
|
+
const expiresInDays = readOption(options.expiresInDays);
|
|
1007
|
+
if (expiresInDays)
|
|
1008
|
+
body.expires_in_days = Number(expiresInDays);
|
|
1009
|
+
if (options.disabled)
|
|
1010
|
+
body.enabled = false;
|
|
1011
|
+
return requestOxygen("/api/cli/blueprints/share", { method: "POST", body });
|
|
1012
|
+
});
|
|
1013
|
+
}))
|
|
1014
|
+
.addCommand(new Command("shares")
|
|
1015
|
+
.description("List existing share URLs for this workspace.")
|
|
1016
|
+
.option("--json", "Print a JSON envelope.")
|
|
1017
|
+
.action(async (options) => {
|
|
1018
|
+
await handleAsyncAction("blueprints shares", options, () => requestOxygen("/api/cli/blueprints/share"));
|
|
1019
|
+
}))
|
|
1020
|
+
.addCommand(new Command("unshare")
|
|
1021
|
+
.description("Revoke a public share URL by short code.")
|
|
1022
|
+
.argument("<code>", "Share short code (the slug after /b/ in the URL).")
|
|
1023
|
+
.option("--json", "Print a JSON envelope.")
|
|
1024
|
+
.action(async (code, options) => {
|
|
1025
|
+
await handleAsyncAction("blueprints unshare", options, () => requestOxygen("/api/cli/blueprints/share/revoke", {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
body: { short_code: code },
|
|
1028
|
+
}));
|
|
1029
|
+
}))
|
|
1030
|
+
.addCommand(new Command("publish")
|
|
1031
|
+
.description("List a blueprint on the public Oxygen marketplace. Auto-creates a share URL if none exists.")
|
|
1032
|
+
.argument("<slug>", "Blueprint slug to publish.")
|
|
1033
|
+
.option("--category <name>", "Optional free-text category (e.g. outbound, enrichment).")
|
|
1034
|
+
.option("--kind <kind>", "Listing kind: community (default) or official (Oxygen-curated; gated).")
|
|
1035
|
+
.option("--approved", "Acknowledge the publish-safety warnings (required if the preview is not clean).")
|
|
1036
|
+
.option("--json", "Print a JSON envelope.")
|
|
1037
|
+
.action(async (slug, options) => {
|
|
1038
|
+
await handleAsyncAction("blueprints publish", options, () => {
|
|
1039
|
+
const body = { slug };
|
|
1040
|
+
const category = readOption(options.category);
|
|
1041
|
+
if (category)
|
|
1042
|
+
body.category = category;
|
|
1043
|
+
const kind = readOption(options.kind);
|
|
1044
|
+
if (kind)
|
|
1045
|
+
body.kind = kind;
|
|
1046
|
+
if (options.approved)
|
|
1047
|
+
body.approved = true;
|
|
1048
|
+
return requestOxygen("/api/cli/blueprints/publish", { method: "POST", body });
|
|
1049
|
+
});
|
|
1050
|
+
}))
|
|
1051
|
+
.addCommand(new Command("unpublish")
|
|
1052
|
+
.description("Remove a blueprint from the public marketplace. The share URL stays valid for direct visitors.")
|
|
1053
|
+
.argument("<slug>", "Blueprint slug to unlist.")
|
|
1054
|
+
.option("--json", "Print a JSON envelope.")
|
|
1055
|
+
.action(async (slug, options) => {
|
|
1056
|
+
await handleAsyncAction("blueprints unpublish", options, () => requestOxygen("/api/cli/blueprints/unpublish", {
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
body: { slug },
|
|
1059
|
+
}));
|
|
1060
|
+
}))
|
|
1061
|
+
.addCommand(new Command("marketplace")
|
|
1062
|
+
.description("Browse the public Oxygen blueprint marketplace.")
|
|
1063
|
+
.option("--category <name>", "Filter by listing category.")
|
|
1064
|
+
.option("--kind <kind>", "Filter by listing kind: community or official.")
|
|
1065
|
+
.option("--json", "Print a JSON envelope.")
|
|
1066
|
+
.action(async (options) => {
|
|
1067
|
+
await handleAsyncAction("blueprints marketplace", options, () => {
|
|
1068
|
+
const params = new URLSearchParams();
|
|
1069
|
+
const category = readOption(options.category);
|
|
1070
|
+
if (category)
|
|
1071
|
+
params.set("category", category);
|
|
1072
|
+
const kind = readOption(options.kind);
|
|
1073
|
+
if (kind)
|
|
1074
|
+
params.set("kind", kind);
|
|
1075
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1076
|
+
return requestOxygen(`/api/blueprints/marketplace${qs}`, { requireAuth: false });
|
|
1077
|
+
});
|
|
1078
|
+
}));
|
|
1079
|
+
program
|
|
1080
|
+
.command("prompts")
|
|
809
1081
|
.description("Reusable prompt templates layered into AI columns at run time.")
|
|
810
1082
|
.addCommand(new Command("list")
|
|
811
1083
|
.description("List prompt templates in the workspace.")
|
|
@@ -813,10 +1085,85 @@ export function createProgram() {
|
|
|
813
1085
|
.option("--include-archived", "Include archived templates.")
|
|
814
1086
|
.option("--json", "Print a JSON envelope.")
|
|
815
1087
|
.action(async (options) => {
|
|
816
|
-
await handleAsyncAction("
|
|
1088
|
+
await handleAsyncAction("prompts list", options, () => {
|
|
817
1089
|
const params = new URLSearchParams();
|
|
1090
|
+
const kind = readOption(options.kind);
|
|
1091
|
+
if (kind)
|
|
1092
|
+
params.set("kind", kind);
|
|
1093
|
+
if (options.includeArchived)
|
|
1094
|
+
params.set("include_archived", "true");
|
|
1095
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1096
|
+
return requestOxygen(`/api/cli/templates${qs}`);
|
|
1097
|
+
});
|
|
1098
|
+
}))
|
|
1099
|
+
.addCommand(new Command("get")
|
|
1100
|
+
.description("Read one prompt template by id or slug.")
|
|
1101
|
+
.argument("<id_or_slug>", "Template UUID or slug.")
|
|
1102
|
+
.option("--json", "Print a JSON envelope.")
|
|
1103
|
+
.action(async (idOrSlug, options) => {
|
|
1104
|
+
await handleAsyncAction("prompts get", options, () => requestOxygen("/api/cli/templates/get", {
|
|
1105
|
+
method: "POST",
|
|
1106
|
+
body: idOrSlug.includes("-") && idOrSlug.length >= 32
|
|
1107
|
+
? { id: idOrSlug }
|
|
1108
|
+
: { slug: idOrSlug },
|
|
1109
|
+
}));
|
|
1110
|
+
}))
|
|
1111
|
+
.addCommand(new Command("upsert")
|
|
1112
|
+
.description("Create or update a prompt template.")
|
|
1113
|
+
.option("--id <id>", "Existing template UUID to update. Omit to create.")
|
|
1114
|
+
.option("--slug <slug>", "Stable slug (kebab-case).")
|
|
1115
|
+
.option("--name <name>", "Human-readable name.")
|
|
1116
|
+
.option("--description <text>", "Short description.")
|
|
1117
|
+
.option("--kind <kind>", "Template kind: ai_column_system, scoring_rubric, or other.")
|
|
1118
|
+
.option("--body <text>", "Prompt body.")
|
|
1119
|
+
.option("--body-file <path>", "Path to a file containing the prompt body.")
|
|
1120
|
+
.option("--json", "Print a JSON envelope.")
|
|
1121
|
+
.action(async (options) => {
|
|
1122
|
+
await handleAsyncAction("prompts upsert", options, async () => {
|
|
1123
|
+
const body = {};
|
|
1124
|
+
if (readOption(options.id))
|
|
1125
|
+
body.id = readOption(options.id);
|
|
1126
|
+
if (readOption(options.slug))
|
|
1127
|
+
body.slug = readOption(options.slug);
|
|
1128
|
+
if (readOption(options.name))
|
|
1129
|
+
body.name = readOption(options.name);
|
|
1130
|
+
if (readOption(options.description) !== undefined)
|
|
1131
|
+
body.description = readOption(options.description);
|
|
818
1132
|
if (readOption(options.kind))
|
|
819
|
-
|
|
1133
|
+
body.kind = readOption(options.kind);
|
|
1134
|
+
if (readOption(options.body))
|
|
1135
|
+
body.body = readOption(options.body);
|
|
1136
|
+
else {
|
|
1137
|
+
const path = readOption(options.bodyFile);
|
|
1138
|
+
if (path) {
|
|
1139
|
+
const fs = await import("node:fs/promises");
|
|
1140
|
+
body.body = await fs.readFile(path, "utf8");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
|
|
1144
|
+
});
|
|
1145
|
+
}))
|
|
1146
|
+
.addCommand(new Command("archive")
|
|
1147
|
+
.description("Archive a prompt template. Seeded templates cannot be archived.")
|
|
1148
|
+
.argument("<id>", "Template UUID.")
|
|
1149
|
+
.option("--json", "Print a JSON envelope.")
|
|
1150
|
+
.action(async (id, options) => {
|
|
1151
|
+
await handleAsyncAction("prompts archive", options, () => requestOxygen("/api/cli/templates/archive", { method: "POST", body: { id } }));
|
|
1152
|
+
}));
|
|
1153
|
+
program
|
|
1154
|
+
.command("templates")
|
|
1155
|
+
.description("Deprecated alias for 'oxygen prompts'. Will be removed in a future release.")
|
|
1156
|
+
.addCommand(new Command("list")
|
|
1157
|
+
.description("List prompt templates in the workspace.")
|
|
1158
|
+
.option("--kind <kind>", "Filter by ai_column_system, scoring_rubric, or other.")
|
|
1159
|
+
.option("--include-archived", "Include archived templates.")
|
|
1160
|
+
.option("--json", "Print a JSON envelope.")
|
|
1161
|
+
.action(async (options) => {
|
|
1162
|
+
await handleAsyncAction("templates list", options, () => {
|
|
1163
|
+
const params = new URLSearchParams();
|
|
1164
|
+
const kind = readOption(options.kind);
|
|
1165
|
+
if (kind)
|
|
1166
|
+
params.set("kind", kind);
|
|
820
1167
|
if (options.includeArchived)
|
|
821
1168
|
params.set("include_archived", "true");
|
|
822
1169
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -860,10 +1207,12 @@ export function createProgram() {
|
|
|
860
1207
|
body.kind = readOption(options.kind);
|
|
861
1208
|
if (readOption(options.body))
|
|
862
1209
|
body.body = readOption(options.body);
|
|
863
|
-
else
|
|
1210
|
+
else {
|
|
864
1211
|
const path = readOption(options.bodyFile);
|
|
865
|
-
|
|
866
|
-
|
|
1212
|
+
if (path) {
|
|
1213
|
+
const fs = await import("node:fs/promises");
|
|
1214
|
+
body.body = await fs.readFile(path, "utf8");
|
|
1215
|
+
}
|
|
867
1216
|
}
|
|
868
1217
|
return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
|
|
869
1218
|
});
|
|
@@ -885,14 +1234,17 @@ export function createProgram() {
|
|
|
885
1234
|
.option("--limit <n>", "Max rows to return (default 50, max 200).")
|
|
886
1235
|
.option("--json", "Print a JSON envelope.")
|
|
887
1236
|
.action(async (options) => {
|
|
888
|
-
await handleAsyncAction("reviews list", options,
|
|
1237
|
+
await handleAsyncAction("reviews list", options, () => {
|
|
889
1238
|
const params = new URLSearchParams();
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1239
|
+
const status = readOption(options.status);
|
|
1240
|
+
const table = readOption(options.table);
|
|
1241
|
+
const limit = readOption(options.limit);
|
|
1242
|
+
if (status)
|
|
1243
|
+
params.set("status", status);
|
|
1244
|
+
if (table)
|
|
1245
|
+
params.set("table_id", table);
|
|
1246
|
+
if (limit)
|
|
1247
|
+
params.set("limit", limit);
|
|
896
1248
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
897
1249
|
return requestOxygen(`/api/cli/message-reviews${qs}`);
|
|
898
1250
|
});
|
|
@@ -902,10 +1254,11 @@ export function createProgram() {
|
|
|
902
1254
|
.option("--table <table_id>", "Filter by table id.")
|
|
903
1255
|
.option("--json", "Print a JSON envelope.")
|
|
904
1256
|
.action(async (options) => {
|
|
905
|
-
await handleAsyncAction("reviews next", options,
|
|
1257
|
+
await handleAsyncAction("reviews next", options, () => {
|
|
906
1258
|
const params = new URLSearchParams();
|
|
907
|
-
|
|
908
|
-
|
|
1259
|
+
const table = readOption(options.table);
|
|
1260
|
+
if (table)
|
|
1261
|
+
params.set("table_id", table);
|
|
909
1262
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
910
1263
|
return requestOxygen(`/api/cli/message-reviews/next${qs}`);
|
|
911
1264
|
});
|
|
@@ -927,10 +1280,11 @@ export function createProgram() {
|
|
|
927
1280
|
.option("--auto-rerun", "Trigger a single-row rerun of the column after rejecting.")
|
|
928
1281
|
.option("--json", "Print a JSON envelope.")
|
|
929
1282
|
.action(async (reviewId, options) => {
|
|
930
|
-
await handleAsyncAction("reviews reject", options,
|
|
1283
|
+
await handleAsyncAction("reviews reject", options, () => {
|
|
931
1284
|
const body = { id: reviewId, decision: "reject" };
|
|
932
|
-
|
|
933
|
-
|
|
1285
|
+
const highlightsJson = readOption(options.highlightsJson);
|
|
1286
|
+
if (highlightsJson) {
|
|
1287
|
+
body.highlights = JSON.parse(highlightsJson);
|
|
934
1288
|
}
|
|
935
1289
|
if (options.autoRerun)
|
|
936
1290
|
body.auto_rerun = true;
|
|
@@ -1039,21 +1393,21 @@ export function createProgram() {
|
|
|
1039
1393
|
...(localConcurrency ? { concurrency: localConcurrency } : {}),
|
|
1040
1394
|
});
|
|
1041
1395
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1396
|
+
const body = {
|
|
1397
|
+
table,
|
|
1398
|
+
column,
|
|
1399
|
+
...(options.all ? { selection: { mode: "all" } } : {}),
|
|
1400
|
+
...(filterSelection ? { selection: filterSelection } : {}),
|
|
1401
|
+
...(!options.all && readOption(options.rowId) ? { row_id: readOption(options.rowId) } : {}),
|
|
1402
|
+
...(!options.all && !filterSelection && limit ? { limit } : {}),
|
|
1403
|
+
...(options.force ? { force: true } : {}),
|
|
1404
|
+
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
1405
|
+
...(options.background ? { background: true } : {}),
|
|
1406
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1407
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1408
|
+
};
|
|
1409
|
+
return requestColumnsRun(body, table, {
|
|
1410
|
+
background: Boolean(options.background),
|
|
1057
1411
|
});
|
|
1058
1412
|
});
|
|
1059
1413
|
}))
|
|
@@ -1121,10 +1475,11 @@ export function createProgram() {
|
|
|
1121
1475
|
.argument("<column>", "Column id or key.")
|
|
1122
1476
|
.option("--label <label>", "New display label.")
|
|
1123
1477
|
.option("--semantic-type <type>", "New semantic type.")
|
|
1124
|
-
.option("--definition-json <json>", "Definition metadata to merge into the column.")
|
|
1478
|
+
.option("--definition-json <json>", "Definition metadata to shallow-merge into the column; arrays are replaced wholesale and missing keys leave existing fields unchanged.")
|
|
1479
|
+
.option("--dry-run", "Return the would-be merged definition without writing.")
|
|
1125
1480
|
.option("--json", "Print a JSON envelope.")
|
|
1126
1481
|
.action(async (table, column, options) => {
|
|
1127
|
-
await handleAsyncAction("columns update", options,
|
|
1482
|
+
await handleAsyncAction("columns update", options, () => requestOxygen("/api/cli/tables/columns/update", {
|
|
1128
1483
|
method: "POST",
|
|
1129
1484
|
body: {
|
|
1130
1485
|
table,
|
|
@@ -1132,6 +1487,7 @@ export function createProgram() {
|
|
|
1132
1487
|
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
1133
1488
|
...(readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
|
|
1134
1489
|
...(options.definitionJson ? { definition: parseJsonObject(options.definitionJson) } : {}),
|
|
1490
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
1135
1491
|
},
|
|
1136
1492
|
}));
|
|
1137
1493
|
}))
|
|
@@ -1234,10 +1590,19 @@ export function createProgram() {
|
|
|
1234
1590
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
1235
1591
|
},
|
|
1236
1592
|
}));
|
|
1593
|
+
}))
|
|
1594
|
+
.addCommand(new Command("list")
|
|
1595
|
+
.description("List durable table action runs for one table.")
|
|
1596
|
+
.requiredOption("--table <table>", "Table id or slug.")
|
|
1597
|
+
.option("--status <status>", "Filter by active, queued, running, paused, completed, completed_with_errors, failed, canceling, or canceled. Defaults to active.")
|
|
1598
|
+
.option("--limit <n>", "Maximum runs to return. Defaults to 20.")
|
|
1599
|
+
.option("--json", "Print a JSON envelope.")
|
|
1600
|
+
.action(async (options) => {
|
|
1601
|
+
await handleAsyncAction("table-runs list", options, () => requestOxygen(tableRunsListPath(options)));
|
|
1237
1602
|
}))
|
|
1238
1603
|
.addCommand(new Command("get")
|
|
1239
1604
|
.description("Get one durable table action run.")
|
|
1240
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1605
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1241
1606
|
.option("--json", "Print a JSON envelope.")
|
|
1242
1607
|
.action(async (runId, options) => {
|
|
1243
1608
|
await handleAsyncAction("table-runs get", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`));
|
|
@@ -1262,7 +1627,7 @@ export function createProgram() {
|
|
|
1262
1627
|
}))
|
|
1263
1628
|
.addCommand(new Command("wait")
|
|
1264
1629
|
.description("Poll a durable table action run until it finishes.")
|
|
1265
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1630
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1266
1631
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
1267
1632
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
|
|
1268
1633
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -1278,7 +1643,7 @@ export function createProgram() {
|
|
|
1278
1643
|
}))
|
|
1279
1644
|
.addCommand(new Command("cancel")
|
|
1280
1645
|
.description("Request cancellation for a durable table action run.")
|
|
1281
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1646
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1282
1647
|
.option("--json", "Print a JSON envelope.")
|
|
1283
1648
|
.action(async (runId, options) => {
|
|
1284
1649
|
await handleAsyncAction("table-runs cancel", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/cancel`, {
|
|
@@ -1532,7 +1897,7 @@ export function createProgram() {
|
|
|
1532
1897
|
});
|
|
1533
1898
|
}))
|
|
1534
1899
|
.addCommand(new Command("repair")
|
|
1535
|
-
.description("Repair stale background action and
|
|
1900
|
+
.description("Repair stale background action, ingestion, and workflow queue state.")
|
|
1536
1901
|
.option("--json", "Print a JSON envelope.")
|
|
1537
1902
|
.action(async (options) => {
|
|
1538
1903
|
await handleAsyncAction("worker repair", options, async () => requestOxygen("/api/cli/worker/repair", {
|
|
@@ -1828,6 +2193,36 @@ export function createProgram() {
|
|
|
1828
2193
|
.action(async (options) => {
|
|
1829
2194
|
await handleAsyncAction("session usage", options, async () => getSessionUsage({ sessionId: readOption(options.sessionId) }));
|
|
1830
2195
|
}));
|
|
2196
|
+
program
|
|
2197
|
+
.command("custom-integrations")
|
|
2198
|
+
.description("Custom HTTP integration commands.")
|
|
2199
|
+
.addCommand(new Command("apply")
|
|
2200
|
+
.description("Create or update a custom HTTP integration manifest. Store credentials from the web Connections page.")
|
|
2201
|
+
.option("--manifest <path>", "Path to a custom HTTP manifest JSON file.")
|
|
2202
|
+
.option("--manifest-json <json>", "Custom HTTP manifest as a JSON object.")
|
|
2203
|
+
.option("--json", "Print a JSON envelope.")
|
|
2204
|
+
.action(async (options) => {
|
|
2205
|
+
await handleAsyncAction("custom-integrations apply", options, () => {
|
|
2206
|
+
const manifest = readCustomIntegrationManifest(options);
|
|
2207
|
+
return requestOxygen("/api/cli/custom-integrations", {
|
|
2208
|
+
method: "POST",
|
|
2209
|
+
body: { manifest },
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
}))
|
|
2213
|
+
.addCommand(new Command("list")
|
|
2214
|
+
.description("List custom HTTP integrations for the active organization.")
|
|
2215
|
+
.option("--slug <slug>", "Filter to one custom integration slug.")
|
|
2216
|
+
.option("--json", "Print a JSON envelope.")
|
|
2217
|
+
.action(async (options) => {
|
|
2218
|
+
await handleAsyncAction("custom-integrations list", options, () => {
|
|
2219
|
+
const params = new URLSearchParams();
|
|
2220
|
+
if (readOption(options.slug))
|
|
2221
|
+
params.set("slug", readOption(options.slug) ?? "");
|
|
2222
|
+
const suffix = params.toString();
|
|
2223
|
+
return requestOxygen(`/api/cli/custom-integrations${suffix ? `?${suffix}` : ""}`);
|
|
2224
|
+
});
|
|
2225
|
+
}));
|
|
1831
2226
|
program
|
|
1832
2227
|
.command("tools")
|
|
1833
2228
|
.description("Tool catalog commands.")
|
|
@@ -1858,11 +2253,48 @@ export function createProgram() {
|
|
|
1858
2253
|
.action(async (toolId, options) => {
|
|
1859
2254
|
await handleAsyncAction("tools get", options, async () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
|
|
1860
2255
|
}))
|
|
2256
|
+
.addCommand(new Command("enums")
|
|
2257
|
+
.description("Provider enum catalogs for fields that accept normalized values.")
|
|
2258
|
+
.addCommand(new Command("list")
|
|
2259
|
+
.description("List enum catalogs for a provider (or all providers).")
|
|
2260
|
+
.option("--provider <provider>", "Filter to a single provider, e.g. blitzapi.")
|
|
2261
|
+
.option("--json", "Print a JSON envelope.")
|
|
2262
|
+
.action(async (options) => {
|
|
2263
|
+
await handleAsyncAction("tools enums list", options, () => {
|
|
2264
|
+
const params = new URLSearchParams();
|
|
2265
|
+
const provider = readOption(options.provider);
|
|
2266
|
+
if (provider)
|
|
2267
|
+
params.set("provider", provider);
|
|
2268
|
+
const suffix = params.toString();
|
|
2269
|
+
return requestOxygen(`/api/cli/tools/enums${suffix ? `?${suffix}` : ""}`);
|
|
2270
|
+
});
|
|
2271
|
+
}))
|
|
2272
|
+
.addCommand(new Command("get")
|
|
2273
|
+
.description("Fetch the accepted values for one provider enum catalog. Use this before drafting payloads with enumRef fields.")
|
|
2274
|
+
.argument("<provider>", "Provider name, e.g. blitzapi.")
|
|
2275
|
+
.argument("<enum_ref>", "Enum reference id, e.g. industry, employee_range, country_code.")
|
|
2276
|
+
.option("-q, --query <query>", "Case-insensitive substring or token search across the catalog values.")
|
|
2277
|
+
.option("--limit <limit>", "Maximum number of values to return. Defaults to 100. Maximum 1000.")
|
|
2278
|
+
.option("--json", "Print a JSON envelope.")
|
|
2279
|
+
.action(async (provider, enumRef, options) => {
|
|
2280
|
+
await handleAsyncAction("tools enums get", options, () => {
|
|
2281
|
+
const params = new URLSearchParams();
|
|
2282
|
+
const query = readOption(options.query);
|
|
2283
|
+
const limit = readOption(options.limit);
|
|
2284
|
+
if (query)
|
|
2285
|
+
params.set("q", query);
|
|
2286
|
+
if (limit)
|
|
2287
|
+
params.set("limit", limit);
|
|
2288
|
+
const suffix = params.toString();
|
|
2289
|
+
return requestOxygen(`/api/cli/tools/enums/${encodeURIComponent(provider)}/${encodeURIComponent(enumRef)}${suffix ? `?${suffix}` : ""}`);
|
|
2290
|
+
});
|
|
2291
|
+
})))
|
|
1861
2292
|
.addCommand(new Command("run")
|
|
1862
2293
|
.description("Run an executable Oxygen tool through the HTTPS API.")
|
|
1863
2294
|
.argument("<tool_id>", "Tool id.")
|
|
1864
2295
|
.requiredOption("--input-json <json>", "Tool input as a JSON object.")
|
|
1865
2296
|
.option("--mode <mode>", "Execution mode: live, dry-run, or dry_run. Mutating tools default to dry-run.")
|
|
2297
|
+
.option("--credential-mode <mode>", "Credential mode: managed, user_api_key, or user_oauth.")
|
|
1866
2298
|
.option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
1867
2299
|
.option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
1868
2300
|
.option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the org's active default connection.")
|
|
@@ -1872,12 +2304,13 @@ export function createProgram() {
|
|
|
1872
2304
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
1873
2305
|
.option("--json", "Print a JSON envelope.")
|
|
1874
2306
|
.action(async (toolId, options) => {
|
|
1875
|
-
await handleAsyncAction("tools run", options,
|
|
2307
|
+
await handleAsyncAction("tools run", options, () => requestOxygen("/api/cli/tools/run", {
|
|
1876
2308
|
method: "POST",
|
|
1877
2309
|
body: {
|
|
1878
2310
|
tool_id: toolId,
|
|
1879
2311
|
input: parseJsonObject(options.inputJson),
|
|
1880
2312
|
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
2313
|
+
...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
|
|
1881
2314
|
...(readOption(options.org) ? { org_id: readOption(options.org) } : {}),
|
|
1882
2315
|
...(readOption(options.orgId) ? { org_id: readOption(options.orgId) } : {}),
|
|
1883
2316
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
@@ -1906,7 +2339,7 @@ export function createProgram() {
|
|
|
1906
2339
|
.option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
|
|
1907
2340
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
1908
2341
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
1909
|
-
.option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact.")
|
|
2342
|
+
.option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
|
|
1910
2343
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
|
|
1911
2344
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
1912
2345
|
.option("--limit <n>", "Rows to estimate. Defaults to 10.")
|
|
@@ -1937,7 +2370,7 @@ export function createProgram() {
|
|
|
1937
2370
|
.option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
|
|
1938
2371
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
1939
2372
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
1940
|
-
.option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact.")
|
|
2373
|
+
.option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
|
|
1941
2374
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
|
|
1942
2375
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
1943
2376
|
.option("--limit <n>", "Rows to queue.")
|
|
@@ -1960,6 +2393,25 @@ export function createProgram() {
|
|
|
1960
2393
|
},
|
|
1961
2394
|
}));
|
|
1962
2395
|
}));
|
|
2396
|
+
program
|
|
2397
|
+
.command("enrichment")
|
|
2398
|
+
.description("High-level enrichment column definition helpers.")
|
|
2399
|
+
.addCommand(new Command("apply-default-cascade")
|
|
2400
|
+
.description("Patch an existing enrichment column to the server-side default provider cascade for its intent.")
|
|
2401
|
+
.argument("<table>", "Table id or slug.")
|
|
2402
|
+
.argument("<column>", "Enrichment column id or key.")
|
|
2403
|
+
.option("--dry-run", "Return the would-be merged definition without writing.")
|
|
2404
|
+
.option("--json", "Print a JSON envelope.")
|
|
2405
|
+
.action(async (table, column, options) => {
|
|
2406
|
+
await handleAsyncAction("enrichment apply-default-cascade", options, () => requestOxygen("/api/cli/enrichment/apply-default-cascade", {
|
|
2407
|
+
method: "POST",
|
|
2408
|
+
body: {
|
|
2409
|
+
table,
|
|
2410
|
+
column,
|
|
2411
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
2412
|
+
},
|
|
2413
|
+
}));
|
|
2414
|
+
}));
|
|
1963
2415
|
program
|
|
1964
2416
|
.command("integrations")
|
|
1965
2417
|
.description("Integration connection and event trigger commands.")
|
|
@@ -2035,6 +2487,33 @@ export function createProgram() {
|
|
|
2035
2487
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
2036
2488
|
},
|
|
2037
2489
|
}));
|
|
2490
|
+
}))
|
|
2491
|
+
.addCommand(new Command("deliveries")
|
|
2492
|
+
.description("List recent provider webhook deliveries (Composio triggers, HubSpot webhooks, etc.) and the workflow runs they kicked off.")
|
|
2493
|
+
.option("--source <source>", "Filter by event source, such as hubspot or composio.gmail.")
|
|
2494
|
+
.option("--event <event>", "Filter by event type, such as contact.created or GMAIL_NEW_GMAIL_MESSAGE.")
|
|
2495
|
+
.option("--toolkit <id>", "Filter by integration/toolkit id, such as gmail or hubspot.")
|
|
2496
|
+
.option("--connection-id <connection_id>", "Filter by a specific integration connection id.")
|
|
2497
|
+
.option("--result <result>", "Filter by delivery result: success or error.")
|
|
2498
|
+
.option("--limit <n>", "Maximum deliveries to return (1-200). Defaults to 50.")
|
|
2499
|
+
.option("--json", "Print a JSON envelope.")
|
|
2500
|
+
.action(async (options) => {
|
|
2501
|
+
await handleAsyncAction("integrations events deliveries", options, () => {
|
|
2502
|
+
const query = new URLSearchParams();
|
|
2503
|
+
const set = (key, value) => {
|
|
2504
|
+
const trimmed = readOption(value);
|
|
2505
|
+
if (trimmed)
|
|
2506
|
+
query.set(key, trimmed);
|
|
2507
|
+
};
|
|
2508
|
+
set("source", options.source);
|
|
2509
|
+
set("event", options.event);
|
|
2510
|
+
set("toolkit", options.toolkit);
|
|
2511
|
+
set("connection_id", options.connectionId);
|
|
2512
|
+
set("result", options.result);
|
|
2513
|
+
set("limit", options.limit);
|
|
2514
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2515
|
+
return requestOxygen(`/api/cli/integrations/events/deliveries${suffix}`);
|
|
2516
|
+
});
|
|
2038
2517
|
})))
|
|
2039
2518
|
.addCommand(new Command("list")
|
|
2040
2519
|
.description("List supported Composio integrations and this org's connections.")
|
|
@@ -2062,11 +2541,16 @@ export function createProgram() {
|
|
|
2062
2541
|
.addCommand(new Command("disconnect")
|
|
2063
2542
|
.description("Disconnect a Composio integration.")
|
|
2064
2543
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
2544
|
+
.option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
|
|
2065
2545
|
.option("--json", "Print a JSON envelope.")
|
|
2066
2546
|
.action(async (integrationId, options) => {
|
|
2067
|
-
|
|
2547
|
+
const connectionId = readOption(options.connectionId)?.trim();
|
|
2548
|
+
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/composio/disconnect", {
|
|
2068
2549
|
method: "POST",
|
|
2069
|
-
body: {
|
|
2550
|
+
body: {
|
|
2551
|
+
integration_id: integrationId,
|
|
2552
|
+
...(connectionId ? { connection_id: connectionId } : {}),
|
|
2553
|
+
},
|
|
2070
2554
|
}));
|
|
2071
2555
|
}))
|
|
2072
2556
|
.addCommand(new Command("actions")
|
|
@@ -2335,6 +2819,7 @@ export function createProgram() {
|
|
|
2335
2819
|
.addCommand(new Command("failures")
|
|
2336
2820
|
.description("List failed workflow runs and trigger scheduler failures.")
|
|
2337
2821
|
.option("--limit <n>", "Maximum failures per group. Defaults to 25; server cap is 100.")
|
|
2822
|
+
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
2338
2823
|
.option("--json", "Print a JSON envelope.")
|
|
2339
2824
|
.action(async (options) => {
|
|
2340
2825
|
await handleAsyncAction("workflows failures", options, async () => {
|
|
@@ -2342,8 +2827,10 @@ export function createProgram() {
|
|
|
2342
2827
|
const limit = readPositiveInt(options.limit);
|
|
2343
2828
|
if (limit)
|
|
2344
2829
|
query.set("limit", String(limit));
|
|
2830
|
+
if (options.includeBundle)
|
|
2831
|
+
query.set("include_bundle", "true");
|
|
2345
2832
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2346
|
-
return requestOxygen(`/api/cli/workflows/failures${suffix}`);
|
|
2833
|
+
return prepareWorkflowCliOutput(await requestOxygen(`/api/cli/workflows/failures${suffix}`), options);
|
|
2347
2834
|
});
|
|
2348
2835
|
}))
|
|
2349
2836
|
.addCommand(new Command("run")
|
|
@@ -2420,7 +2907,7 @@ export function createProgram() {
|
|
|
2420
2907
|
.option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
|
|
2421
2908
|
.option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
|
|
2422
2909
|
.option("--project", "Install into the current project instead of global agent scope.")
|
|
2423
|
-
.option("--copy", "Copy skill files instead of symlinking when supported by npx skills.")
|
|
2910
|
+
.option("--copy", "Copy skill files instead of symlinking when supported by npx skills. Default on Windows, where symlinks need Developer Mode or admin.")
|
|
2424
2911
|
.option("--json", "Print a JSON envelope.")
|
|
2425
2912
|
.action(async (options) => {
|
|
2426
2913
|
await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
|
|
@@ -2598,11 +3085,11 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
|
|
|
2598
3085
|
esModuleInterop: true,
|
|
2599
3086
|
},
|
|
2600
3087
|
}).outputText;
|
|
2601
|
-
const
|
|
3088
|
+
const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
|
|
3089
|
+
const workflowsUrl = await resolveWorkflowRuntimeModuleUrl(dir);
|
|
2602
3090
|
const rewritten = transpiled
|
|
2603
3091
|
.replaceAll("from \"@oxygen/workflows\"", `from "${workflowsUrl}"`)
|
|
2604
3092
|
.replaceAll("from '@oxygen/workflows'", `from "${workflowsUrl}"`);
|
|
2605
|
-
const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
|
|
2606
3093
|
const compiledPath = `${dir}/workflow.mjs`;
|
|
2607
3094
|
writeFileSync(compiledPath, rewritten, "utf8");
|
|
2608
3095
|
try {
|
|
@@ -2612,6 +3099,84 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
|
|
|
2612
3099
|
rmSync(dir, { recursive: true, force: true });
|
|
2613
3100
|
}
|
|
2614
3101
|
}
|
|
3102
|
+
async function resolveWorkflowRuntimeModuleUrl(tempDir) {
|
|
3103
|
+
try {
|
|
3104
|
+
return await import.meta.resolve("@oxygen/workflows");
|
|
3105
|
+
}
|
|
3106
|
+
catch {
|
|
3107
|
+
const shimPath = `${tempDir}/oxygen-workflows-shim.mjs`;
|
|
3108
|
+
writeFileSync(shimPath, workflowRuntimeShimSource(), "utf8");
|
|
3109
|
+
return pathToFileURL(shimPath).href;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
function workflowRuntimeShimSource() {
|
|
3113
|
+
return `
|
|
3114
|
+
export function defineWorkflow(input) {
|
|
3115
|
+
return {
|
|
3116
|
+
__oxygen_workflow_definition: true,
|
|
3117
|
+
id: input.id,
|
|
3118
|
+
name: input.name,
|
|
3119
|
+
...(input.status ? { status: input.status } : {}),
|
|
3120
|
+
...(input.specification ? { specification: input.specification } : {}),
|
|
3121
|
+
...(input.trigger ? { trigger: input.trigger } : {}),
|
|
3122
|
+
...(input.inputSchema ? { inputSchema: input.inputSchema } : {}),
|
|
3123
|
+
steps: input.steps,
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
export function apiTrigger(input = {}) {
|
|
3127
|
+
return { type: "api", ...(input.status ? { status: input.status } : {}) };
|
|
3128
|
+
}
|
|
3129
|
+
export function webhookTrigger(input) {
|
|
3130
|
+
return {
|
|
3131
|
+
type: "webhook",
|
|
3132
|
+
trigger_id: input.id,
|
|
3133
|
+
...(input.name !== undefined ? { trigger_name: input.name } : {}),
|
|
3134
|
+
secret_required: input.secret ?? true,
|
|
3135
|
+
...(input.idempotencyKeyPath !== undefined ? { idempotency_key_path: input.idempotencyKeyPath } : {}),
|
|
3136
|
+
...(input.status ? { status: input.status } : {}),
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
export function cronTrigger(input) {
|
|
3140
|
+
return {
|
|
3141
|
+
type: "cron",
|
|
3142
|
+
cron: input.cron,
|
|
3143
|
+
...(input.timezone !== undefined ? { timezone: input.timezone } : {}),
|
|
3144
|
+
...(input.status ? { status: input.status } : {}),
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
export function eventTrigger(input) {
|
|
3148
|
+
return {
|
|
3149
|
+
type: "event",
|
|
3150
|
+
source: input.source,
|
|
3151
|
+
event: input.event,
|
|
3152
|
+
...(input.filters ? { filters: input.filters } : {}),
|
|
3153
|
+
...(input.idempotencyKeyPath !== undefined ? { idempotency_key_path: input.idempotencyKeyPath } : {}),
|
|
3154
|
+
...(input.status ? { status: input.status } : {}),
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
export function transformStep(input) {
|
|
3158
|
+
return {
|
|
3159
|
+
__oxygen_workflow_step: true,
|
|
3160
|
+
kind: "transform",
|
|
3161
|
+
id: input.id,
|
|
3162
|
+
...(input.description ? { description: input.description } : {}),
|
|
3163
|
+
run: input.run,
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
export function toolStep(input) {
|
|
3167
|
+
return {
|
|
3168
|
+
__oxygen_workflow_step: true,
|
|
3169
|
+
kind: "tool",
|
|
3170
|
+
id: input.id,
|
|
3171
|
+
...(input.description ? { description: input.description } : {}),
|
|
3172
|
+
tool: input.tool,
|
|
3173
|
+
...(input.effect ? { effect: input.effect } : {}),
|
|
3174
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
3175
|
+
payload: input.payload,
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
`;
|
|
3179
|
+
}
|
|
2615
3180
|
async function tailWorkflowRun(runId, options) {
|
|
2616
3181
|
const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
|
|
2617
3182
|
?? WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS;
|
|
@@ -2640,12 +3205,23 @@ async function tailWorkflowRun(runId, options) {
|
|
|
2640
3205
|
}
|
|
2641
3206
|
const remainingMs = deadline - Date.now();
|
|
2642
3207
|
if (remainingMs <= 0) {
|
|
3208
|
+
const worker = isRecord(run.worker) ? run.worker : null;
|
|
3209
|
+
const queuedWithoutWorker = status === "queued"
|
|
3210
|
+
&& worker !== null
|
|
3211
|
+
&& worker.active === null
|
|
3212
|
+
&& worker.lastClaim === null;
|
|
2643
3213
|
throw new OxygenError("workflow_tail_timeout", "Timed out waiting for workflow run to finish.", {
|
|
2644
3214
|
details: {
|
|
2645
3215
|
workflow_run_id: runId,
|
|
2646
3216
|
status: status ?? null,
|
|
2647
3217
|
timeout_seconds: timeoutSeconds,
|
|
2648
3218
|
polls,
|
|
3219
|
+
...(queuedWithoutWorker
|
|
3220
|
+
? {
|
|
3221
|
+
worker_status: "queued_no_worker",
|
|
3222
|
+
guidance: "No worker has claimed this workflow run. Check `oxygen worker queue-stats --json` for BullMQ and workflow queue health.",
|
|
3223
|
+
}
|
|
3224
|
+
: {}),
|
|
2649
3225
|
},
|
|
2650
3226
|
exitCode: 1,
|
|
2651
3227
|
});
|
|
@@ -2745,23 +3321,89 @@ function readTableRunSelection(options) {
|
|
|
2745
3321
|
exitCode: 1,
|
|
2746
3322
|
});
|
|
2747
3323
|
}
|
|
2748
|
-
function
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
3324
|
+
function tableRunsListPath(options) {
|
|
3325
|
+
const table = readOption(options.table);
|
|
3326
|
+
if (!table) {
|
|
3327
|
+
throw new OxygenError("invalid_table_run", "Pass --table <table>.", {
|
|
3328
|
+
exitCode: 1,
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
const query = new URLSearchParams({ table });
|
|
3332
|
+
const status = readOption(options.status);
|
|
3333
|
+
const limit = readPositiveInt(options.limit);
|
|
3334
|
+
if (status)
|
|
3335
|
+
query.set("status", status);
|
|
3336
|
+
if (limit)
|
|
3337
|
+
query.set("limit", String(limit));
|
|
3338
|
+
if (options.traceId)
|
|
3339
|
+
query.set("trace_id", options.traceId);
|
|
3340
|
+
return `/api/cli/table-action-runs?${query.toString()}`;
|
|
3341
|
+
}
|
|
3342
|
+
async function requestColumnsRun(body, table, options) {
|
|
3343
|
+
const traceId = randomUUID();
|
|
3344
|
+
try {
|
|
3345
|
+
return await requestOxygen("/api/cli/tables/columns/run", {
|
|
3346
|
+
method: "POST",
|
|
3347
|
+
body,
|
|
3348
|
+
traceId,
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
catch (error) {
|
|
3352
|
+
if (!options.background || !isNetworkTimeoutError(error))
|
|
3353
|
+
throw error;
|
|
3354
|
+
const recovered = await recoverBackgroundColumnRun(table, traceId);
|
|
3355
|
+
if (!recovered)
|
|
3356
|
+
throw error;
|
|
3357
|
+
return recovered;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
async function recoverBackgroundColumnRun(table, traceId) {
|
|
3361
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3362
|
+
const listed = await requestOxygen(tableRunsListPath({ table, status: "active", limit: "5", traceId }), { timeoutMs: 30_000 });
|
|
3363
|
+
const runs = Array.isArray(listed.runs)
|
|
3364
|
+
? listed.runs.filter(isRecord)
|
|
3365
|
+
: [];
|
|
3366
|
+
const run = runs[0];
|
|
3367
|
+
if (run) {
|
|
3368
|
+
const actionRunId = readRecordString(run, "action_run_id") ?? readRecordString(run, "id");
|
|
3369
|
+
const workspaceRunId = readRecordString(run, "workspace_run_id") ?? readRecordString(run, "runId");
|
|
3370
|
+
return {
|
|
3371
|
+
...run,
|
|
3372
|
+
...(actionRunId ? { action_run_id: actionRunId } : {}),
|
|
3373
|
+
...(workspaceRunId ? { workspace_run_id: workspaceRunId } : {}),
|
|
3374
|
+
timeout_recovered: true,
|
|
3375
|
+
recovery: {
|
|
3376
|
+
reason: "columns_run_background_timeout",
|
|
3377
|
+
trace_id: traceId,
|
|
3378
|
+
lookup_command: `oxygen table-runs list --table ${table} --status active --json`,
|
|
3379
|
+
},
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
if (attempt < 2)
|
|
3383
|
+
await sleep(750);
|
|
3384
|
+
}
|
|
3385
|
+
return null;
|
|
3386
|
+
}
|
|
3387
|
+
function isNetworkTimeoutError(error) {
|
|
3388
|
+
return error instanceof OxygenError && error.code === "network_timeout";
|
|
3389
|
+
}
|
|
3390
|
+
function readCompaniesEnrichBody(table, options) {
|
|
3391
|
+
const body = { table };
|
|
3392
|
+
const fields = readCsvOption(options.missingFields);
|
|
3393
|
+
const providers = readCsvOption(options.providers);
|
|
3394
|
+
const selection = readCompaniesEnrichSelection(options);
|
|
3395
|
+
const mode = readOption(options.mode);
|
|
3396
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3397
|
+
if (fields.length > 0)
|
|
3398
|
+
body.missing_fields = fields;
|
|
3399
|
+
if (providers.length > 0)
|
|
3400
|
+
body.providers = providers;
|
|
3401
|
+
if (selection)
|
|
3402
|
+
body.selection = selection;
|
|
3403
|
+
if (mode)
|
|
3404
|
+
body.mode = mode;
|
|
3405
|
+
if (maxCredits !== undefined)
|
|
3406
|
+
body.max_credits = maxCredits;
|
|
2765
3407
|
if (options.force !== undefined)
|
|
2766
3408
|
body.force = Boolean(options.force);
|
|
2767
3409
|
return body;
|
|
@@ -2861,19 +3503,26 @@ async function importRows(table, options) {
|
|
|
2861
3503
|
exitCode: 1,
|
|
2862
3504
|
});
|
|
2863
3505
|
}
|
|
2864
|
-
const target = await prepareImportTarget(table, options, parsedRows);
|
|
2865
3506
|
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
2866
3507
|
const sourceHash = hashImportFile(options.file);
|
|
2867
3508
|
const shouldUseBackground = options.background
|
|
2868
|
-
|| (!options.sync &&
|
|
3509
|
+
|| (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
|
|
2869
3510
|
if (shouldUseBackground) {
|
|
2870
|
-
|
|
3511
|
+
// Prefer the object-storage fast path: upload the raw file once via a
|
|
3512
|
+
// presigned URL (no request-body limit) so the worker COPY-loads it.
|
|
3513
|
+
// Falls back to the inline multipart import when storage is unconfigured.
|
|
3514
|
+
const staged = await tryEnqueueStagedFileImport(table, options, format, parsedRows, {
|
|
3515
|
+
autoBackground: !options.background,
|
|
3516
|
+
sourceHash,
|
|
3517
|
+
});
|
|
3518
|
+
if (staged)
|
|
3519
|
+
return staged;
|
|
3520
|
+
return enqueueImportFile(table, options, format, batchSize, {
|
|
2871
3521
|
autoBackground: !options.background,
|
|
2872
3522
|
sourceHash,
|
|
2873
|
-
createdTable: target.createdTable,
|
|
2874
|
-
...(target.upsertKey ? { upsertKey: target.upsertKey } : {}),
|
|
2875
3523
|
});
|
|
2876
3524
|
}
|
|
3525
|
+
const target = await prepareImportTarget(table, options, parsedRows);
|
|
2877
3526
|
let rowCount = 0;
|
|
2878
3527
|
let insertedCount = 0;
|
|
2879
3528
|
let updatedCount = 0;
|
|
@@ -2939,6 +3588,7 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
2939
3588
|
createdTable: null,
|
|
2940
3589
|
tableWebUrl: null,
|
|
2941
3590
|
upsertKey: options.upsertKey,
|
|
3591
|
+
sourceKeyMap: null,
|
|
2942
3592
|
};
|
|
2943
3593
|
}
|
|
2944
3594
|
const normalized = normalizeRowsForNewTable(parsedRows);
|
|
@@ -2965,103 +3615,138 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
2965
3615
|
createdTable: created,
|
|
2966
3616
|
tableWebUrl: createdWebUrl ?? tableWebUrl(createdSlug),
|
|
2967
3617
|
upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
|
|
3618
|
+
sourceKeyMap: normalized.keyBySource,
|
|
2968
3619
|
};
|
|
2969
3620
|
}
|
|
2970
|
-
async function
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
}
|
|
2982
|
-
const
|
|
3621
|
+
async function tryEnqueueStagedFileImport(// skipcq: JS-R1005
|
|
3622
|
+
table, options, format, parsedRows, context) {
|
|
3623
|
+
if (options.create && table) {
|
|
3624
|
+
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
3625
|
+
exitCode: 1,
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
if (!options.create && !table) {
|
|
3629
|
+
throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
|
|
3630
|
+
exitCode: 1,
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
const fileBuffer = readFileSync(options.file);
|
|
3634
|
+
const filename = basename(options.file);
|
|
3635
|
+
// Probe for a presigned upload URL. If object storage isn't configured the
|
|
3636
|
+
// server returns object_storage_not_configured; signal the caller to fall
|
|
3637
|
+
// back to the inline multipart import by returning null.
|
|
3638
|
+
let presigned;
|
|
3639
|
+
try {
|
|
3640
|
+
presigned = await requestOxygen("/api/cli/tables/import-url", {
|
|
3641
|
+
method: "POST",
|
|
3642
|
+
body: { file_name: filename, byte_length: fileBuffer.byteLength },
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
catch (error) {
|
|
3646
|
+
if (error instanceof OxygenError && error.code === "object_storage_not_configured") {
|
|
3647
|
+
return null;
|
|
3648
|
+
}
|
|
3649
|
+
throw error;
|
|
3650
|
+
}
|
|
3651
|
+
const uploadUrl = readRecordString(presigned, "upload_url");
|
|
3652
|
+
const storageKey = readRecordString(presigned, "storage_key");
|
|
3653
|
+
const storageBucket = readRecordString(presigned, "storage_bucket");
|
|
3654
|
+
const storageProvider = readRecordString(presigned, "storage_provider") ?? "s3";
|
|
3655
|
+
if (!uploadUrl || !storageKey)
|
|
3656
|
+
return null;
|
|
3657
|
+
// Create the table for --create (or resolve the existing ref) before the
|
|
3658
|
+
// upload so a presign success always pairs with a real target.
|
|
3659
|
+
const target = await prepareImportTarget(table, options, parsedRows);
|
|
3660
|
+
const controller = new AbortController();
|
|
3661
|
+
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
3662
|
+
let putResponse;
|
|
3663
|
+
try {
|
|
3664
|
+
putResponse = await fetch(uploadUrl, {
|
|
3665
|
+
method: "PUT",
|
|
3666
|
+
body: new Uint8Array(fileBuffer),
|
|
3667
|
+
signal: controller.signal,
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
finally {
|
|
3671
|
+
clearTimeout(timer);
|
|
3672
|
+
}
|
|
3673
|
+
if (!putResponse.ok) {
|
|
3674
|
+
throw new OxygenError("import_upload_failed", `Uploading the import file to object storage failed (HTTP ${putResponse.status}).`, { details: { status: putResponse.status }, exitCode: 1 });
|
|
3675
|
+
}
|
|
3676
|
+
const result = await requestOxygen("/api/cli/tables/import-staged", {
|
|
2983
3677
|
method: "POST",
|
|
3678
|
+
timeoutMs: 120_000,
|
|
2984
3679
|
body: {
|
|
2985
|
-
table,
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
requested_row_count: rows.length,
|
|
2997
|
-
batch_size: batchSize,
|
|
2998
|
-
auto_background: context.autoBackground,
|
|
2999
|
-
source_hash: context.sourceHash,
|
|
3000
|
-
idempotency_key: idempotencyKey,
|
|
3001
|
-
},
|
|
3680
|
+
table: target.tableRef,
|
|
3681
|
+
storage_provider: storageProvider,
|
|
3682
|
+
storage_bucket: storageBucket,
|
|
3683
|
+
storage_key: storageKey,
|
|
3684
|
+
format,
|
|
3685
|
+
file_name: filename,
|
|
3686
|
+
byte_length: fileBuffer.byteLength,
|
|
3687
|
+
sha256: context.sourceHash,
|
|
3688
|
+
row_count: parsedRows.length,
|
|
3689
|
+
...(target.upsertKey ? { upsert_key: target.upsertKey } : {}),
|
|
3690
|
+
...(target.sourceKeyMap ? { source_key_map: target.sourceKeyMap } : {}),
|
|
3002
3691
|
},
|
|
3003
3692
|
});
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
batchSize,
|
|
3021
|
-
mode: upsertKey ? "upsert" : "insert",
|
|
3022
|
-
upsertKey: upsertKey ?? null,
|
|
3023
|
-
idempotencyKey,
|
|
3024
|
-
...(queueWait ? { queue_wait: queueWait } : {}),
|
|
3025
|
-
};
|
|
3693
|
+
emitQueueWaitStderrNote(readRecord(result, "queue_wait"));
|
|
3694
|
+
return {
|
|
3695
|
+
...result,
|
|
3696
|
+
...(target.createdTable ? { createdTable: target.createdTable } : {}),
|
|
3697
|
+
...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
|
|
3698
|
+
background: true,
|
|
3699
|
+
autoBackground: context.autoBackground,
|
|
3700
|
+
import_engine: "bulk_file_v1",
|
|
3701
|
+
storage_provider: storageProvider,
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
async function enqueueImportFile(table, options, format, batchSize, context) {
|
|
3705
|
+
if (options.create && table) {
|
|
3706
|
+
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
3707
|
+
exitCode: 1,
|
|
3708
|
+
});
|
|
3026
3709
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
for (const [index, batch] of batches.entries()) {
|
|
3031
|
-
const appended = await requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(ingestionRunId)}/items`, {
|
|
3032
|
-
method: "POST",
|
|
3033
|
-
body: {
|
|
3034
|
-
items: [
|
|
3035
|
-
{
|
|
3036
|
-
position: index,
|
|
3037
|
-
payload: { rows: batch },
|
|
3038
|
-
},
|
|
3039
|
-
],
|
|
3040
|
-
},
|
|
3710
|
+
if (!options.create && !table) {
|
|
3711
|
+
throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
|
|
3712
|
+
exitCode: 1,
|
|
3041
3713
|
});
|
|
3042
|
-
insertedItems += readCount(appended.inserted);
|
|
3043
|
-
duplicatePositions += readCount(appended.duplicatePositions);
|
|
3044
|
-
latestRun = appended.run ?? latestRun;
|
|
3045
3714
|
}
|
|
3046
|
-
|
|
3715
|
+
const fileBuffer = readFileSync(options.file);
|
|
3716
|
+
const filename = basename(options.file);
|
|
3717
|
+
const form = new FormData();
|
|
3718
|
+
const file = new File([fileBuffer], filename);
|
|
3719
|
+
form.append("file", file, filename);
|
|
3720
|
+
form.append("filename", filename);
|
|
3721
|
+
form.append("format", format);
|
|
3722
|
+
form.append("batch_size", String(batchSize));
|
|
3723
|
+
form.append("auto_background", String(context.autoBackground));
|
|
3724
|
+
form.append("source_hash", context.sourceHash);
|
|
3725
|
+
if (table)
|
|
3726
|
+
form.append("table", table);
|
|
3727
|
+
if (options.create)
|
|
3728
|
+
form.append("table_name", options.create);
|
|
3729
|
+
const project = readOption(options.project);
|
|
3730
|
+
if (project)
|
|
3731
|
+
form.append("project", project);
|
|
3732
|
+
const upsertKey = readOption(options.upsertKey);
|
|
3733
|
+
if (upsertKey)
|
|
3734
|
+
form.append("upsert_key", upsertKey);
|
|
3735
|
+
if (options.sheet)
|
|
3736
|
+
form.append("sheet", options.sheet);
|
|
3737
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
3738
|
+
if (maxConcurrency)
|
|
3739
|
+
form.append("max_concurrency", String(maxConcurrency));
|
|
3740
|
+
const result = await requestOxygen("/api/cli/tables/import-file", {
|
|
3741
|
+
method: "POST",
|
|
3742
|
+
timeoutMs: 300_000,
|
|
3743
|
+
formData: form,
|
|
3744
|
+
});
|
|
3745
|
+
emitQueueWaitStderrNote(readRecord(result, "queue_wait"));
|
|
3047
3746
|
return {
|
|
3048
|
-
|
|
3049
|
-
ingestionRunId,
|
|
3050
|
-
...(context.createdTable ? { createdTable: context.createdTable } : {}),
|
|
3051
|
-
...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
|
|
3052
|
-
table_web_url: tableWebUrl(table),
|
|
3747
|
+
...result,
|
|
3053
3748
|
background: true,
|
|
3054
3749
|
autoBackground: context.autoBackground,
|
|
3055
|
-
sourceType: `file.${format}`,
|
|
3056
|
-
rowCount: rows.length,
|
|
3057
|
-
enqueuedItems: insertedItems,
|
|
3058
|
-
duplicatePositions,
|
|
3059
|
-
batchCount: batches.length,
|
|
3060
|
-
batchSize,
|
|
3061
|
-
mode: upsertKey ? "upsert" : "insert",
|
|
3062
|
-
upsertKey: upsertKey ?? null,
|
|
3063
|
-
idempotencyKey,
|
|
3064
|
-
...(queueWait ? { queue_wait: queueWait } : {}),
|
|
3065
3750
|
};
|
|
3066
3751
|
}
|
|
3067
3752
|
function emitQueueWaitStderrNote(queueWait) {
|
|
@@ -3257,7 +3942,8 @@ async function exportTableBundle(table, options) {
|
|
|
3257
3942
|
// stdout response a small summary so it's readable.
|
|
3258
3943
|
return options.output ? summary : { ...summary, bundle };
|
|
3259
3944
|
}
|
|
3260
|
-
async function importTableBundle(
|
|
3945
|
+
async function importTableBundle(// skipcq: JS-R1005
|
|
3946
|
+
options) {
|
|
3261
3947
|
const path = options.file;
|
|
3262
3948
|
if (!path || !path.trim()) {
|
|
3263
3949
|
throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
|
|
@@ -3269,49 +3955,169 @@ async function importTableBundle(options) {
|
|
|
3269
3955
|
if (columns.length === 0) {
|
|
3270
3956
|
throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
|
|
3271
3957
|
}
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3958
|
+
const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
|
|
3959
|
+
const into = readOption(options.into);
|
|
3960
|
+
const upsertKey = readOption(options.key);
|
|
3961
|
+
// Resuming/appending into an existing table without an upsert key would
|
|
3962
|
+
// duplicate every already-imported row. Require --key so the resume matches
|
|
3963
|
+
// rows instead of re-inserting them.
|
|
3964
|
+
if (into && !upsertKey) {
|
|
3965
|
+
throw new OxygenError("invalid_input", "--into requires --key so rows already imported are matched instead of duplicated. Pass --key <uniqueColumn>.", { details: { into }, exitCode: 1 });
|
|
3275
3966
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
if (
|
|
3290
|
-
|
|
3967
|
+
if (upsertKey) {
|
|
3968
|
+
if (upsertKey.startsWith("_")) {
|
|
3969
|
+
throw new OxygenError("invalid_input", "--key must be a workspace column key, not an internal field.", { details: { key: upsertKey }, exitCode: 1 });
|
|
3970
|
+
}
|
|
3971
|
+
if (validColumnKeys.size > 0 && !validColumnKeys.has(upsertKey)) {
|
|
3972
|
+
throw new OxygenError("invalid_input", `--key ${upsertKey} is not a column in this bundle.`, { details: { key: upsertKey, available_keys: [...validColumnKeys] }, exitCode: 1 });
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
// Resolve the target table: resume into an existing one (--into), or create a
|
|
3976
|
+
// fresh one from the bundle schema.
|
|
3977
|
+
let newTableId;
|
|
3978
|
+
let newTableSlug;
|
|
3979
|
+
let createdTable;
|
|
3980
|
+
if (into) {
|
|
3981
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table: into } });
|
|
3982
|
+
const tableMeta = describe.table ?? null;
|
|
3983
|
+
const resolvedId = tableMeta ? readRecordString(tableMeta, "id") : null;
|
|
3984
|
+
if (!resolvedId) {
|
|
3985
|
+
throw new OxygenError("table_not_found", `--into table ${into} was not found in this org.`, { details: { into }, exitCode: 1 });
|
|
3986
|
+
}
|
|
3987
|
+
newTableId = resolvedId;
|
|
3988
|
+
newTableSlug = tableMeta ? readRecordString(tableMeta, "slug") ?? null : null;
|
|
3989
|
+
createdTable = false;
|
|
3990
|
+
}
|
|
3991
|
+
else {
|
|
3992
|
+
const tableName = readOption(options.name) ?? bundle.tableName;
|
|
3993
|
+
if (!tableName) {
|
|
3994
|
+
throw new OxygenError("invalid_bundle", "Bundle has no table.name and no --name override was provided.", { exitCode: 1 });
|
|
3995
|
+
}
|
|
3996
|
+
const createResponse = await requestOxygen("/api/cli/tables", {
|
|
3997
|
+
method: "POST",
|
|
3998
|
+
body: {
|
|
3999
|
+
name: tableName,
|
|
4000
|
+
columns,
|
|
4001
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
4002
|
+
},
|
|
4003
|
+
});
|
|
4004
|
+
const created = readRecord(createResponse, "table");
|
|
4005
|
+
const createdId = readRecordString(createResponse, "table_id")
|
|
4006
|
+
?? (created ? readRecordString(created, "id") : null);
|
|
4007
|
+
if (!createdId) {
|
|
4008
|
+
throw new OxygenError("invalid_response", "Table create response did not include an id; bundle import cannot proceed.", { exitCode: 1 });
|
|
4009
|
+
}
|
|
4010
|
+
newTableId = createdId;
|
|
4011
|
+
newTableSlug = readRecordString(createResponse, "table_slug")
|
|
4012
|
+
?? (created ? readRecordString(created, "slug") : null);
|
|
4013
|
+
createdTable = true;
|
|
3291
4014
|
}
|
|
3292
|
-
const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
|
|
3293
4015
|
const stagedRows = bundle.rows.map((row) => stripRowForImport(row, validColumnKeys));
|
|
3294
|
-
|
|
4016
|
+
// In upsert mode every row must carry the key value; otherwise the upsert
|
|
4017
|
+
// route rejects the whole batch midway. Fail fast with a clear message before
|
|
4018
|
+
// writing anything so we never leave a half-imported table behind for a
|
|
4019
|
+
// predictable data problem.
|
|
4020
|
+
if (upsertKey) {
|
|
4021
|
+
const missingIndex = stagedRows.findIndex((row) => row[upsertKey] === undefined || row[upsertKey] === null);
|
|
4022
|
+
if (missingIndex >= 0) {
|
|
4023
|
+
throw new OxygenError("invalid_bundle", `Row ${missingIndex + 1} is missing a value for the upsert key "${upsertKey}".`, { details: { key: upsertKey, row_number: missingIndex + 1 }, exitCode: 1 });
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
const resumeCommand = buildBundleResumeCommand({
|
|
4027
|
+
file: path,
|
|
4028
|
+
tableId: newTableId,
|
|
4029
|
+
key: upsertKey ?? null,
|
|
4030
|
+
batchSize,
|
|
4031
|
+
});
|
|
4032
|
+
let processed = 0;
|
|
4033
|
+
let insertedCount = 0;
|
|
4034
|
+
let updatedCount = 0;
|
|
3295
4035
|
for (let offset = 0; offset < stagedRows.length; offset += batchSize) {
|
|
3296
4036
|
const batch = stagedRows.slice(offset, offset + batchSize);
|
|
3297
4037
|
if (batch.length === 0)
|
|
3298
4038
|
continue;
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
4039
|
+
try {
|
|
4040
|
+
if (upsertKey) {
|
|
4041
|
+
const response = await requestOxygen("/api/cli/tables/rows/upsert", {
|
|
4042
|
+
method: "POST",
|
|
4043
|
+
body: { table: newTableId, key: upsertKey, rows: batch, return: "summary" },
|
|
4044
|
+
});
|
|
4045
|
+
insertedCount += readCount(response.insertedCount);
|
|
4046
|
+
updatedCount += readCount(response.updatedCount);
|
|
4047
|
+
}
|
|
4048
|
+
else {
|
|
4049
|
+
await requestOxygen("/api/cli/tables/rows", {
|
|
4050
|
+
method: "POST",
|
|
4051
|
+
body: { table: newTableId, rows: batch },
|
|
4052
|
+
});
|
|
4053
|
+
insertedCount += batch.length;
|
|
4054
|
+
}
|
|
4055
|
+
processed += batch.length;
|
|
4056
|
+
}
|
|
4057
|
+
catch (error) {
|
|
4058
|
+
// A mid-loop failure used to leave a partial table with no way forward.
|
|
4059
|
+
// Surface exactly where it stopped and how to resume so the import is
|
|
4060
|
+
// recoverable instead of a silent half-write.
|
|
4061
|
+
const cause = error instanceof OxygenError
|
|
4062
|
+
? error.code
|
|
4063
|
+
: error instanceof Error
|
|
4064
|
+
? error.message
|
|
4065
|
+
: "unknown";
|
|
4066
|
+
const recovery = upsertKey
|
|
4067
|
+
? `Re-run with --into ${newTableId} --key ${upsertKey} to resume — already-imported rows are matched by "${upsertKey}", not duplicated.`
|
|
4068
|
+
: `Resume with: ${resumeCommand}`;
|
|
4069
|
+
throw new OxygenError("bundle_import_incomplete", `Bundle import stopped after ${processed}/${stagedRows.length} rows. ${recovery}`, {
|
|
4070
|
+
details: {
|
|
4071
|
+
table_id: newTableId,
|
|
4072
|
+
table_slug: newTableSlug,
|
|
4073
|
+
rows_total: stagedRows.length,
|
|
4074
|
+
rows_processed: processed,
|
|
4075
|
+
failed_batch_offset: offset,
|
|
4076
|
+
failed_batch_size: batch.length,
|
|
4077
|
+
mode: upsertKey ? "upsert" : "insert",
|
|
4078
|
+
...(upsertKey ? { key: upsertKey } : {}),
|
|
4079
|
+
resume_command: resumeCommand,
|
|
4080
|
+
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
4081
|
+
cause,
|
|
4082
|
+
},
|
|
4083
|
+
exitCode: 1,
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
3304
4086
|
}
|
|
3305
4087
|
return {
|
|
3306
4088
|
table_id: newTableId,
|
|
3307
4089
|
table_slug: newTableSlug ?? null,
|
|
4090
|
+
created_table: createdTable,
|
|
3308
4091
|
column_count: columns.length,
|
|
3309
|
-
row_count:
|
|
4092
|
+
row_count: processed,
|
|
4093
|
+
mode: upsertKey ? "upsert" : "insert",
|
|
4094
|
+
...(upsertKey
|
|
4095
|
+
? { upsert_key: upsertKey, inserted_count: insertedCount, updated_count: updatedCount }
|
|
4096
|
+
: {}),
|
|
3310
4097
|
source_table: bundle.tableSummary,
|
|
3311
4098
|
schema_version: bundle.schemaVersion,
|
|
3312
4099
|
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
3313
4100
|
};
|
|
3314
4101
|
}
|
|
4102
|
+
// Builds the exact CLI command that resumes a failed `import-bundle` into the
|
|
4103
|
+
// table it already created. Always targets that table with --into + --key so the
|
|
4104
|
+
// resume is idempotent; falls back to a <uniqueColumn> placeholder when the
|
|
4105
|
+
// original run had no key (insert mode can't be safely auto-resumed).
|
|
4106
|
+
function buildBundleResumeCommand(input) {
|
|
4107
|
+
const fileArg = /\s/.test(input.file) ? `"${input.file}"` : input.file;
|
|
4108
|
+
const parts = [
|
|
4109
|
+
"oxygen tables import-bundle",
|
|
4110
|
+
`--file ${fileArg}`,
|
|
4111
|
+
`--into ${input.tableId}`,
|
|
4112
|
+
`--key ${input.key ?? "<uniqueColumn>"}`,
|
|
4113
|
+
];
|
|
4114
|
+
// 500 is the default in normalizeImportBatchSize; only echo --batch-size when
|
|
4115
|
+
// the user picked something else, to keep the resume command minimal.
|
|
4116
|
+
if (input.batchSize !== 500) {
|
|
4117
|
+
parts.push(`--batch-size ${input.batchSize}`);
|
|
4118
|
+
}
|
|
4119
|
+
return parts.join(" ");
|
|
4120
|
+
}
|
|
3315
4121
|
function toBundleColumn(column) {
|
|
3316
4122
|
// Persist only the fields `tables create` knows how to ingest. Drop ids,
|
|
3317
4123
|
// physical column names, archivedAt — those are tenant-local and would
|
|
@@ -3458,7 +4264,7 @@ function formatRows(rows, format, columns) {
|
|
|
3458
4264
|
};
|
|
3459
4265
|
}
|
|
3460
4266
|
// "table": render with type-aware formatting and a Markdown-style frame.
|
|
3461
|
-
const columnByKey = new Map(columns?.map((
|
|
4267
|
+
const columnByKey = new Map(columns?.map((column) => [column.key, column]) ?? []);
|
|
3462
4268
|
let rescuedCount = 0;
|
|
3463
4269
|
const headers = keys.map((key) => columnByKey.get(key)?.label ?? key);
|
|
3464
4270
|
const formattedRows = rows.map((row) => keys.map((key) => {
|
|
@@ -3486,12 +4292,12 @@ function formatRows(rows, format, columns) {
|
|
|
3486
4292
|
});
|
|
3487
4293
|
const renderRow = (cells) => {
|
|
3488
4294
|
const padded = cells.map((cell, i) => {
|
|
3489
|
-
const
|
|
3490
|
-
return clipCell(cell,
|
|
4295
|
+
const width = widths[i] ?? 0;
|
|
4296
|
+
return clipCell(cell, width).padEnd(width);
|
|
3491
4297
|
});
|
|
3492
4298
|
return `| ${padded.join(" | ")} |`;
|
|
3493
4299
|
};
|
|
3494
|
-
const separator = `|${widths.map((
|
|
4300
|
+
const separator = `|${widths.map((width) => "-".repeat(width + 2)).join("|")}|`;
|
|
3495
4301
|
const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
|
|
3496
4302
|
if (rescuedCount > 0) {
|
|
3497
4303
|
lines.push("");
|
|
@@ -3542,15 +4348,6 @@ function isRecord(value) {
|
|
|
3542
4348
|
function tableWebUrl(tableIdOrSlug) {
|
|
3543
4349
|
return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
|
|
3544
4350
|
}
|
|
3545
|
-
function readRequiredResponseString(value, key) {
|
|
3546
|
-
const entry = value[key];
|
|
3547
|
-
if (typeof entry === "string" && entry.trim())
|
|
3548
|
-
return entry.trim();
|
|
3549
|
-
throw new OxygenError("invalid_response", `Oxygen API response is missing ${key}.`, {
|
|
3550
|
-
details: { key },
|
|
3551
|
-
exitCode: 1,
|
|
3552
|
-
});
|
|
3553
|
-
}
|
|
3554
4351
|
function normalizeImportBatchSize(value) {
|
|
3555
4352
|
const batchSize = readPositiveInt(value) ?? 500;
|
|
3556
4353
|
if (batchSize > 5000) {
|
|
@@ -3564,17 +4361,6 @@ function normalizeImportBatchSize(value) {
|
|
|
3564
4361
|
function hashImportFile(path) {
|
|
3565
4362
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
3566
4363
|
}
|
|
3567
|
-
function buildImportIdempotencyKey(input) {
|
|
3568
|
-
return [
|
|
3569
|
-
"tables-import",
|
|
3570
|
-
input.table,
|
|
3571
|
-
input.format,
|
|
3572
|
-
input.mode,
|
|
3573
|
-
input.upsertKey ?? "none",
|
|
3574
|
-
String(input.batchSize),
|
|
3575
|
-
input.sourceHash,
|
|
3576
|
-
].join(":");
|
|
3577
|
-
}
|
|
3578
4364
|
function isTerminalTableIngestionStatus(status) {
|
|
3579
4365
|
return status === "completed"
|
|
3580
4366
|
|| status === "completed_with_errors"
|
|
@@ -3606,15 +4392,20 @@ async function handleWhoamiAction(options) {
|
|
|
3606
4392
|
const identity = await requestOxygen("/api/cli/whoami");
|
|
3607
4393
|
const context = await resolveActiveProfileWithSource();
|
|
3608
4394
|
if (context.resolution.exists) {
|
|
3609
|
-
const
|
|
3610
|
-
const
|
|
4395
|
+
const existingCredentials = context.resolution.credentials;
|
|
4396
|
+
const cached = existingCredentials?.identity;
|
|
4397
|
+
const cachedOrg = existingCredentials?.activeOrganization ?? cached?.organization ?? null;
|
|
4398
|
+
const orgChanged = !cachedOrg || cachedOrg.id !== identity.organization.id;
|
|
3611
4399
|
const userChanged = !cached || cached.user.id !== identity.user.id;
|
|
3612
|
-
if (orgChanged || userChanged) {
|
|
4400
|
+
if (existingCredentials && (orgChanged || userChanged)) {
|
|
3613
4401
|
const refreshed = {
|
|
3614
|
-
token:
|
|
3615
|
-
apiUrl:
|
|
4402
|
+
token: existingCredentials.token,
|
|
4403
|
+
apiUrl: existingCredentials.apiUrl,
|
|
4404
|
+
activeOrganization: storedOrganizationFromOption(identity.organization),
|
|
3616
4405
|
identity: identityFromWhoami(identity),
|
|
3617
4406
|
};
|
|
4407
|
+
if (existingCredentials.authKind)
|
|
4408
|
+
refreshed.authKind = existingCredentials.authKind;
|
|
3618
4409
|
await saveCredentials(refreshed, process.env, {
|
|
3619
4410
|
profile: context.resolution.name,
|
|
3620
4411
|
activate: false,
|
|
@@ -3638,6 +4429,90 @@ async function handleWhoamiAction(options) {
|
|
|
3638
4429
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3639
4430
|
}
|
|
3640
4431
|
}
|
|
4432
|
+
async function handleOnboardingStartAction(options) {
|
|
4433
|
+
try {
|
|
4434
|
+
const data = await requestOxygen("/api/cli/onboarding/start", { method: "POST" });
|
|
4435
|
+
if (options.json) {
|
|
4436
|
+
writeJson(success("onboarding start", data));
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
process.stdout.write(formatOnboardingStart(data));
|
|
4440
|
+
}
|
|
4441
|
+
catch (error) {
|
|
4442
|
+
const failure = toFailure("onboarding start", error);
|
|
4443
|
+
writeJson(failure);
|
|
4444
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
async function handleOnboardingResetAction(options) {
|
|
4448
|
+
if (!options.confirm) {
|
|
4449
|
+
const failure = toFailure("onboarding reset", new OxygenError("confirmation_required", "Refusing to reset onboarding without --confirm. Re-run with `oxygen onboarding reset --confirm`.", { exitCode: 1 }));
|
|
4450
|
+
writeJson(failure);
|
|
4451
|
+
process.exitCode = 1;
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
try {
|
|
4455
|
+
const data = await requestOxygen("/api/cli/onboarding/reset", {
|
|
4456
|
+
method: "POST",
|
|
4457
|
+
body: { confirm: true },
|
|
4458
|
+
});
|
|
4459
|
+
if (options.json) {
|
|
4460
|
+
writeJson(success("onboarding reset", data));
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
process.stdout.write(formatOnboardingReset(data));
|
|
4464
|
+
}
|
|
4465
|
+
catch (error) {
|
|
4466
|
+
const failure = toFailure("onboarding reset", error);
|
|
4467
|
+
writeJson(failure);
|
|
4468
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
function formatOnboardingReset(data) {
|
|
4472
|
+
const lines = [];
|
|
4473
|
+
lines.push(data.message);
|
|
4474
|
+
if (data.cleared && data.cleared_keys.length > 0) {
|
|
4475
|
+
lines.push(`Cleared keys: ${data.cleared_keys.join(", ")}`);
|
|
4476
|
+
}
|
|
4477
|
+
if (data.next_steps.length > 0) {
|
|
4478
|
+
lines.push("");
|
|
4479
|
+
lines.push("Next:");
|
|
4480
|
+
for (const step of data.next_steps)
|
|
4481
|
+
lines.push(` - ${step}`);
|
|
4482
|
+
}
|
|
4483
|
+
if (data.web_url) {
|
|
4484
|
+
lines.push("");
|
|
4485
|
+
lines.push(`Continue in the web app: ${data.web_url}`);
|
|
4486
|
+
}
|
|
4487
|
+
return `${lines.join("\n")}\n`;
|
|
4488
|
+
}
|
|
4489
|
+
function formatOnboardingStart(data) {
|
|
4490
|
+
const lines = [];
|
|
4491
|
+
lines.push(`Onboarding ${data.context.already_started ? "already started" : "started"} for ${data.organization.name} (${data.organization.slug ?? data.organization.id}).`);
|
|
4492
|
+
if (data.user.domain) {
|
|
4493
|
+
lines.push(`Inferred work domain: ${data.user.domain}`);
|
|
4494
|
+
}
|
|
4495
|
+
else if (data.user.email) {
|
|
4496
|
+
lines.push(`No work domain inferred from ${data.user.email}; the skill will ask you for one.`);
|
|
4497
|
+
}
|
|
4498
|
+
lines.push("");
|
|
4499
|
+
lines.push(`Filled context sections: ${data.context.filled_sections.length > 0 ? data.context.filled_sections.join(", ") : "(none)"}`);
|
|
4500
|
+
lines.push(`Empty context sections: ${data.context.empty_sections.length > 0 ? data.context.empty_sections.join(", ") : "(none)"}`);
|
|
4501
|
+
lines.push("");
|
|
4502
|
+
lines.push(`Next: load and follow the \`${data.skill_to_load}\` skill.`);
|
|
4503
|
+
lines.push(` Skill URL: ${data.skill_url}`);
|
|
4504
|
+
if (data.next_steps.length > 0) {
|
|
4505
|
+
lines.push("");
|
|
4506
|
+
lines.push("Steps:");
|
|
4507
|
+
for (const step of data.next_steps)
|
|
4508
|
+
lines.push(` - ${step}`);
|
|
4509
|
+
}
|
|
4510
|
+
if (data.web_url) {
|
|
4511
|
+
lines.push("");
|
|
4512
|
+
lines.push(`View context in the web app: ${data.web_url}`);
|
|
4513
|
+
}
|
|
4514
|
+
return `${lines.join("\n")}\n`;
|
|
4515
|
+
}
|
|
3641
4516
|
async function handleLoginAction(options) {
|
|
3642
4517
|
try {
|
|
3643
4518
|
const data = await login(options);
|
|
@@ -3653,19 +4528,21 @@ async function handleLoginAction(options) {
|
|
|
3653
4528
|
}
|
|
3654
4529
|
async function handleAuthUseTokenAction(options) {
|
|
3655
4530
|
try {
|
|
3656
|
-
const data = await
|
|
4531
|
+
const data = await applyAuthToken(options);
|
|
3657
4532
|
if (options.json) {
|
|
3658
4533
|
writeJson(success("auth use-token", {
|
|
3659
4534
|
logged_in: true,
|
|
3660
4535
|
profile: data.profile,
|
|
3661
4536
|
api_url: data.credentials.apiUrl,
|
|
3662
|
-
user: data.
|
|
3663
|
-
organization: data.
|
|
4537
|
+
user: data.loginIdentity.user,
|
|
4538
|
+
organization: data.loginIdentity.activeOrganization,
|
|
4539
|
+
organizations: data.loginIdentity.organizations,
|
|
4540
|
+
selection_required: data.loginIdentity.selectionRequired,
|
|
3664
4541
|
renamed: data.renamed,
|
|
3665
4542
|
}));
|
|
3666
4543
|
return;
|
|
3667
4544
|
}
|
|
3668
|
-
process.stdout.write(
|
|
4545
|
+
process.stdout.write(formatLoginSuccessForResolved(data.loginIdentity, data.profile, { renamed: data.renamed }));
|
|
3669
4546
|
const hint = await buildPostLoginHint(data.profile);
|
|
3670
4547
|
if (hint)
|
|
3671
4548
|
process.stdout.write(hint);
|
|
@@ -3679,6 +4556,38 @@ async function handleAuthUseTokenAction(options) {
|
|
|
3679
4556
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3680
4557
|
}
|
|
3681
4558
|
}
|
|
4559
|
+
async function handleOrgUseAction(organization, options, command) {
|
|
4560
|
+
try {
|
|
4561
|
+
const data = await requestOxygen("/api/cli/orgs/select", {
|
|
4562
|
+
method: "POST",
|
|
4563
|
+
body: { organization },
|
|
4564
|
+
});
|
|
4565
|
+
const context = await resolveActiveProfileWithSource();
|
|
4566
|
+
const credentials = context.resolution.credentials;
|
|
4567
|
+
const canPersistSelection = Boolean(context.resolution.exists &&
|
|
4568
|
+
credentials &&
|
|
4569
|
+
(credentials.authKind === "user_session" || credentials.token.startsWith("oxy_user_")) &&
|
|
4570
|
+
data.currentOrganization);
|
|
4571
|
+
if (canPersistSelection && data.currentOrganization) {
|
|
4572
|
+
await updateActiveOrganizationForProfile(context.resolution.name, storedOrganizationFromOption(data.currentOrganization));
|
|
4573
|
+
}
|
|
4574
|
+
const result = {
|
|
4575
|
+
...data,
|
|
4576
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
4577
|
+
selection_persisted: canPersistSelection,
|
|
4578
|
+
};
|
|
4579
|
+
if (options.json) {
|
|
4580
|
+
writeJson(success(command, result));
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4583
|
+
writeJson(result);
|
|
4584
|
+
}
|
|
4585
|
+
catch (error) {
|
|
4586
|
+
const failure = toFailure(command, error);
|
|
4587
|
+
writeJson(failure);
|
|
4588
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
3682
4591
|
async function handleProfilesListAction(options) {
|
|
3683
4592
|
try {
|
|
3684
4593
|
const state = await listCredentialProfiles();
|
|
@@ -3873,9 +4782,8 @@ async function login(options) {
|
|
|
3873
4782
|
browser: options.browser !== false,
|
|
3874
4783
|
json: Boolean(options.json),
|
|
3875
4784
|
});
|
|
3876
|
-
const
|
|
3877
|
-
const
|
|
3878
|
-
credentials.identity = identityFromWhoami(identity);
|
|
4785
|
+
const loginIdentity = await resolveLoginIdentity(token, apiUrl);
|
|
4786
|
+
const credentials = loginIdentity.credentials;
|
|
3879
4787
|
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
3880
4788
|
let chosenProfile;
|
|
3881
4789
|
let renamed = false;
|
|
@@ -3883,15 +4791,14 @@ async function login(options) {
|
|
|
3883
4791
|
chosenProfile = explicitProfile;
|
|
3884
4792
|
}
|
|
3885
4793
|
else {
|
|
3886
|
-
const
|
|
3887
|
-
const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
|
|
4794
|
+
const picked = await pickProfileForLogin(loginIdentity);
|
|
3888
4795
|
chosenProfile = picked.name;
|
|
3889
4796
|
renamed = picked.renamed;
|
|
3890
4797
|
}
|
|
3891
4798
|
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
3892
4799
|
const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
|
|
3893
4800
|
if (!options.json) {
|
|
3894
|
-
process.stdout.write(
|
|
4801
|
+
process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
|
|
3895
4802
|
const hint = await buildPostLoginHint(profile);
|
|
3896
4803
|
if (hint)
|
|
3897
4804
|
process.stdout.write(hint);
|
|
@@ -3900,12 +4807,14 @@ async function login(options) {
|
|
|
3900
4807
|
logged_in: true,
|
|
3901
4808
|
profile,
|
|
3902
4809
|
api_url: credentials.apiUrl,
|
|
3903
|
-
user:
|
|
3904
|
-
organization:
|
|
4810
|
+
user: loginIdentity.user,
|
|
4811
|
+
organization: loginIdentity.activeOrganization,
|
|
4812
|
+
organizations: loginIdentity.organizations,
|
|
4813
|
+
selection_required: loginIdentity.selectionRequired,
|
|
3905
4814
|
skills_install: skillsInstall,
|
|
3906
4815
|
};
|
|
3907
4816
|
}
|
|
3908
|
-
async function
|
|
4817
|
+
async function applyAuthToken(options) {
|
|
3909
4818
|
const token = readOption(options.token);
|
|
3910
4819
|
if (!token) {
|
|
3911
4820
|
throw new OxygenError("missing_token", "Pass a CLI API token with --token.", {
|
|
@@ -3916,8 +4825,7 @@ async function useAuthToken(options) {
|
|
|
3916
4825
|
token,
|
|
3917
4826
|
apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
|
|
3918
4827
|
};
|
|
3919
|
-
const
|
|
3920
|
-
credentials.identity = identityFromWhoami(identity);
|
|
4828
|
+
const loginIdentity = await resolveLoginIdentity(token, credentials.apiUrl);
|
|
3921
4829
|
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
3922
4830
|
let chosenProfile;
|
|
3923
4831
|
let renamed = false;
|
|
@@ -3925,31 +4833,83 @@ async function useAuthToken(options) {
|
|
|
3925
4833
|
chosenProfile = explicitProfile;
|
|
3926
4834
|
}
|
|
3927
4835
|
else {
|
|
3928
|
-
const
|
|
3929
|
-
const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
|
|
4836
|
+
const picked = await pickProfileForLogin(loginIdentity);
|
|
3930
4837
|
chosenProfile = picked.name;
|
|
3931
4838
|
renamed = picked.renamed;
|
|
3932
4839
|
}
|
|
3933
|
-
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
4840
|
+
const profile = await saveCredentials(loginIdentity.credentials, process.env, { profile: chosenProfile });
|
|
3934
4841
|
return {
|
|
3935
|
-
identity,
|
|
3936
|
-
|
|
3937
|
-
|
|
4842
|
+
identity: loginIdentity.whoami,
|
|
4843
|
+
loginIdentity,
|
|
4844
|
+
credentials: loginIdentity.credentials,
|
|
4845
|
+
api_url: loginIdentity.credentials.apiUrl,
|
|
3938
4846
|
profile,
|
|
3939
4847
|
renamed,
|
|
3940
4848
|
};
|
|
3941
4849
|
}
|
|
4850
|
+
async function resolveLoginIdentity(token, apiUrl) {
|
|
4851
|
+
if (isUserSessionToken(token)) {
|
|
4852
|
+
const credentials = {
|
|
4853
|
+
token,
|
|
4854
|
+
apiUrl,
|
|
4855
|
+
authKind: "user_session",
|
|
4856
|
+
};
|
|
4857
|
+
const orgs = await requestOxygen("/api/cli/orgs", { credentials });
|
|
4858
|
+
const activeOrganization = orgs.currentOrganization
|
|
4859
|
+
? storedOrganizationFromOption(orgs.currentOrganization)
|
|
4860
|
+
: null;
|
|
4861
|
+
credentials.activeOrganization = activeOrganization;
|
|
4862
|
+
credentials.identity = identityFromUserAndOrganization(orgs.user, activeOrganization);
|
|
4863
|
+
return {
|
|
4864
|
+
credentials,
|
|
4865
|
+
whoami: null,
|
|
4866
|
+
user: orgs.user,
|
|
4867
|
+
activeOrganization,
|
|
4868
|
+
organizations: orgs.organizations,
|
|
4869
|
+
selectionRequired: Boolean(orgs.selectionRequired),
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
const credentials = {
|
|
4873
|
+
token,
|
|
4874
|
+
apiUrl,
|
|
4875
|
+
};
|
|
4876
|
+
if (token.startsWith("oxy_live_"))
|
|
4877
|
+
credentials.authKind = "org_api_key";
|
|
4878
|
+
const identity = await requestOxygen("/api/cli/whoami", { credentials });
|
|
4879
|
+
const activeOrganization = storedOrganizationFromOption(identity.organization);
|
|
4880
|
+
credentials.authKind = identity.authType === "user_session" ? "user_session" : "org_api_key";
|
|
4881
|
+
credentials.activeOrganization = activeOrganization;
|
|
4882
|
+
credentials.identity = identityFromWhoami(identity);
|
|
4883
|
+
return {
|
|
4884
|
+
credentials,
|
|
4885
|
+
whoami: identity,
|
|
4886
|
+
user: identity.user,
|
|
4887
|
+
activeOrganization,
|
|
4888
|
+
organizations: [{ ...identity.organization, selected: true }],
|
|
4889
|
+
selectionRequired: false,
|
|
4890
|
+
};
|
|
4891
|
+
}
|
|
4892
|
+
function pickProfileForLogin(loginIdentity) {
|
|
4893
|
+
if (loginIdentity.credentials.authKind === "user_session") {
|
|
4894
|
+
const seed = loginIdentity.activeOrganization?.slug
|
|
4895
|
+
?? loginIdentity.user.email
|
|
4896
|
+
?? loginIdentity.user.id;
|
|
4897
|
+
return pickProfileNameForUserSession(loginIdentity.user.id, seed);
|
|
4898
|
+
}
|
|
4899
|
+
const organization = loginIdentity.activeOrganization;
|
|
4900
|
+
if (!organization) {
|
|
4901
|
+
return pickProfileNameForUserSession(loginIdentity.user.id, loginIdentity.user.email ?? loginIdentity.user.id);
|
|
4902
|
+
}
|
|
4903
|
+
const seed = organization.slug?.trim() || organization.id;
|
|
4904
|
+
return pickProfileNameForIdentity(organization.id, seed);
|
|
4905
|
+
}
|
|
3942
4906
|
function readEnvProfileName() {
|
|
3943
4907
|
const value = process.env.OXYGEN_PROFILE?.trim();
|
|
3944
4908
|
return value ? value : null;
|
|
3945
4909
|
}
|
|
3946
4910
|
function identityFromWhoami(response) {
|
|
3947
4911
|
return {
|
|
3948
|
-
organization:
|
|
3949
|
-
id: response.organization.id,
|
|
3950
|
-
slug: response.organization.slug ?? null,
|
|
3951
|
-
name: response.organization.name,
|
|
3952
|
-
},
|
|
4912
|
+
organization: storedOrganizationFromOption(response.organization),
|
|
3953
4913
|
user: {
|
|
3954
4914
|
id: response.user.id,
|
|
3955
4915
|
email: response.user.email ?? null,
|
|
@@ -3957,6 +4917,26 @@ function identityFromWhoami(response) {
|
|
|
3957
4917
|
capturedAt: new Date().toISOString(),
|
|
3958
4918
|
};
|
|
3959
4919
|
}
|
|
4920
|
+
function identityFromUserAndOrganization(user, organization) {
|
|
4921
|
+
return {
|
|
4922
|
+
organization,
|
|
4923
|
+
user: {
|
|
4924
|
+
id: user.id,
|
|
4925
|
+
email: user.email ?? null,
|
|
4926
|
+
},
|
|
4927
|
+
capturedAt: new Date().toISOString(),
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
function storedOrganizationFromOption(organization) {
|
|
4931
|
+
return {
|
|
4932
|
+
id: organization.id,
|
|
4933
|
+
name: organization.name,
|
|
4934
|
+
slug: organization.slug ?? null,
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
function isUserSessionToken(token) {
|
|
4938
|
+
return token.startsWith("oxy_user_");
|
|
4939
|
+
}
|
|
3960
4940
|
async function buildPostLoginHint(activeProfile) {
|
|
3961
4941
|
if (output.isTTY !== true || process.env.NO_COLOR) {
|
|
3962
4942
|
const state = await listCredentialProfiles().catch(() => null);
|
|
@@ -3967,8 +4947,8 @@ async function buildPostLoginHint(activeProfile) {
|
|
|
3967
4947
|
const state = await listCredentialProfiles().catch(() => null);
|
|
3968
4948
|
if (!state || state.profiles.length < 2)
|
|
3969
4949
|
return "";
|
|
3970
|
-
const
|
|
3971
|
-
return `\n ${
|
|
4950
|
+
const styles = ansi(true);
|
|
4951
|
+
return `\n ${styles.dim("Pin this terminal:")} ${styles.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
|
|
3972
4952
|
}
|
|
3973
4953
|
async function promptForToken(options) {
|
|
3974
4954
|
const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
|
|
@@ -4055,7 +5035,7 @@ async function waitForBrowserToken(session) {
|
|
|
4055
5035
|
}
|
|
4056
5036
|
function createCliLoginUrl(apiUrl) {
|
|
4057
5037
|
const url = new URL("/auth/sign-in", apiUrl);
|
|
4058
|
-
url.searchParams.set("redirect_url", "/settings/
|
|
5038
|
+
url.searchParams.set("redirect_url", "/settings/cli");
|
|
4059
5039
|
url.searchParams.set("source", "oxygen_cli");
|
|
4060
5040
|
return url.toString();
|
|
4061
5041
|
}
|
|
@@ -4107,6 +5087,67 @@ function startOxygenSpinner(message) {
|
|
|
4107
5087
|
},
|
|
4108
5088
|
};
|
|
4109
5089
|
}
|
|
5090
|
+
function formatLoginSuccessForResolved(loginIdentity, profile, options = {}) {
|
|
5091
|
+
if (loginIdentity.whoami) {
|
|
5092
|
+
return formatLoginSuccess(loginIdentity.whoami, loginIdentity.credentials, profile, options);
|
|
5093
|
+
}
|
|
5094
|
+
if (loginIdentity.activeOrganization) {
|
|
5095
|
+
return formatLoginSuccess({
|
|
5096
|
+
user: loginIdentity.user,
|
|
5097
|
+
organization: loginIdentity.activeOrganization,
|
|
5098
|
+
apiKey: {
|
|
5099
|
+
id: "user_session",
|
|
5100
|
+
name: "CLI user session",
|
|
5101
|
+
tokenPrefix: loginIdentity.credentials.token.slice(0, 17),
|
|
5102
|
+
tokenSuffix: loginIdentity.credentials.token.slice(-4),
|
|
5103
|
+
kind: "user_session",
|
|
5104
|
+
},
|
|
5105
|
+
authType: "user_session",
|
|
5106
|
+
}, loginIdentity.credentials, profile, options);
|
|
5107
|
+
}
|
|
5108
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5109
|
+
const email = loginIdentity.user.email ?? loginIdentity.user.id;
|
|
5110
|
+
const fingerprint = createHash("sha256")
|
|
5111
|
+
.update(`oxygen-cli:${loginIdentity.credentials.token}`)
|
|
5112
|
+
.digest("hex");
|
|
5113
|
+
const profileLabel = options.renamed
|
|
5114
|
+
? `${profile} ${styles.dim("(auto-renamed to avoid collision)")}`
|
|
5115
|
+
: profile;
|
|
5116
|
+
const rows = [
|
|
5117
|
+
["Account", email],
|
|
5118
|
+
["Organization", "not selected"],
|
|
5119
|
+
["Profile", profileLabel],
|
|
5120
|
+
["API", loginIdentity.credentials.apiUrl],
|
|
5121
|
+
["Token", `${loginIdentity.credentials.token.slice(0, 17)}...${loginIdentity.credentials.token.slice(-4)}`],
|
|
5122
|
+
["Fingerprint", formatFingerprint(fingerprint)],
|
|
5123
|
+
];
|
|
5124
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
5125
|
+
const wordmark = renderBox([
|
|
5126
|
+
"",
|
|
5127
|
+
...OXYGEN_WORDMARK.split("\n").map(line => styles.green(line)),
|
|
5128
|
+
"",
|
|
5129
|
+
]);
|
|
5130
|
+
const orgLines = loginIdentity.organizations.length > 0
|
|
5131
|
+
? [
|
|
5132
|
+
"",
|
|
5133
|
+
"Available organizations:",
|
|
5134
|
+
...loginIdentity.organizations.map((organization) => ` - ${organization.name}${organization.slug ? ` (${organization.slug})` : ""}`),
|
|
5135
|
+
"",
|
|
5136
|
+
`Select one: ${styles.bold("oxygen orgs use <organization>")}`,
|
|
5137
|
+
]
|
|
5138
|
+
: [];
|
|
5139
|
+
return [
|
|
5140
|
+
"",
|
|
5141
|
+
wordmark,
|
|
5142
|
+
"",
|
|
5143
|
+
`${styles.green("[OK]")} ${styles.bold("CLI connected")}`,
|
|
5144
|
+
"",
|
|
5145
|
+
...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
|
|
5146
|
+
...orgLines,
|
|
5147
|
+
"",
|
|
5148
|
+
...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
|
|
5149
|
+
].join("\n");
|
|
5150
|
+
}
|
|
4110
5151
|
function formatLoginSuccess(identity, credentials, profile, options = {}) {
|
|
4111
5152
|
const email = identity.user.email ?? identity.user.id;
|
|
4112
5153
|
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
@@ -4118,9 +5159,9 @@ function formatLoginSuccess(identity, credentials, profile, options = {}) {
|
|
|
4118
5159
|
const fingerprint = createHash("sha256")
|
|
4119
5160
|
.update(`oxygen-cli:${credentials.token}`)
|
|
4120
5161
|
.digest("hex");
|
|
4121
|
-
const
|
|
5162
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4122
5163
|
const profileLabel = options.renamed
|
|
4123
|
-
? `${profile} ${
|
|
5164
|
+
? `${profile} ${styles.dim("(auto-renamed to avoid collision)")}`
|
|
4124
5165
|
: profile;
|
|
4125
5166
|
// skipcq: JS-0820 — not a React component; rule misfire on array of tuples
|
|
4126
5167
|
const rows = [
|
|
@@ -4134,16 +5175,16 @@ function formatLoginSuccess(identity, credentials, profile, options = {}) {
|
|
|
4134
5175
|
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
4135
5176
|
const wordmark = renderBox([
|
|
4136
5177
|
"",
|
|
4137
|
-
...OXYGEN_WORDMARK.split("\n").map(line =>
|
|
5178
|
+
...OXYGEN_WORDMARK.split("\n").map(line => styles.green(line)),
|
|
4138
5179
|
"",
|
|
4139
5180
|
]);
|
|
4140
5181
|
return [
|
|
4141
5182
|
"",
|
|
4142
5183
|
wordmark,
|
|
4143
5184
|
"",
|
|
4144
|
-
`${
|
|
5185
|
+
`${styles.green("[OK]")} ${styles.bold("CLI connected")}`,
|
|
4145
5186
|
"",
|
|
4146
|
-
...rows.map(([label, value]) => ` ${
|
|
5187
|
+
...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
|
|
4147
5188
|
"",
|
|
4148
5189
|
...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
|
|
4149
5190
|
].join("\n");
|
|
@@ -4157,7 +5198,7 @@ function summarizeCredentialProfile(profile, active) {
|
|
|
4157
5198
|
active,
|
|
4158
5199
|
api_url: profile.apiUrl,
|
|
4159
5200
|
token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
|
|
4160
|
-
organization: profile.identity?.organization ?? null,
|
|
5201
|
+
organization: profile.activeOrganization ?? profile.identity?.organization ?? null,
|
|
4161
5202
|
user: profile.identity?.user ?? null,
|
|
4162
5203
|
identity_captured_at: profile.identity?.capturedAt ?? null,
|
|
4163
5204
|
};
|
|
@@ -4168,11 +5209,11 @@ function createCredentialFingerprint(token) {
|
|
|
4168
5209
|
.digest("hex");
|
|
4169
5210
|
}
|
|
4170
5211
|
function formatProfilesList(data) {
|
|
4171
|
-
const
|
|
5212
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4172
5213
|
if (data.profiles.length === 0) {
|
|
4173
5214
|
return [
|
|
4174
5215
|
"",
|
|
4175
|
-
`${
|
|
5216
|
+
`${styles.dim("No stored Oxygen CLI profiles.")}`,
|
|
4176
5217
|
"",
|
|
4177
5218
|
].join("\n");
|
|
4178
5219
|
}
|
|
@@ -4180,7 +5221,7 @@ function formatProfilesList(data) {
|
|
|
4180
5221
|
const orgWidth = Math.max(...data.profiles.map((profile) => formatProfileOrgCell(profile).length));
|
|
4181
5222
|
const lines = [
|
|
4182
5223
|
"",
|
|
4183
|
-
`${
|
|
5224
|
+
`${styles.bold("Oxygen CLI Profiles")}`,
|
|
4184
5225
|
"",
|
|
4185
5226
|
...data.profiles.map((profile) => {
|
|
4186
5227
|
const marker = profile.active ? "*" : " ";
|
|
@@ -4188,14 +5229,14 @@ function formatProfilesList(data) {
|
|
|
4188
5229
|
return [
|
|
4189
5230
|
`${marker} ${profile.name.padEnd(nameWidth)}`,
|
|
4190
5231
|
orgCell.padEnd(orgWidth),
|
|
4191
|
-
|
|
4192
|
-
|
|
5232
|
+
styles.dim(profile.api_url),
|
|
5233
|
+
styles.dim(profile.token_fingerprint),
|
|
4193
5234
|
].join(" ");
|
|
4194
5235
|
}),
|
|
4195
5236
|
"",
|
|
4196
5237
|
];
|
|
4197
5238
|
if (data.profiles.length >= 2) {
|
|
4198
|
-
lines.push(` ${
|
|
5239
|
+
lines.push(` ${styles.dim("Pin a terminal:")} ${styles.bold('eval "$(oxygen profiles env <profile>)"')}`);
|
|
4199
5240
|
lines.push("");
|
|
4200
5241
|
}
|
|
4201
5242
|
return lines.join("\n");
|
|
@@ -4209,26 +5250,26 @@ function formatProfileOrgCell(profile) {
|
|
|
4209
5250
|
return name || slug || profile.organization.id;
|
|
4210
5251
|
}
|
|
4211
5252
|
function formatProfileUseSuccess(profile, options) {
|
|
4212
|
-
const
|
|
5253
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4213
5254
|
const lines = [
|
|
4214
5255
|
"",
|
|
4215
|
-
`${
|
|
5256
|
+
`${styles.green("[OK]")} ${styles.bold("CLI profile selected")}`,
|
|
4216
5257
|
"",
|
|
4217
|
-
` ${
|
|
4218
|
-
` ${
|
|
4219
|
-
` ${
|
|
4220
|
-
` ${
|
|
5258
|
+
` ${styles.dim("Profile")} ${profile.name}`,
|
|
5259
|
+
` ${styles.dim("Organization")} ${formatProfileOrgCell(profile)}`,
|
|
5260
|
+
` ${styles.dim("API")} ${profile.api_url}`,
|
|
5261
|
+
` ${styles.dim("Fingerprint")} ${profile.token_fingerprint}`,
|
|
4221
5262
|
"",
|
|
4222
5263
|
];
|
|
4223
5264
|
if (options.totalProfiles >= 2) {
|
|
4224
|
-
lines.push(` ${
|
|
4225
|
-
lines.push(` ${
|
|
5265
|
+
lines.push(` ${styles.dim("Note: this changes the active profile for every shell.")}`);
|
|
5266
|
+
lines.push(` ${styles.dim("To pin this terminal only, run:")} ${styles.bold(`eval "$(oxygen profiles env ${profile.name})"`)}`);
|
|
4226
5267
|
lines.push("");
|
|
4227
5268
|
}
|
|
4228
5269
|
return lines.join("\n");
|
|
4229
5270
|
}
|
|
4230
5271
|
function formatWhoami(identity, context) {
|
|
4231
|
-
const
|
|
5272
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4232
5273
|
const email = identity.user.email ?? identity.user.id;
|
|
4233
5274
|
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
4234
5275
|
const orgSlug = identity.organization.slug ?? null;
|
|
@@ -4239,42 +5280,56 @@ function formatWhoami(identity, context) {
|
|
|
4239
5280
|
const rows = [
|
|
4240
5281
|
["Account", email],
|
|
4241
5282
|
["Organization", org],
|
|
4242
|
-
["Profile", `${profileName} ${
|
|
5283
|
+
["Profile", `${profileName} ${styles.dim(`(${sourceLabel})`)}`],
|
|
4243
5284
|
["API", apiUrl],
|
|
4244
5285
|
];
|
|
4245
5286
|
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
4246
|
-
|
|
5287
|
+
const lines = [
|
|
4247
5288
|
"",
|
|
4248
|
-
...rows.map(([label, value]) => ` ${
|
|
5289
|
+
...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
|
|
4249
5290
|
"",
|
|
4250
|
-
]
|
|
5291
|
+
];
|
|
5292
|
+
if (identity.onboarding && identity.onboarding.complete === false) {
|
|
5293
|
+
const missing = identity.onboarding.missing_sections.join(", ");
|
|
5294
|
+
lines.push(` ${styles.dim("Onboarding")} ${styles.bold("workspace context not loaded")} ${styles.dim(`(missing: ${missing})`)}`, ` ${styles.dim("→ Run")} ${styles.bold("oxygen onboarding start")} ${styles.dim("(or oxygen_onboarding_start in MCP)")}`, "");
|
|
5295
|
+
}
|
|
5296
|
+
return lines.join("\n");
|
|
4251
5297
|
} // skipcq: JS-C1002
|
|
4252
5298
|
function formatProfilesCurrent(context) {
|
|
4253
|
-
const
|
|
5299
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4254
5300
|
const sourceLabel = describeProfileSource(context.source);
|
|
4255
5301
|
if (!context.resolution.exists) {
|
|
4256
5302
|
return [
|
|
4257
5303
|
"",
|
|
4258
|
-
`${
|
|
4259
|
-
` ${
|
|
4260
|
-
` ${
|
|
5304
|
+
`${styles.dim("No stored profile resolves for this shell.")}`,
|
|
5305
|
+
` ${styles.dim("Source:")} ${sourceLabel}`,
|
|
5306
|
+
` ${styles.dim("Run")} ${styles.bold("oxygen login")} ${styles.dim("to create one.")}`,
|
|
4261
5307
|
"",
|
|
4262
5308
|
].join("\n");
|
|
4263
5309
|
}
|
|
4264
5310
|
const credentials = context.resolution.credentials;
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
5311
|
+
if (!credentials) {
|
|
5312
|
+
return [
|
|
5313
|
+
"",
|
|
5314
|
+
`${styles.dim("No stored profile resolves for this shell.")}`,
|
|
5315
|
+
` ${styles.dim("Source:")} ${sourceLabel}`,
|
|
5316
|
+
"",
|
|
5317
|
+
].join("\n");
|
|
5318
|
+
}
|
|
5319
|
+
const organization = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
|
|
5320
|
+
const orgCell = organization
|
|
5321
|
+
? (organization.slug && organization.slug !== organization.name
|
|
5322
|
+
? `${organization.name} (${organization.slug})`
|
|
5323
|
+
: organization.name)
|
|
4269
5324
|
: "(unknown — run `oxygen whoami` to refresh)";
|
|
4270
5325
|
return [
|
|
4271
5326
|
"",
|
|
4272
|
-
`${
|
|
5327
|
+
`${styles.bold("Active Oxygen CLI Profile")}`,
|
|
4273
5328
|
"",
|
|
4274
|
-
` ${
|
|
4275
|
-
` ${
|
|
4276
|
-
` ${
|
|
4277
|
-
` ${
|
|
5329
|
+
` ${styles.dim("Profile")} ${context.resolution.name}`,
|
|
5330
|
+
` ${styles.dim("Source")} ${sourceLabel}`,
|
|
5331
|
+
` ${styles.dim("Organization")} ${orgCell}`,
|
|
5332
|
+
` ${styles.dim("API")} ${credentials.apiUrl}`,
|
|
4278
5333
|
"",
|
|
4279
5334
|
].join("\n");
|
|
4280
5335
|
}
|
|
@@ -4287,28 +5342,28 @@ function describeProfileSource(source) {
|
|
|
4287
5342
|
}
|
|
4288
5343
|
}
|
|
4289
5344
|
function formatLogoutSuccess(result) {
|
|
4290
|
-
const
|
|
5345
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4291
5346
|
const removed = result.removedProfile
|
|
4292
5347
|
? `profile "${result.removedProfile}" removed`
|
|
4293
5348
|
: "removed";
|
|
4294
5349
|
return [
|
|
4295
5350
|
"",
|
|
4296
|
-
`${
|
|
5351
|
+
`${styles.green("[OK]")} ${styles.bold("CLI logged out")}`,
|
|
4297
5352
|
"",
|
|
4298
|
-
` ${
|
|
4299
|
-
` ${
|
|
5353
|
+
` ${styles.dim("Credentials")} ${removed}`,
|
|
5354
|
+
` ${styles.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
|
|
4300
5355
|
"",
|
|
4301
5356
|
].join("\n");
|
|
4302
5357
|
}
|
|
4303
5358
|
function formatUpdateSuccess(result) {
|
|
4304
|
-
const
|
|
5359
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4305
5360
|
return [
|
|
4306
5361
|
"",
|
|
4307
|
-
`${
|
|
5362
|
+
`${styles.green("[OK]")} ${styles.bold(result.dry_run ? "CLI update command ready" : "CLI update completed")}`,
|
|
4308
5363
|
"",
|
|
4309
|
-
` ${
|
|
4310
|
-
` ${
|
|
4311
|
-
` ${
|
|
5364
|
+
` ${styles.dim("Current")} ${result.current_version}`,
|
|
5365
|
+
` ${styles.dim("Package")} ${result.package}`,
|
|
5366
|
+
` ${styles.dim("Command")} ${result.command}`,
|
|
4312
5367
|
"",
|
|
4313
5368
|
...formatAutomaticSkillsInstallStatusLines(result.skills_install),
|
|
4314
5369
|
].join("\n");
|
|
@@ -4316,25 +5371,25 @@ function formatUpdateSuccess(result) {
|
|
|
4316
5371
|
function formatAutomaticSkillsInstallStatusLines(result) {
|
|
4317
5372
|
if (!result)
|
|
4318
5373
|
return [];
|
|
4319
|
-
const
|
|
5374
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4320
5375
|
if (!result.attempted) {
|
|
4321
5376
|
if (result.reason === "dry_run")
|
|
4322
5377
|
return [];
|
|
4323
5378
|
return [
|
|
4324
|
-
`${
|
|
5379
|
+
`${styles.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
|
|
4325
5380
|
"",
|
|
4326
5381
|
];
|
|
4327
5382
|
}
|
|
4328
5383
|
if (result.ok) {
|
|
4329
5384
|
return [
|
|
4330
|
-
`${
|
|
5385
|
+
`${styles.green("[OK]")} ${styles.bold("Oxygen skills installed")}`,
|
|
4331
5386
|
"",
|
|
4332
5387
|
];
|
|
4333
5388
|
}
|
|
4334
5389
|
return [
|
|
4335
|
-
`${
|
|
4336
|
-
` ${
|
|
4337
|
-
` ${
|
|
5390
|
+
`${styles.yellow("[WARN]")} Oxygen skills could not be installed; the CLI command still completed.`,
|
|
5391
|
+
` ${styles.dim("Check")} ${"oxygen skills doctor --json"}`,
|
|
5392
|
+
` ${styles.dim("Retry")} ${result.command}`,
|
|
4338
5393
|
"",
|
|
4339
5394
|
];
|
|
4340
5395
|
}
|
|
@@ -4371,6 +5426,62 @@ function readCsvOption(value) {
|
|
|
4371
5426
|
.map((entry) => entry.trim())
|
|
4372
5427
|
.filter(Boolean);
|
|
4373
5428
|
}
|
|
5429
|
+
function splitCsv(value) {
|
|
5430
|
+
return value
|
|
5431
|
+
.split(",")
|
|
5432
|
+
.map((entry) => entry.trim())
|
|
5433
|
+
.filter(Boolean);
|
|
5434
|
+
}
|
|
5435
|
+
function collectMultiple(value, previous) {
|
|
5436
|
+
return [...previous, value];
|
|
5437
|
+
}
|
|
5438
|
+
async function buildBlueprintRequestBody(// skipcq: JS-R1005
|
|
5439
|
+
slug, options) {
|
|
5440
|
+
const body = {};
|
|
5441
|
+
if (slug?.trim())
|
|
5442
|
+
body.slug = slug.trim();
|
|
5443
|
+
const filePath = readOption(options.file);
|
|
5444
|
+
if (filePath) {
|
|
5445
|
+
const fs = await import("node:fs/promises");
|
|
5446
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
5447
|
+
body.envelope = JSON.parse(raw);
|
|
5448
|
+
}
|
|
5449
|
+
const fromUrl = readOption(options.fromUrl);
|
|
5450
|
+
if (fromUrl) {
|
|
5451
|
+
const response = await fetch(fromUrl, { headers: { Accept: "application/json" } });
|
|
5452
|
+
if (!response.ok) {
|
|
5453
|
+
throw new Error(`Failed to fetch blueprint from URL (${response.status} ${response.statusText}).`);
|
|
5454
|
+
}
|
|
5455
|
+
const payload = await response.json();
|
|
5456
|
+
body.envelope = payload.blueprint
|
|
5457
|
+
?? payload.envelope
|
|
5458
|
+
?? payload;
|
|
5459
|
+
}
|
|
5460
|
+
const inputJson = readOption(options.inputJson);
|
|
5461
|
+
if (inputJson) {
|
|
5462
|
+
try {
|
|
5463
|
+
body.inputs = JSON.parse(inputJson);
|
|
5464
|
+
}
|
|
5465
|
+
catch {
|
|
5466
|
+
throw new Error("--input-json must be valid JSON.");
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
const tableRefEntries = Array.isArray(options.tableRef) ? options.tableRef : [];
|
|
5470
|
+
if (tableRefEntries.length > 0) {
|
|
5471
|
+
const refs = {};
|
|
5472
|
+
for (const entry of tableRefEntries) {
|
|
5473
|
+
const [key, value] = entry.split("=");
|
|
5474
|
+
if (key && value)
|
|
5475
|
+
refs[key.trim()] = value.trim();
|
|
5476
|
+
}
|
|
5477
|
+
if (Object.keys(refs).length > 0)
|
|
5478
|
+
body.table_refs = refs;
|
|
5479
|
+
}
|
|
5480
|
+
if (!body.slug && !body.envelope) {
|
|
5481
|
+
throw new Error("Pass a blueprint slug, --file, or --from-url.");
|
|
5482
|
+
}
|
|
5483
|
+
return body;
|
|
5484
|
+
}
|
|
4374
5485
|
function contextAssetsQuery(options) {
|
|
4375
5486
|
const query = new URLSearchParams();
|
|
4376
5487
|
if (readOption(options.type))
|