@oxygen-agent/cli 1.50.37 → 1.98.7
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 +33 -0
- package/dist/credentials.js +200 -7
- package/dist/http-client.d.ts +5 -1
- package/dist/http-client.js +23 -2
- package/dist/index.js +2123 -366
- package/dist/skills.d.ts +47 -0
- package/dist/skills.js +375 -0
- package/dist/update.d.ts +3 -0
- package/dist/update.js +9 -5
- package/dist/windows-shim.d.ts +7 -0
- package/dist/windows-shim.js +21 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +2 -3
- 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/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- 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/provider-request-outcomes.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/provider-request-outcomes.js +5 -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 +268 -4
- 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,13 +14,16 @@ 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, 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";
|
|
21
|
+
import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
|
|
21
22
|
import { updateCli } from "./update.js";
|
|
22
23
|
const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
23
24
|
const OXYGEN_SPINNER_INTERVAL_MS = 90;
|
|
25
|
+
const INITIAL_OXYGEN_PROFILE_ENV = process.env.OXYGEN_PROFILE?.trim() || null;
|
|
26
|
+
let globalProfileFlag = null;
|
|
24
27
|
const OXYGEN_SPINNER_FRAMES = [
|
|
25
28
|
"[Oxygen ]",
|
|
26
29
|
"[ Oxygen ]",
|
|
@@ -109,6 +112,21 @@ function parseJsonArray(value) {
|
|
|
109
112
|
}
|
|
110
113
|
return parsed;
|
|
111
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
|
+
}
|
|
112
130
|
function readFileIfPresent(value) {
|
|
113
131
|
const candidate = resolve(value);
|
|
114
132
|
try {
|
|
@@ -158,11 +176,17 @@ export function createProgram() {
|
|
|
158
176
|
.name("oxygen")
|
|
159
177
|
.description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
|
|
160
178
|
.version(OXYGEN_VERSION)
|
|
161
|
-
.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.");
|
|
162
181
|
program.hook("preAction", () => {
|
|
163
182
|
const options = program.opts();
|
|
164
|
-
if (options.profile)
|
|
183
|
+
if (options.profile) {
|
|
184
|
+
globalProfileFlag = options.profile;
|
|
165
185
|
process.env.OXYGEN_PROFILE = options.profile;
|
|
186
|
+
}
|
|
187
|
+
if (options.org) {
|
|
188
|
+
process.env.OXYGEN_ORG = options.org;
|
|
189
|
+
}
|
|
166
190
|
});
|
|
167
191
|
program
|
|
168
192
|
.command("login")
|
|
@@ -209,6 +233,20 @@ export function createProgram() {
|
|
|
209
233
|
.option("--json", "Print a JSON envelope.")
|
|
210
234
|
.action(async (profile, options) => {
|
|
211
235
|
await handleLogoutAction({ ...options, profile });
|
|
236
|
+
}))
|
|
237
|
+
.addCommand(new Command("env")
|
|
238
|
+
.description("Print shell `export` lines that pin this terminal to a stored CLI profile.")
|
|
239
|
+
.argument("<profile>", "Stored profile name.")
|
|
240
|
+
.option("--unset", "Print `unset` lines instead of `export` lines.")
|
|
241
|
+
.option("--json", "Print a JSON envelope.")
|
|
242
|
+
.action(async (profile, options) => {
|
|
243
|
+
await handleProfilesEnvAction(profile, options);
|
|
244
|
+
}))
|
|
245
|
+
.addCommand(new Command("current")
|
|
246
|
+
.description("Show the profile the next command will use, and where it came from.")
|
|
247
|
+
.option("--json", "Print a JSON envelope.")
|
|
248
|
+
.action(async (options) => {
|
|
249
|
+
await handleProfilesCurrentAction(options);
|
|
212
250
|
}));
|
|
213
251
|
program
|
|
214
252
|
.command("logout")
|
|
@@ -263,8 +301,24 @@ export function createProgram() {
|
|
|
263
301
|
.description("Show the current Oxygen CLI identity.")
|
|
264
302
|
.option("--json", "Print a JSON envelope.")
|
|
265
303
|
.action(async (options) => {
|
|
266
|
-
await
|
|
304
|
+
await handleWhoamiAction(options);
|
|
267
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
|
+
}));
|
|
268
322
|
program
|
|
269
323
|
.command("status")
|
|
270
324
|
.description("Compare the local Oxygen CLI version against what's deployed in prod.")
|
|
@@ -289,16 +343,20 @@ export function createProgram() {
|
|
|
289
343
|
.option("--json", "Print a JSON envelope.")
|
|
290
344
|
.action(async (options) => {
|
|
291
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");
|
|
292
353
|
}))
|
|
293
354
|
.addCommand(new Command("select")
|
|
294
|
-
.description("
|
|
355
|
+
.description("Alias for orgs use.")
|
|
295
356
|
.argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
|
|
296
357
|
.option("--json", "Print a JSON envelope.")
|
|
297
358
|
.action(async (organization, options) => {
|
|
298
|
-
await
|
|
299
|
-
method: "POST",
|
|
300
|
-
body: { organization },
|
|
301
|
-
}));
|
|
359
|
+
await handleOrgUseAction(organization, options, "orgs select");
|
|
302
360
|
}));
|
|
303
361
|
program
|
|
304
362
|
.command("db")
|
|
@@ -527,6 +585,27 @@ export function createProgram() {
|
|
|
527
585
|
.option("--json", "Print a JSON envelope.")
|
|
528
586
|
.action(async (table, options) => {
|
|
529
587
|
await handleAsyncAction("tables export", options, async () => exportRows(table, options));
|
|
588
|
+
}))
|
|
589
|
+
.addCommand(new Command("export-bundle")
|
|
590
|
+
.description("Export a workspace table as a portable bundle (schema + every row) for cross-environment / cross-org copies.")
|
|
591
|
+
.argument("<table>", "Table id or slug to export.")
|
|
592
|
+
.option("--output <path>", "Write the bundle JSON to a file. Defaults to stdout.")
|
|
593
|
+
.option("--page-size <n>", "Rows per cursor-paginated request. Defaults to 500; hard cap is 1000.")
|
|
594
|
+
.option("--json", "Print a JSON envelope (omit row payload — use --output to keep the rows).")
|
|
595
|
+
.action(async (table, options) => {
|
|
596
|
+
await handleAsyncAction("tables export-bundle", options, async () => exportTableBundle(table, options));
|
|
597
|
+
}))
|
|
598
|
+
.addCommand(new Command("import-bundle")
|
|
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.")
|
|
600
|
+
.requiredOption("--file <path>", "Bundle JSON file produced by `tables export-bundle`.")
|
|
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.")
|
|
606
|
+
.option("--json", "Print a JSON envelope.")
|
|
607
|
+
.action(async (options) => {
|
|
608
|
+
await handleAsyncAction("tables import-bundle", options, async () => importTableBundle(options));
|
|
530
609
|
}))
|
|
531
610
|
.addCommand(new Command("list")
|
|
532
611
|
.description("List workspace tables in the current tenant database.")
|
|
@@ -629,6 +708,23 @@ export function createProgram() {
|
|
|
629
708
|
method: "POST",
|
|
630
709
|
body: { table, project: options.project },
|
|
631
710
|
}));
|
|
711
|
+
}))
|
|
712
|
+
.addCommand(new Command("recover-pending")
|
|
713
|
+
.description("Re-poll BetterContact async enrichments that timed out mid-poll and write any newly-found phones back to the table.")
|
|
714
|
+
.argument("<table>", "Table id or slug.")
|
|
715
|
+
.option("--max-items <n>", "Max pending cache rows to poll in this run. Defaults to 1000.")
|
|
716
|
+
.option("--json", "Print a JSON envelope.")
|
|
717
|
+
.action(async (table, options) => {
|
|
718
|
+
await handleAsyncAction("tables recover-pending", options, async () => {
|
|
719
|
+
const maxItems = options.maxItems ? Number.parseInt(options.maxItems, 10) : undefined;
|
|
720
|
+
return requestOxygen("/api/cli/tables/recover-pending", {
|
|
721
|
+
method: "POST",
|
|
722
|
+
body: {
|
|
723
|
+
table,
|
|
724
|
+
...(Number.isFinite(maxItems) ? { max_items: maxItems } : {}),
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
});
|
|
632
728
|
}));
|
|
633
729
|
tablesCommand.addCommand(new Command("webhook")
|
|
634
730
|
.description("Create and manage direct table webhooks.")
|
|
@@ -661,6 +757,22 @@ export function createProgram() {
|
|
|
661
757
|
program
|
|
662
758
|
.command("context")
|
|
663
759
|
.description("Workspace-level GTM context commands.")
|
|
760
|
+
.addCommand(new Command("resolve")
|
|
761
|
+
.description("Resolve task-scoped workspace GTM context with readiness and revision provenance.")
|
|
762
|
+
.option("--purpose <purpose>", "general, lead_sourcing, qualification, outbound_copy, or workflow_design.")
|
|
763
|
+
.option("--asset-type <csv>", "Comma-separated context asset types to include.")
|
|
764
|
+
.option("--asset-status <status>", "draft, active, archived, or all. Defaults to active.")
|
|
765
|
+
.option("--tags <csv>", "Comma-separated asset tags that must be present.")
|
|
766
|
+
.option("--include-archived", "Include archived assets when no asset status is set.")
|
|
767
|
+
.option("--max-assets <n>", "Maximum assets to include. Defaults to 10, max 50.")
|
|
768
|
+
.option("--require-ready", "Exit with a conflict error when required context sections are missing.")
|
|
769
|
+
.option("--json", "Print a JSON envelope.")
|
|
770
|
+
.action(async (options) => {
|
|
771
|
+
await handleAsyncAction("context resolve", options, async () => requestOxygen("/api/cli/context/resolve", {
|
|
772
|
+
method: "POST",
|
|
773
|
+
body: buildContextResolveBody(options),
|
|
774
|
+
}));
|
|
775
|
+
}))
|
|
664
776
|
.addCommand(new Command("profile")
|
|
665
777
|
.description("Company profile and ICP memory.")
|
|
666
778
|
.addCommand(new Command("get")
|
|
@@ -734,7 +846,238 @@ export function createProgram() {
|
|
|
734
846
|
}));
|
|
735
847
|
})));
|
|
736
848
|
program
|
|
737
|
-
.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")
|
|
738
1081
|
.description("Reusable prompt templates layered into AI columns at run time.")
|
|
739
1082
|
.addCommand(new Command("list")
|
|
740
1083
|
.description("List prompt templates in the workspace.")
|
|
@@ -742,10 +1085,85 @@ export function createProgram() {
|
|
|
742
1085
|
.option("--include-archived", "Include archived templates.")
|
|
743
1086
|
.option("--json", "Print a JSON envelope.")
|
|
744
1087
|
.action(async (options) => {
|
|
745
|
-
await handleAsyncAction("
|
|
1088
|
+
await handleAsyncAction("prompts list", options, () => {
|
|
746
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);
|
|
747
1132
|
if (readOption(options.kind))
|
|
748
|
-
|
|
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);
|
|
749
1167
|
if (options.includeArchived)
|
|
750
1168
|
params.set("include_archived", "true");
|
|
751
1169
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -789,10 +1207,12 @@ export function createProgram() {
|
|
|
789
1207
|
body.kind = readOption(options.kind);
|
|
790
1208
|
if (readOption(options.body))
|
|
791
1209
|
body.body = readOption(options.body);
|
|
792
|
-
else
|
|
1210
|
+
else {
|
|
793
1211
|
const path = readOption(options.bodyFile);
|
|
794
|
-
|
|
795
|
-
|
|
1212
|
+
if (path) {
|
|
1213
|
+
const fs = await import("node:fs/promises");
|
|
1214
|
+
body.body = await fs.readFile(path, "utf8");
|
|
1215
|
+
}
|
|
796
1216
|
}
|
|
797
1217
|
return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
|
|
798
1218
|
});
|
|
@@ -814,14 +1234,17 @@ export function createProgram() {
|
|
|
814
1234
|
.option("--limit <n>", "Max rows to return (default 50, max 200).")
|
|
815
1235
|
.option("--json", "Print a JSON envelope.")
|
|
816
1236
|
.action(async (options) => {
|
|
817
|
-
await handleAsyncAction("reviews list", options,
|
|
1237
|
+
await handleAsyncAction("reviews list", options, () => {
|
|
818
1238
|
const params = new URLSearchParams();
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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);
|
|
825
1248
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
826
1249
|
return requestOxygen(`/api/cli/message-reviews${qs}`);
|
|
827
1250
|
});
|
|
@@ -831,10 +1254,11 @@ export function createProgram() {
|
|
|
831
1254
|
.option("--table <table_id>", "Filter by table id.")
|
|
832
1255
|
.option("--json", "Print a JSON envelope.")
|
|
833
1256
|
.action(async (options) => {
|
|
834
|
-
await handleAsyncAction("reviews next", options,
|
|
1257
|
+
await handleAsyncAction("reviews next", options, () => {
|
|
835
1258
|
const params = new URLSearchParams();
|
|
836
|
-
|
|
837
|
-
|
|
1259
|
+
const table = readOption(options.table);
|
|
1260
|
+
if (table)
|
|
1261
|
+
params.set("table_id", table);
|
|
838
1262
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
839
1263
|
return requestOxygen(`/api/cli/message-reviews/next${qs}`);
|
|
840
1264
|
});
|
|
@@ -856,10 +1280,11 @@ export function createProgram() {
|
|
|
856
1280
|
.option("--auto-rerun", "Trigger a single-row rerun of the column after rejecting.")
|
|
857
1281
|
.option("--json", "Print a JSON envelope.")
|
|
858
1282
|
.action(async (reviewId, options) => {
|
|
859
|
-
await handleAsyncAction("reviews reject", options,
|
|
1283
|
+
await handleAsyncAction("reviews reject", options, () => {
|
|
860
1284
|
const body = { id: reviewId, decision: "reject" };
|
|
861
|
-
|
|
862
|
-
|
|
1285
|
+
const highlightsJson = readOption(options.highlightsJson);
|
|
1286
|
+
if (highlightsJson) {
|
|
1287
|
+
body.highlights = JSON.parse(highlightsJson);
|
|
863
1288
|
}
|
|
864
1289
|
if (options.autoRerun)
|
|
865
1290
|
body.auto_rerun = true;
|
|
@@ -872,15 +1297,25 @@ export function createProgram() {
|
|
|
872
1297
|
.addCommand(new Command("add")
|
|
873
1298
|
.description("Add a nullable column to a workspace table.")
|
|
874
1299
|
.argument("<table>", "Table id or slug.")
|
|
875
|
-
.
|
|
1300
|
+
.option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
|
|
876
1301
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
877
1302
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
878
1303
|
.option("--kind <kind>", "Column kind. Defaults to manual.")
|
|
879
1304
|
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
880
1305
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata.")
|
|
1306
|
+
.option("--prompt-key <key>", "OXYGEN prompt-library key (e.g. email_draft_v1). Materializes prompt + output_schema and forces kind=ai.")
|
|
1307
|
+
.option("--input-mapping <json>", "Required with --prompt-key. JSON object mapping prompt input names to column or literal refs.")
|
|
881
1308
|
.option("--json", "Print a JSON envelope.")
|
|
882
1309
|
.action(async (table, options) => {
|
|
883
|
-
|
|
1310
|
+
if (!options.promptKey && !options.label) {
|
|
1311
|
+
throw new OxygenError("invalid_request", "--label is required.", { exitCode: 1 });
|
|
1312
|
+
}
|
|
1313
|
+
if (options.promptKey && !options.inputMapping) {
|
|
1314
|
+
throw new OxygenError("missing_input_mapping", "--input-mapping is required when --prompt-key is provided.", { exitCode: 1 });
|
|
1315
|
+
}
|
|
1316
|
+
const column = {};
|
|
1317
|
+
if (options.label)
|
|
1318
|
+
column.label = options.label;
|
|
884
1319
|
if (options.key)
|
|
885
1320
|
column.key = options.key;
|
|
886
1321
|
if (options.dataType)
|
|
@@ -891,12 +1326,14 @@ export function createProgram() {
|
|
|
891
1326
|
column.semantic_type = options.semanticType;
|
|
892
1327
|
if (options.definitionJson)
|
|
893
1328
|
column.definition = parseJsonObject(options.definitionJson);
|
|
1329
|
+
const body = { table, column };
|
|
1330
|
+
if (options.promptKey)
|
|
1331
|
+
body.prompt_key = options.promptKey;
|
|
1332
|
+
if (options.inputMapping)
|
|
1333
|
+
body.input_mapping = parseJsonObject(options.inputMapping);
|
|
894
1334
|
await handleAsyncAction("columns add", options, async () => requestOxygen("/api/cli/tables/columns", {
|
|
895
1335
|
method: "POST",
|
|
896
|
-
body
|
|
897
|
-
table,
|
|
898
|
-
column,
|
|
899
|
-
},
|
|
1336
|
+
body,
|
|
900
1337
|
}));
|
|
901
1338
|
}))
|
|
902
1339
|
.addCommand(new Command("run")
|
|
@@ -911,6 +1348,7 @@ export function createProgram() {
|
|
|
911
1348
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
912
1349
|
.option("--background", "Create a durable background table action run instead of executing synchronously.")
|
|
913
1350
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for a background run.")
|
|
1351
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run. Defaults to 50.")
|
|
914
1352
|
.option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
|
|
915
1353
|
.option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
|
|
916
1354
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -918,6 +1356,7 @@ export function createProgram() {
|
|
|
918
1356
|
const limit = readPositiveInt(options.limit);
|
|
919
1357
|
const localConcurrency = readPositiveInt(options.localConcurrency);
|
|
920
1358
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
1359
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
921
1360
|
const filterSelection = readFilterSelectionOption(options.filterJson);
|
|
922
1361
|
const selectedModes = [
|
|
923
1362
|
Boolean(options.all),
|
|
@@ -954,20 +1393,21 @@ export function createProgram() {
|
|
|
954
1393
|
...(localConcurrency ? { concurrency: localConcurrency } : {}),
|
|
955
1394
|
});
|
|
956
1395
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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),
|
|
971
1411
|
});
|
|
972
1412
|
});
|
|
973
1413
|
}))
|
|
@@ -1035,10 +1475,11 @@ export function createProgram() {
|
|
|
1035
1475
|
.argument("<column>", "Column id or key.")
|
|
1036
1476
|
.option("--label <label>", "New display label.")
|
|
1037
1477
|
.option("--semantic-type <type>", "New semantic type.")
|
|
1038
|
-
.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.")
|
|
1039
1480
|
.option("--json", "Print a JSON envelope.")
|
|
1040
1481
|
.action(async (table, column, options) => {
|
|
1041
|
-
await handleAsyncAction("columns update", options,
|
|
1482
|
+
await handleAsyncAction("columns update", options, () => requestOxygen("/api/cli/tables/columns/update", {
|
|
1042
1483
|
method: "POST",
|
|
1043
1484
|
body: {
|
|
1044
1485
|
table,
|
|
@@ -1046,6 +1487,7 @@ export function createProgram() {
|
|
|
1046
1487
|
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
1047
1488
|
...(readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
|
|
1048
1489
|
...(options.definitionJson ? { definition: parseJsonObject(options.definitionJson) } : {}),
|
|
1490
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
1049
1491
|
},
|
|
1050
1492
|
}));
|
|
1051
1493
|
}))
|
|
@@ -1129,10 +1571,12 @@ export function createProgram() {
|
|
|
1129
1571
|
.option("--force", "Run even when the target cell already has a value.")
|
|
1130
1572
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
1131
1573
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
1574
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
|
|
1132
1575
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
1133
1576
|
.option("--json", "Print a JSON envelope.")
|
|
1134
1577
|
.action(async (table, options) => {
|
|
1135
1578
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
1579
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
1136
1580
|
await handleAsyncAction("table-runs create", options, async () => requestOxygen("/api/cli/table-action-runs", {
|
|
1137
1581
|
method: "POST",
|
|
1138
1582
|
body: {
|
|
@@ -1142,13 +1586,23 @@ export function createProgram() {
|
|
|
1142
1586
|
...(options.force ? { force: true } : {}),
|
|
1143
1587
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
1144
1588
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1589
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1145
1590
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
1146
1591
|
},
|
|
1147
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)));
|
|
1148
1602
|
}))
|
|
1149
1603
|
.addCommand(new Command("get")
|
|
1150
1604
|
.description("Get one durable table action run.")
|
|
1151
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1605
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1152
1606
|
.option("--json", "Print a JSON envelope.")
|
|
1153
1607
|
.action(async (runId, options) => {
|
|
1154
1608
|
await handleAsyncAction("table-runs get", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`));
|
|
@@ -1173,7 +1627,7 @@ export function createProgram() {
|
|
|
1173
1627
|
}))
|
|
1174
1628
|
.addCommand(new Command("wait")
|
|
1175
1629
|
.description("Poll a durable table action run until it finishes.")
|
|
1176
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1630
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1177
1631
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
1178
1632
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
|
|
1179
1633
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -1189,7 +1643,7 @@ export function createProgram() {
|
|
|
1189
1643
|
}))
|
|
1190
1644
|
.addCommand(new Command("cancel")
|
|
1191
1645
|
.description("Request cancellation for a durable table action run.")
|
|
1192
|
-
.argument("<run_id>", "Table action run UUID.")
|
|
1646
|
+
.argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
|
|
1193
1647
|
.option("--json", "Print a JSON envelope.")
|
|
1194
1648
|
.action(async (runId, options) => {
|
|
1195
1649
|
await handleAsyncAction("table-runs cancel", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/cancel`, {
|
|
@@ -1427,7 +1881,7 @@ export function createProgram() {
|
|
|
1427
1881
|
}))
|
|
1428
1882
|
.addCommand(new Command("failures")
|
|
1429
1883
|
.description("List failed background action and ingestion items.")
|
|
1430
|
-
.option("--queue <queue>", "all, actions, or
|
|
1884
|
+
.option("--queue <queue>", "all, actions, ingestions, or bullmq. Defaults to all.")
|
|
1431
1885
|
.option("--limit <n>", "Maximum failed items per queue. Defaults to 25; server cap is 100.")
|
|
1432
1886
|
.option("--json", "Print a JSON envelope.")
|
|
1433
1887
|
.action(async (options) => {
|
|
@@ -1443,7 +1897,7 @@ export function createProgram() {
|
|
|
1443
1897
|
});
|
|
1444
1898
|
}))
|
|
1445
1899
|
.addCommand(new Command("repair")
|
|
1446
|
-
.description("Repair stale background action and
|
|
1900
|
+
.description("Repair stale background action, ingestion, and workflow queue state.")
|
|
1447
1901
|
.option("--json", "Print a JSON envelope.")
|
|
1448
1902
|
.action(async (options) => {
|
|
1449
1903
|
await handleAsyncAction("worker repair", options, async () => requestOxygen("/api/cli/worker/repair", {
|
|
@@ -1533,6 +1987,7 @@ export function createProgram() {
|
|
|
1533
1987
|
.addCommand(new Command("costs")
|
|
1534
1988
|
.description("Show provider costs (COGS) per workspace. Staff only.")
|
|
1535
1989
|
.option("--top <n>", "Limit number of workspace columns. Defaults to all.")
|
|
1990
|
+
.option("--credential-mode <mode>", "managed (default; real COGS), byok (customer-supplied credentials), or all.", "managed")
|
|
1536
1991
|
.option("--json", "Print a JSON envelope.")
|
|
1537
1992
|
.action(async (options) => {
|
|
1538
1993
|
await handleAsyncAction("admin costs", options, async () => {
|
|
@@ -1540,6 +1995,10 @@ export function createProgram() {
|
|
|
1540
1995
|
const top = readPositiveInt(options.top);
|
|
1541
1996
|
if (top)
|
|
1542
1997
|
params.set("top", String(top));
|
|
1998
|
+
const credentialMode = readOption(options.credentialMode);
|
|
1999
|
+
if (credentialMode && credentialMode !== "managed") {
|
|
2000
|
+
params.set("credential_mode", credentialMode);
|
|
2001
|
+
}
|
|
1543
2002
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
1544
2003
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
1545
2004
|
});
|
|
@@ -1621,6 +2080,27 @@ export function createProgram() {
|
|
|
1621
2080
|
program
|
|
1622
2081
|
.command("cells")
|
|
1623
2082
|
.description("Workspace cell provenance commands.")
|
|
2083
|
+
.addCommand(new Command("inspect")
|
|
2084
|
+
.description("Inspect one cell's value, row context, and recent history.")
|
|
2085
|
+
.argument("<table>", "Table id or slug.")
|
|
2086
|
+
.argument("<row_id>", "Workspace row UUID.")
|
|
2087
|
+
.argument("<column>", "Column id or key.")
|
|
2088
|
+
.option("--history-limit <n>", "Maximum recent cell changes to include. Defaults to 10; cap 50.")
|
|
2089
|
+
.option("--json", "Print a JSON envelope.")
|
|
2090
|
+
.action(async (table, rowId, column, options) => {
|
|
2091
|
+
await handleAsyncAction("cells inspect", options, async () => {
|
|
2092
|
+
const historyLimit = readPositiveInt(options.historyLimit);
|
|
2093
|
+
return requestOxygen("/api/cli/tables/cells/inspect", {
|
|
2094
|
+
method: "POST",
|
|
2095
|
+
body: {
|
|
2096
|
+
table,
|
|
2097
|
+
row_id: rowId,
|
|
2098
|
+
column,
|
|
2099
|
+
...(historyLimit ? { history_limit: historyLimit } : {}),
|
|
2100
|
+
},
|
|
2101
|
+
});
|
|
2102
|
+
});
|
|
2103
|
+
}))
|
|
1624
2104
|
.addCommand(new Command("history")
|
|
1625
2105
|
.description("Show cell change history.")
|
|
1626
2106
|
.argument("<table>", "Table id or slug.")
|
|
@@ -1713,13 +2193,43 @@ export function createProgram() {
|
|
|
1713
2193
|
.action(async (options) => {
|
|
1714
2194
|
await handleAsyncAction("session usage", options, async () => getSessionUsage({ sessionId: readOption(options.sessionId) }));
|
|
1715
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
|
+
}));
|
|
1716
2226
|
program
|
|
1717
2227
|
.command("tools")
|
|
1718
2228
|
.description("Tool catalog commands.")
|
|
1719
2229
|
.addCommand(new Command("search")
|
|
1720
2230
|
.description("Search the tool catalog.")
|
|
1721
2231
|
.argument("[query]", "Search text.")
|
|
1722
|
-
.option("--verbosity <verbosity>", "summary or full. Defaults to summary.")
|
|
2232
|
+
.option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to summary. Use minimal for high-fanout discovery sweeps that need to stay under the MCP token budget.")
|
|
1723
2233
|
.option("--only-runnable", "Only return tools runnable by the active organization.")
|
|
1724
2234
|
.option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
|
|
1725
2235
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -1743,11 +2253,48 @@ export function createProgram() {
|
|
|
1743
2253
|
.action(async (toolId, options) => {
|
|
1744
2254
|
await handleAsyncAction("tools get", options, async () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
|
|
1745
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
|
+
})))
|
|
1746
2292
|
.addCommand(new Command("run")
|
|
1747
2293
|
.description("Run an executable Oxygen tool through the HTTPS API.")
|
|
1748
2294
|
.argument("<tool_id>", "Tool id.")
|
|
1749
2295
|
.requiredOption("--input-json <json>", "Tool input as a JSON object.")
|
|
1750
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.")
|
|
1751
2298
|
.option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
1752
2299
|
.option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
1753
2300
|
.option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the org's active default connection.")
|
|
@@ -1757,12 +2304,13 @@ export function createProgram() {
|
|
|
1757
2304
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
1758
2305
|
.option("--json", "Print a JSON envelope.")
|
|
1759
2306
|
.action(async (toolId, options) => {
|
|
1760
|
-
await handleAsyncAction("tools run", options,
|
|
2307
|
+
await handleAsyncAction("tools run", options, () => requestOxygen("/api/cli/tools/run", {
|
|
1761
2308
|
method: "POST",
|
|
1762
2309
|
body: {
|
|
1763
2310
|
tool_id: toolId,
|
|
1764
2311
|
input: parseJsonObject(options.inputJson),
|
|
1765
2312
|
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
2313
|
+
...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
|
|
1766
2314
|
...(readOption(options.org) ? { org_id: readOption(options.org) } : {}),
|
|
1767
2315
|
...(readOption(options.orgId) ? { org_id: readOption(options.orgId) } : {}),
|
|
1768
2316
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
@@ -1791,7 +2339,7 @@ export function createProgram() {
|
|
|
1791
2339
|
.option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
|
|
1792
2340
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
1793
2341
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
1794
|
-
.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.")
|
|
1795
2343
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
|
|
1796
2344
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
1797
2345
|
.option("--limit <n>", "Rows to estimate. Defaults to 10.")
|
|
@@ -1822,7 +2370,7 @@ export function createProgram() {
|
|
|
1822
2370
|
.option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
|
|
1823
2371
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
1824
2372
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
1825
|
-
.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.")
|
|
1826
2374
|
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
|
|
1827
2375
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
1828
2376
|
.option("--limit <n>", "Rows to queue.")
|
|
@@ -1831,27 +2379,49 @@ export function createProgram() {
|
|
|
1831
2379
|
.option("--selection-json <json>", "Full row selection object for server-side row selection.")
|
|
1832
2380
|
.option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
|
|
1833
2381
|
.option("--force", "Re-run rows with an existing target enrichment value.")
|
|
2382
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run. Defaults to 20.")
|
|
1834
2383
|
.option("--json", "Print a JSON envelope.")
|
|
1835
2384
|
.action(async (table, options) => {
|
|
2385
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
1836
2386
|
await handleAsyncAction("enrich-column run", options, async () => requestOxygen("/api/cli/enrich-column/run", {
|
|
1837
2387
|
method: "POST",
|
|
1838
2388
|
body: {
|
|
1839
2389
|
...buildEnrichColumnBody(table, options),
|
|
1840
2390
|
max_credits: readPositiveNumber(options.maxCredits),
|
|
2391
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1841
2392
|
...(options.force ? { force: true } : {}),
|
|
1842
2393
|
},
|
|
1843
2394
|
}));
|
|
1844
2395
|
}));
|
|
1845
2396
|
program
|
|
1846
|
-
.command("
|
|
1847
|
-
.description("
|
|
1848
|
-
.addCommand(new Command("
|
|
1849
|
-
.description("
|
|
1850
|
-
.
|
|
1851
|
-
.
|
|
1852
|
-
.option("--
|
|
1853
|
-
.option("--
|
|
1854
|
-
.
|
|
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
|
+
}));
|
|
2415
|
+
program
|
|
2416
|
+
.command("integrations")
|
|
2417
|
+
.description("Integration connection and event trigger commands.")
|
|
2418
|
+
.addCommand(new Command("events")
|
|
2419
|
+
.description("Configure provider events that can trigger workflows.")
|
|
2420
|
+
.addCommand(new Command("list")
|
|
2421
|
+
.description("List supported provider events and this org's enabled subscriptions.")
|
|
2422
|
+
.option("--source <source>", "Filter by event source, such as hubspot or composio.gmail.")
|
|
2423
|
+
.option("--event <event>", "Filter by event type, such as contact.created.")
|
|
2424
|
+
.option("--toolkit <id>", "Filter by toolkit / integration id, such as gmail.")
|
|
1855
2425
|
.option("--json", "Print a JSON envelope.")
|
|
1856
2426
|
.action(async (options) => {
|
|
1857
2427
|
await handleAsyncAction("integrations events list", options, async () => {
|
|
@@ -1917,6 +2487,33 @@ export function createProgram() {
|
|
|
1917
2487
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
1918
2488
|
},
|
|
1919
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
|
+
});
|
|
1920
2517
|
})))
|
|
1921
2518
|
.addCommand(new Command("list")
|
|
1922
2519
|
.description("List supported Composio integrations and this org's connections.")
|
|
@@ -1944,11 +2541,16 @@ export function createProgram() {
|
|
|
1944
2541
|
.addCommand(new Command("disconnect")
|
|
1945
2542
|
.description("Disconnect a Composio integration.")
|
|
1946
2543
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
2544
|
+
.option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
|
|
1947
2545
|
.option("--json", "Print a JSON envelope.")
|
|
1948
2546
|
.action(async (integrationId, options) => {
|
|
1949
|
-
|
|
2547
|
+
const connectionId = readOption(options.connectionId)?.trim();
|
|
2548
|
+
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/composio/disconnect", {
|
|
1950
2549
|
method: "POST",
|
|
1951
|
-
body: {
|
|
2550
|
+
body: {
|
|
2551
|
+
integration_id: integrationId,
|
|
2552
|
+
...(connectionId ? { connection_id: connectionId } : {}),
|
|
2553
|
+
},
|
|
1952
2554
|
}));
|
|
1953
2555
|
}))
|
|
1954
2556
|
.addCommand(new Command("actions")
|
|
@@ -2217,6 +2819,7 @@ export function createProgram() {
|
|
|
2217
2819
|
.addCommand(new Command("failures")
|
|
2218
2820
|
.description("List failed workflow runs and trigger scheduler failures.")
|
|
2219
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.")
|
|
2220
2823
|
.option("--json", "Print a JSON envelope.")
|
|
2221
2824
|
.action(async (options) => {
|
|
2222
2825
|
await handleAsyncAction("workflows failures", options, async () => {
|
|
@@ -2224,8 +2827,10 @@ export function createProgram() {
|
|
|
2224
2827
|
const limit = readPositiveInt(options.limit);
|
|
2225
2828
|
if (limit)
|
|
2226
2829
|
query.set("limit", String(limit));
|
|
2830
|
+
if (options.includeBundle)
|
|
2831
|
+
query.set("include_bundle", "true");
|
|
2227
2832
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2228
|
-
return requestOxygen(`/api/cli/workflows/failures${suffix}`);
|
|
2833
|
+
return prepareWorkflowCliOutput(await requestOxygen(`/api/cli/workflows/failures${suffix}`), options);
|
|
2229
2834
|
});
|
|
2230
2835
|
}))
|
|
2231
2836
|
.addCommand(new Command("run")
|
|
@@ -2281,14 +2886,28 @@ export function createProgram() {
|
|
|
2281
2886
|
}));
|
|
2282
2887
|
program
|
|
2283
2888
|
.command("skills")
|
|
2284
|
-
.description("Agent skill installation commands.")
|
|
2889
|
+
.description("Agent skill discovery and installation commands.")
|
|
2890
|
+
.addCommand(new Command("list")
|
|
2891
|
+
.description("List Oxygen agent skills available from the public skill index.")
|
|
2892
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2893
|
+
.option("--json", "Print a JSON envelope.")
|
|
2894
|
+
.action(async (options) => {
|
|
2895
|
+
await handleAsyncAction("skills list", options, async () => listAgentSkills(options));
|
|
2896
|
+
}))
|
|
2897
|
+
.addCommand(new Command("doctor")
|
|
2898
|
+
.description("Check Oxygen skill index reachability and local installer prerequisites.")
|
|
2899
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2900
|
+
.option("--json", "Print a JSON envelope.")
|
|
2901
|
+
.action(async (options) => {
|
|
2902
|
+
await handleAsyncAction("skills doctor", options, async () => doctorAgentSkills(options));
|
|
2903
|
+
}))
|
|
2285
2904
|
.addCommand(new Command("install")
|
|
2286
2905
|
.description("Install Oxygen agent skills into local agent skill directories.")
|
|
2287
2906
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2288
|
-
.option("--agents <agents
|
|
2907
|
+
.option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
|
|
2289
2908
|
.option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
|
|
2290
2909
|
.option("--project", "Install into the current project instead of global agent scope.")
|
|
2291
|
-
.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.")
|
|
2292
2911
|
.option("--json", "Print a JSON envelope.")
|
|
2293
2912
|
.action(async (options) => {
|
|
2294
2913
|
await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
|
|
@@ -2466,11 +3085,11 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
|
|
|
2466
3085
|
esModuleInterop: true,
|
|
2467
3086
|
},
|
|
2468
3087
|
}).outputText;
|
|
2469
|
-
const
|
|
3088
|
+
const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
|
|
3089
|
+
const workflowsUrl = await resolveWorkflowRuntimeModuleUrl(dir);
|
|
2470
3090
|
const rewritten = transpiled
|
|
2471
3091
|
.replaceAll("from \"@oxygen/workflows\"", `from "${workflowsUrl}"`)
|
|
2472
3092
|
.replaceAll("from '@oxygen/workflows'", `from "${workflowsUrl}"`);
|
|
2473
|
-
const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
|
|
2474
3093
|
const compiledPath = `${dir}/workflow.mjs`;
|
|
2475
3094
|
writeFileSync(compiledPath, rewritten, "utf8");
|
|
2476
3095
|
try {
|
|
@@ -2480,6 +3099,84 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
|
|
|
2480
3099
|
rmSync(dir, { recursive: true, force: true });
|
|
2481
3100
|
}
|
|
2482
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
|
+
}
|
|
2483
3180
|
async function tailWorkflowRun(runId, options) {
|
|
2484
3181
|
const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
|
|
2485
3182
|
?? WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS;
|
|
@@ -2508,12 +3205,23 @@ async function tailWorkflowRun(runId, options) {
|
|
|
2508
3205
|
}
|
|
2509
3206
|
const remainingMs = deadline - Date.now();
|
|
2510
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;
|
|
2511
3213
|
throw new OxygenError("workflow_tail_timeout", "Timed out waiting for workflow run to finish.", {
|
|
2512
3214
|
details: {
|
|
2513
3215
|
workflow_run_id: runId,
|
|
2514
3216
|
status: status ?? null,
|
|
2515
3217
|
timeout_seconds: timeoutSeconds,
|
|
2516
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
|
+
: {}),
|
|
2517
3225
|
},
|
|
2518
3226
|
exitCode: 1,
|
|
2519
3227
|
});
|
|
@@ -2613,6 +3321,72 @@ function readTableRunSelection(options) {
|
|
|
2613
3321
|
exitCode: 1,
|
|
2614
3322
|
});
|
|
2615
3323
|
}
|
|
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
|
+
}
|
|
2616
3390
|
function readCompaniesEnrichBody(table, options) {
|
|
2617
3391
|
const body = { table };
|
|
2618
3392
|
const fields = readCsvOption(options.missingFields);
|
|
@@ -2721,58 +3495,6 @@ function normalizeSessionStepStatus(value) {
|
|
|
2721
3495
|
exitCode: 1,
|
|
2722
3496
|
});
|
|
2723
3497
|
}
|
|
2724
|
-
function installAgentSkills(options) {
|
|
2725
|
-
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
2726
|
-
const indexUrl = `${apiUrl}/.well-known/skills/index.json`;
|
|
2727
|
-
const agents = readWords(options.agents ?? "codex claude-code cursor");
|
|
2728
|
-
const skill = readOption(options.skill) ?? "*";
|
|
2729
|
-
const args = [
|
|
2730
|
-
"skills",
|
|
2731
|
-
"add",
|
|
2732
|
-
indexUrl,
|
|
2733
|
-
"--agents",
|
|
2734
|
-
...agents,
|
|
2735
|
-
"--yes",
|
|
2736
|
-
"--skill",
|
|
2737
|
-
skill,
|
|
2738
|
-
"--full-depth",
|
|
2739
|
-
];
|
|
2740
|
-
if (!options.project)
|
|
2741
|
-
args.push("--global");
|
|
2742
|
-
if (options.copy)
|
|
2743
|
-
args.push("--copy");
|
|
2744
|
-
let output = "";
|
|
2745
|
-
try {
|
|
2746
|
-
output = execFileSync("npx", args, {
|
|
2747
|
-
encoding: "utf8",
|
|
2748
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2749
|
-
});
|
|
2750
|
-
}
|
|
2751
|
-
catch (error) {
|
|
2752
|
-
const failure = error;
|
|
2753
|
-
throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
|
|
2754
|
-
details: {
|
|
2755
|
-
index_url: indexUrl,
|
|
2756
|
-
stdout: failure.stdout?.slice(0, 2000) ?? "",
|
|
2757
|
-
stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
|
|
2758
|
-
},
|
|
2759
|
-
exitCode: 1,
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
2762
|
-
return {
|
|
2763
|
-
indexUrl,
|
|
2764
|
-
agents,
|
|
2765
|
-
skill,
|
|
2766
|
-
scope: options.project ? "project" : "global",
|
|
2767
|
-
output,
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
function readWords(value) {
|
|
2771
|
-
return value
|
|
2772
|
-
.split(/[,\s]+/)
|
|
2773
|
-
.map((entry) => entry.trim())
|
|
2774
|
-
.filter(Boolean);
|
|
2775
|
-
}
|
|
2776
3498
|
async function importRows(table, options) {
|
|
2777
3499
|
const format = normalizeRowsFormat(options.format, inferRowsFileFormat(options.file));
|
|
2778
3500
|
const parsedRows = await readRowsFile(options.file, format, options.sheet);
|
|
@@ -2781,19 +3503,26 @@ async function importRows(table, options) {
|
|
|
2781
3503
|
exitCode: 1,
|
|
2782
3504
|
});
|
|
2783
3505
|
}
|
|
2784
|
-
const target = await prepareImportTarget(table, options, parsedRows);
|
|
2785
3506
|
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
2786
3507
|
const sourceHash = hashImportFile(options.file);
|
|
2787
3508
|
const shouldUseBackground = options.background
|
|
2788
|
-
|| (!options.sync &&
|
|
3509
|
+
|| (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
|
|
2789
3510
|
if (shouldUseBackground) {
|
|
2790
|
-
|
|
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, {
|
|
2791
3521
|
autoBackground: !options.background,
|
|
2792
3522
|
sourceHash,
|
|
2793
|
-
createdTable: target.createdTable,
|
|
2794
|
-
...(target.upsertKey ? { upsertKey: target.upsertKey } : {}),
|
|
2795
3523
|
});
|
|
2796
3524
|
}
|
|
3525
|
+
const target = await prepareImportTarget(table, options, parsedRows);
|
|
2797
3526
|
let rowCount = 0;
|
|
2798
3527
|
let insertedCount = 0;
|
|
2799
3528
|
let updatedCount = 0;
|
|
@@ -2859,6 +3588,7 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
2859
3588
|
createdTable: null,
|
|
2860
3589
|
tableWebUrl: null,
|
|
2861
3590
|
upsertKey: options.upsertKey,
|
|
3591
|
+
sourceKeyMap: null,
|
|
2862
3592
|
};
|
|
2863
3593
|
}
|
|
2864
3594
|
const normalized = normalizeRowsForNewTable(parsedRows);
|
|
@@ -2885,101 +3615,147 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
2885
3615
|
createdTable: created,
|
|
2886
3616
|
tableWebUrl: createdWebUrl ?? tableWebUrl(createdSlug),
|
|
2887
3617
|
upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
|
|
3618
|
+
sourceKeyMap: normalized.keyBySource,
|
|
2888
3619
|
};
|
|
2889
3620
|
}
|
|
2890
|
-
async function
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
}
|
|
2902
|
-
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", {
|
|
2903
3677
|
method: "POST",
|
|
3678
|
+
timeoutMs: 120_000,
|
|
2904
3679
|
body: {
|
|
2905
|
-
table,
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
requested_row_count: rows.length,
|
|
2917
|
-
batch_size: batchSize,
|
|
2918
|
-
auto_background: context.autoBackground,
|
|
2919
|
-
source_hash: context.sourceHash,
|
|
2920
|
-
idempotency_key: idempotencyKey,
|
|
2921
|
-
},
|
|
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 } : {}),
|
|
2922
3691
|
},
|
|
2923
3692
|
});
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
mode: upsertKey ? "upsert" : "insert",
|
|
2941
|
-
upsertKey: upsertKey ?? null,
|
|
2942
|
-
idempotencyKey,
|
|
2943
|
-
};
|
|
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
|
+
});
|
|
2944
3709
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
for (const [index, batch] of batches.entries()) {
|
|
2949
|
-
const appended = await requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(ingestionRunId)}/items`, {
|
|
2950
|
-
method: "POST",
|
|
2951
|
-
body: {
|
|
2952
|
-
items: [
|
|
2953
|
-
{
|
|
2954
|
-
position: index,
|
|
2955
|
-
payload: { rows: batch },
|
|
2956
|
-
},
|
|
2957
|
-
],
|
|
2958
|
-
},
|
|
3710
|
+
if (!options.create && !table) {
|
|
3711
|
+
throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
|
|
3712
|
+
exitCode: 1,
|
|
2959
3713
|
});
|
|
2960
|
-
insertedItems += readCount(appended.inserted);
|
|
2961
|
-
duplicatePositions += readCount(appended.duplicatePositions);
|
|
2962
|
-
latestRun = appended.run ?? latestRun;
|
|
2963
3714
|
}
|
|
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"));
|
|
2964
3746
|
return {
|
|
2965
|
-
|
|
2966
|
-
ingestionRunId,
|
|
2967
|
-
...(context.createdTable ? { createdTable: context.createdTable } : {}),
|
|
2968
|
-
...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
|
|
2969
|
-
table_web_url: tableWebUrl(table),
|
|
3747
|
+
...result,
|
|
2970
3748
|
background: true,
|
|
2971
3749
|
autoBackground: context.autoBackground,
|
|
2972
|
-
sourceType: `file.${format}`,
|
|
2973
|
-
rowCount: rows.length,
|
|
2974
|
-
enqueuedItems: insertedItems,
|
|
2975
|
-
duplicatePositions,
|
|
2976
|
-
batchCount: batches.length,
|
|
2977
|
-
batchSize,
|
|
2978
|
-
mode: upsertKey ? "upsert" : "insert",
|
|
2979
|
-
upsertKey: upsertKey ?? null,
|
|
2980
|
-
idempotencyKey,
|
|
2981
3750
|
};
|
|
2982
3751
|
}
|
|
3752
|
+
function emitQueueWaitStderrNote(queueWait) {
|
|
3753
|
+
if (!queueWait)
|
|
3754
|
+
return;
|
|
3755
|
+
const note = readRecordString(queueWait, "note");
|
|
3756
|
+
if (note)
|
|
3757
|
+
process.stderr.write(`note: ${note}\n`);
|
|
3758
|
+
}
|
|
2983
3759
|
async function waitForTableIngestionRun(runId, options) {
|
|
2984
3760
|
const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
|
|
2985
3761
|
?? TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS;
|
|
@@ -3079,94 +3855,462 @@ async function exportRows(table, options) {
|
|
|
3079
3855
|
...(formatted.rescuedCount > 0 ? { rescuedNumericCells: formatted.rescuedCount } : {}),
|
|
3080
3856
|
};
|
|
3081
3857
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
function
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3858
|
+
const TABLE_BUNDLE_SCHEMA_VERSION = 1;
|
|
3859
|
+
const TABLE_BUNDLE_MAX_PAGE_SIZE = 1000;
|
|
3860
|
+
const TABLE_BUNDLE_DEFAULT_PAGE_SIZE = 500;
|
|
3861
|
+
async function exportTableBundle(table, options) {
|
|
3862
|
+
const pageSize = Math.min(readPositiveInt(options.pageSize) ?? TABLE_BUNDLE_DEFAULT_PAGE_SIZE, TABLE_BUNDLE_MAX_PAGE_SIZE);
|
|
3863
|
+
// Pull the canonical schema (incl. enrichment/tool definitions and
|
|
3864
|
+
// semantic types) from `describe`; the paginated query response only
|
|
3865
|
+
// surfaces the user-visible projection.
|
|
3866
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
|
|
3867
|
+
const tableMeta = describe.table ?? null;
|
|
3868
|
+
const columns = (describe.columns ?? []).map(toBundleColumn);
|
|
3869
|
+
const rows = [];
|
|
3870
|
+
let cursor = null;
|
|
3871
|
+
let expectedTotal = null;
|
|
3872
|
+
let pageCount = 0;
|
|
3873
|
+
let hasMoreFlag = false;
|
|
3874
|
+
do {
|
|
3875
|
+
const requestBody = { table, limit: pageSize };
|
|
3876
|
+
if (cursor)
|
|
3877
|
+
requestBody.cursor = cursor;
|
|
3878
|
+
const page = await requestOxygen("/api/cli/tables/query", { method: "POST", body: requestBody });
|
|
3879
|
+
pageCount += 1;
|
|
3880
|
+
if (expectedTotal === null) {
|
|
3881
|
+
expectedTotal = readBundleNumber(page.totalCount) ?? readBundleNumber(page.total_count) ?? null;
|
|
3882
|
+
}
|
|
3883
|
+
for (const row of page.rows ?? [])
|
|
3884
|
+
rows.push(row);
|
|
3885
|
+
cursor = typeof page.nextCursor === "string" && page.nextCursor.length > 0 ? page.nextCursor : null;
|
|
3886
|
+
hasMoreFlag = Boolean(page.hasMore);
|
|
3887
|
+
// Defensive: hasMore=false should always mean cursor=null. If a future
|
|
3888
|
+
// server change ever sets one without the other, stop iterating on
|
|
3889
|
+
// hasMore=false so we don't loop forever.
|
|
3890
|
+
if (!hasMoreFlag)
|
|
3891
|
+
cursor = null;
|
|
3892
|
+
} while (cursor);
|
|
3893
|
+
if (expectedTotal !== null && rows.length !== expectedTotal) {
|
|
3894
|
+
throw new OxygenError("bundle_export_incomplete", "Cursor pagination ended before every row was written. The bundle would be a silent under-export.", {
|
|
3895
|
+
details: {
|
|
3896
|
+
table,
|
|
3897
|
+
totalCount: expectedTotal,
|
|
3898
|
+
exportedCount: rows.length,
|
|
3899
|
+
missing: expectedTotal - rows.length,
|
|
3900
|
+
pages: pageCount,
|
|
3901
|
+
},
|
|
3902
|
+
exitCode: 1,
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
const tableId = readRecordString(tableMeta, "id");
|
|
3906
|
+
const tableSlug = readRecordString(tableMeta, "slug");
|
|
3907
|
+
const projectSlug = readRecordString(tableMeta, "projectSlug");
|
|
3908
|
+
const bundle = {
|
|
3909
|
+
schemaVersion: TABLE_BUNDLE_SCHEMA_VERSION,
|
|
3910
|
+
exportedAt: new Date().toISOString(),
|
|
3911
|
+
table: {
|
|
3912
|
+
id: tableId ?? null,
|
|
3913
|
+
slug: tableSlug ?? null,
|
|
3914
|
+
name: readRecordString(tableMeta, "displayName") ?? readRecordString(tableMeta, "name") ?? null,
|
|
3915
|
+
projectSlug: projectSlug ?? null,
|
|
3916
|
+
},
|
|
3917
|
+
columns,
|
|
3918
|
+
rows,
|
|
3919
|
+
totals: {
|
|
3920
|
+
rowCount: rows.length,
|
|
3921
|
+
...(expectedTotal !== null ? { sourceTotalCount: expectedTotal } : {}),
|
|
3922
|
+
pages: pageCount,
|
|
3923
|
+
pageSize,
|
|
3924
|
+
},
|
|
3925
|
+
};
|
|
3926
|
+
if (options.output) {
|
|
3927
|
+
writeFileSync(options.output, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
3928
|
+
}
|
|
3929
|
+
const summary = {
|
|
3930
|
+
table_id: tableId ?? null,
|
|
3931
|
+
table_slug: tableSlug ?? null,
|
|
3932
|
+
schema_version: TABLE_BUNDLE_SCHEMA_VERSION,
|
|
3933
|
+
column_count: columns.length,
|
|
3934
|
+
row_count: rows.length,
|
|
3935
|
+
pages: pageCount,
|
|
3936
|
+
page_size: pageSize,
|
|
3937
|
+
output: options.output ?? null,
|
|
3938
|
+
...(tableId ? { web_url: tableWebUrl(tableId) } : tableSlug ? { web_url: tableWebUrl(tableSlug) } : {}),
|
|
3939
|
+
};
|
|
3940
|
+
// When piping to stdout, also emit the full bundle so it can be redirected
|
|
3941
|
+
// into a file. With --output, the file is the source of truth; keep the
|
|
3942
|
+
// stdout response a small summary so it's readable.
|
|
3943
|
+
return options.output ? summary : { ...summary, bundle };
|
|
3101
3944
|
}
|
|
3102
|
-
function
|
|
3103
|
-
|
|
3104
|
-
|
|
3945
|
+
async function importTableBundle(// skipcq: JS-R1005
|
|
3946
|
+
options) {
|
|
3947
|
+
const path = options.file;
|
|
3948
|
+
if (!path || !path.trim()) {
|
|
3949
|
+
throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
|
|
3105
3950
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
const keys = [
|
|
3113
|
-
"_row_id",
|
|
3114
|
-
"_created_at",
|
|
3115
|
-
"_updated_at",
|
|
3116
|
-
...(columns?.map((column) => column.key) ?? []),
|
|
3117
|
-
...rows.flatMap((row) => Object.keys(row)),
|
|
3118
|
-
].filter((key, index, all) => all.indexOf(key) === index && rows.some((row) => key in row));
|
|
3119
|
-
if (format === "csv") {
|
|
3120
|
-
return {
|
|
3121
|
-
content: [
|
|
3122
|
-
keys.map(escapeCsvField).join(","),
|
|
3123
|
-
...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
|
|
3124
|
-
].join("\n") + "\n",
|
|
3125
|
-
rescuedCount: 0,
|
|
3126
|
-
};
|
|
3951
|
+
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
3952
|
+
const raw = readFileSync(resolve(path), "utf8");
|
|
3953
|
+
const bundle = parseBundleFile(raw);
|
|
3954
|
+
const columns = bundle.columns.map(bundleColumnToCreateInput);
|
|
3955
|
+
if (columns.length === 0) {
|
|
3956
|
+
throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
|
|
3127
3957
|
}
|
|
3128
|
-
|
|
3129
|
-
const
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
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 });
|
|
3966
|
+
}
|
|
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) } : {}),
|
|
3138
4002
|
},
|
|
3139
4003
|
});
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
}));
|
|
3146
|
-
const widths = headers.map((header, columnIndex) => {
|
|
3147
|
-
let max = header.length;
|
|
3148
|
-
for (const row of formattedRows) {
|
|
3149
|
-
const cell = row[columnIndex] ?? "";
|
|
3150
|
-
if (cell.length > max)
|
|
3151
|
-
max = cell.length;
|
|
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 });
|
|
3152
4009
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
4010
|
+
newTableId = createdId;
|
|
4011
|
+
newTableSlug = readRecordString(createResponse, "table_slug")
|
|
4012
|
+
?? (created ? readRecordString(created, "slug") : null);
|
|
4013
|
+
createdTable = true;
|
|
4014
|
+
}
|
|
4015
|
+
const stagedRows = bundle.rows.map((row) => stripRowForImport(row, validColumnKeys));
|
|
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;
|
|
4035
|
+
for (let offset = 0; offset < stagedRows.length; offset += batchSize) {
|
|
4036
|
+
const batch = stagedRows.slice(offset, offset + batchSize);
|
|
4037
|
+
if (batch.length === 0)
|
|
4038
|
+
continue;
|
|
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
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
return {
|
|
4088
|
+
table_id: newTableId,
|
|
4089
|
+
table_slug: newTableSlug ?? null,
|
|
4090
|
+
created_table: createdTable,
|
|
4091
|
+
column_count: columns.length,
|
|
4092
|
+
row_count: processed,
|
|
4093
|
+
mode: upsertKey ? "upsert" : "insert",
|
|
4094
|
+
...(upsertKey
|
|
4095
|
+
? { upsert_key: upsertKey, inserted_count: insertedCount, updated_count: updatedCount }
|
|
4096
|
+
: {}),
|
|
4097
|
+
source_table: bundle.tableSummary,
|
|
4098
|
+
schema_version: bundle.schemaVersion,
|
|
4099
|
+
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
4100
|
+
};
|
|
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
|
+
}
|
|
4121
|
+
function toBundleColumn(column) {
|
|
4122
|
+
// Persist only the fields `tables create` knows how to ingest. Drop ids,
|
|
4123
|
+
// physical column names, archivedAt — those are tenant-local and would
|
|
4124
|
+
// collide on import.
|
|
4125
|
+
return {
|
|
4126
|
+
key: column.key,
|
|
4127
|
+
...(column.label !== undefined ? { label: column.label } : {}),
|
|
4128
|
+
...(column.dataType !== undefined ? { dataType: column.dataType } : {}),
|
|
4129
|
+
...(column.kind !== undefined ? { kind: column.kind } : {}),
|
|
4130
|
+
...(column.semanticType !== undefined ? { semanticType: column.semanticType } : {}),
|
|
4131
|
+
...(column.definition !== undefined ? { definition: column.definition } : {}),
|
|
4132
|
+
...(column.position !== undefined ? { position: column.position } : {}),
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
function bundleColumnToCreateInput(column) {
|
|
4136
|
+
const label = typeof column.label === "string" && column.label.trim()
|
|
4137
|
+
? column.label.trim()
|
|
4138
|
+
: typeof column.key === "string"
|
|
4139
|
+
? column.key
|
|
4140
|
+
: null;
|
|
4141
|
+
if (!label) {
|
|
4142
|
+
throw new OxygenError("invalid_bundle", "Bundle column is missing label/key.", {
|
|
4143
|
+
details: { column },
|
|
4144
|
+
exitCode: 1,
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
return {
|
|
4148
|
+
label,
|
|
4149
|
+
...(typeof column.key === "string" ? { key: column.key } : {}),
|
|
4150
|
+
...(typeof column.dataType === "string" ? { dataType: column.dataType } : {}),
|
|
4151
|
+
...(typeof column.kind === "string" ? { kind: column.kind } : {}),
|
|
4152
|
+
...(typeof column.semanticType === "string" ? { semanticType: column.semanticType } : {}),
|
|
4153
|
+
...(column.definition && typeof column.definition === "object" && !Array.isArray(column.definition)
|
|
4154
|
+
? { definition: column.definition }
|
|
4155
|
+
: {}),
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
function parseBundleFile(text) {
|
|
4159
|
+
let parsed;
|
|
4160
|
+
try {
|
|
4161
|
+
parsed = JSON.parse(text);
|
|
4162
|
+
}
|
|
4163
|
+
catch (error) {
|
|
4164
|
+
throw new OxygenError("invalid_bundle", "Bundle file is not valid JSON.", {
|
|
4165
|
+
details: { reason: error instanceof Error ? error.message : "unknown" },
|
|
4166
|
+
exitCode: 1,
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4170
|
+
throw new OxygenError("invalid_bundle", "Bundle file must be a JSON object.", { exitCode: 1 });
|
|
4171
|
+
}
|
|
4172
|
+
const record = parsed;
|
|
4173
|
+
const schemaVersion = typeof record.schemaVersion === "number" ? record.schemaVersion : 1;
|
|
4174
|
+
if (schemaVersion !== TABLE_BUNDLE_SCHEMA_VERSION) {
|
|
4175
|
+
throw new OxygenError("unsupported_bundle_version", `Bundle schema version ${schemaVersion} is not supported by this CLI.`, {
|
|
4176
|
+
details: { supported: TABLE_BUNDLE_SCHEMA_VERSION, got: schemaVersion },
|
|
4177
|
+
exitCode: 1,
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
const rawColumns = record.columns;
|
|
4181
|
+
if (!Array.isArray(rawColumns)) {
|
|
4182
|
+
throw new OxygenError("invalid_bundle", "Bundle is missing a columns array.", { exitCode: 1 });
|
|
4183
|
+
}
|
|
4184
|
+
const rawRows = record.rows;
|
|
4185
|
+
if (!Array.isArray(rawRows)) {
|
|
4186
|
+
throw new OxygenError("invalid_bundle", "Bundle is missing a rows array.", { exitCode: 1 });
|
|
4187
|
+
}
|
|
4188
|
+
const tableSummary = record.table && typeof record.table === "object" && !Array.isArray(record.table)
|
|
4189
|
+
? record.table
|
|
4190
|
+
: null;
|
|
4191
|
+
return {
|
|
4192
|
+
schemaVersion,
|
|
4193
|
+
tableName: tableSummary
|
|
4194
|
+
? (readRecordString(tableSummary, "name") ?? readRecordString(tableSummary, "displayName"))
|
|
4195
|
+
: null,
|
|
4196
|
+
tableSummary,
|
|
4197
|
+
columns: rawColumns.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
|
|
4198
|
+
rows: rawRows.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4201
|
+
function stripRowForImport(row, validColumnKeys) {
|
|
4202
|
+
const out = {};
|
|
4203
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4204
|
+
if (key === "_row_id" || key === "_created_at" || key === "_updated_at")
|
|
4205
|
+
continue;
|
|
4206
|
+
if (validColumnKeys.size > 0 && !validColumnKeys.has(key))
|
|
4207
|
+
continue;
|
|
4208
|
+
out[key] = value;
|
|
4209
|
+
}
|
|
4210
|
+
return out;
|
|
4211
|
+
}
|
|
4212
|
+
function readBundleNumber(value) {
|
|
4213
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
4214
|
+
return value;
|
|
4215
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value)))
|
|
4216
|
+
return Number(value);
|
|
4217
|
+
return null;
|
|
4218
|
+
}
|
|
4219
|
+
async function readRowsFile(path, format, sheet) {
|
|
4220
|
+
return await parseRowsFileBuffer(readFileSync(path), format, sheet ? { sheet } : {});
|
|
4221
|
+
}
|
|
4222
|
+
function normalizeCreatedTableUpsertKey(value, keyBySource) {
|
|
4223
|
+
if (!value)
|
|
4224
|
+
return undefined;
|
|
4225
|
+
return keyBySource[value] ?? value;
|
|
4226
|
+
}
|
|
4227
|
+
function normalizeExportRowsFormat(value) {
|
|
4228
|
+
const normalized = value?.trim().toLowerCase() || "json";
|
|
4229
|
+
if (normalized === "json"
|
|
4230
|
+
|| normalized === "jsonl"
|
|
4231
|
+
|| normalized === "csv"
|
|
4232
|
+
|| normalized === "table")
|
|
4233
|
+
return normalized;
|
|
4234
|
+
throw new OxygenError("invalid_format", "Export format must be json, jsonl, csv, or table.", {
|
|
4235
|
+
details: { format: value },
|
|
4236
|
+
exitCode: 1,
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
function formatRows(rows, format, columns) {
|
|
4240
|
+
if (format === "json") {
|
|
4241
|
+
return { content: `${JSON.stringify(rows, null, 2)}\n`, rescuedCount: 0 };
|
|
4242
|
+
}
|
|
4243
|
+
if (format === "jsonl") {
|
|
4244
|
+
return {
|
|
4245
|
+
content: `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`,
|
|
4246
|
+
rescuedCount: 0,
|
|
4247
|
+
};
|
|
4248
|
+
} // skipcq: JS-0246
|
|
4249
|
+
const keys = [
|
|
4250
|
+
"_row_id",
|
|
4251
|
+
"_created_at",
|
|
4252
|
+
"_updated_at",
|
|
4253
|
+
...(columns?.map((column) => column.key) ?? []),
|
|
4254
|
+
...rows.flatMap((row) => Object.keys(row)),
|
|
4255
|
+
].filter((key, index, all) => all.indexOf(key) === index && rows.some((row) => key in row));
|
|
4256
|
+
if (format === "csv") {
|
|
4257
|
+
const csvBody = [
|
|
4258
|
+
keys.map(escapeCsvField).join(","),
|
|
4259
|
+
...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
|
|
4260
|
+
].join("\n");
|
|
4261
|
+
return {
|
|
4262
|
+
content: `${csvBody}\n`,
|
|
4263
|
+
rescuedCount: 0,
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
// "table": render with type-aware formatting and a Markdown-style frame.
|
|
4267
|
+
const columnByKey = new Map(columns?.map((column) => [column.key, column]) ?? []);
|
|
4268
|
+
let rescuedCount = 0;
|
|
4269
|
+
const headers = keys.map((key) => columnByKey.get(key)?.label ?? key);
|
|
4270
|
+
const formattedRows = rows.map((row) => keys.map((key) => {
|
|
4271
|
+
const column = columnByKey.get(key) ?? null;
|
|
4272
|
+
const formatted = formatCellForDisplay(row[key], column, {
|
|
4273
|
+
surface: "cli",
|
|
4274
|
+
onRescued: () => {
|
|
4275
|
+
rescuedCount += 1;
|
|
4276
|
+
},
|
|
4277
|
+
});
|
|
4278
|
+
// Pipe and any line-break char are the row/cell delimiters of the
|
|
4279
|
+
// Markdown frame — raw values containing them would corrupt the layout.
|
|
4280
|
+
// Match on the line-break class (not just `\r?\n`) so a standalone `\r`
|
|
4281
|
+
// doesn't slip through and split the row visually.
|
|
4282
|
+
return formatted.replace(/[\r\n]+/g, " ↵ ").replace(/\|/g, "\\|");
|
|
4283
|
+
}));
|
|
4284
|
+
const widths = headers.map((header, columnIndex) => {
|
|
4285
|
+
let max = header.length;
|
|
4286
|
+
for (const row of formattedRows) {
|
|
4287
|
+
const cell = row[columnIndex] ?? "";
|
|
4288
|
+
if (cell.length > max)
|
|
4289
|
+
max = cell.length;
|
|
4290
|
+
}
|
|
4291
|
+
return Math.min(max, 60);
|
|
4292
|
+
});
|
|
4293
|
+
const renderRow = (cells) => {
|
|
4294
|
+
const padded = cells.map((cell, i) => {
|
|
4295
|
+
const width = widths[i] ?? 0;
|
|
4296
|
+
return clipCell(cell, width).padEnd(width);
|
|
4297
|
+
});
|
|
4298
|
+
return `| ${padded.join(" | ")} |`;
|
|
4299
|
+
};
|
|
4300
|
+
const separator = `|${widths.map((width) => "-".repeat(width + 2)).join("|")}|`;
|
|
4301
|
+
const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
|
|
4302
|
+
if (rescuedCount > 0) {
|
|
3159
4303
|
lines.push("");
|
|
3160
4304
|
lines.push(`# Note: reformatted ${rescuedCount} cell${rescuedCount === 1 ? "" : "s"} that look numeric in text columns. Run \`oxygen columns retype --to numeric\` to make this permanent.`);
|
|
3161
4305
|
}
|
|
3162
|
-
return { content: lines.join("\n")
|
|
4306
|
+
return { content: `${lines.join("\n")}\n`, rescuedCount };
|
|
3163
4307
|
}
|
|
3164
4308
|
function clipCell(value, width) {
|
|
3165
4309
|
if (value.length <= width)
|
|
3166
4310
|
return value;
|
|
3167
4311
|
if (width <= 1)
|
|
3168
4312
|
return value.slice(0, width);
|
|
3169
|
-
return value.slice(0, width - 1)
|
|
4313
|
+
return `${value.slice(0, width - 1)}…`;
|
|
3170
4314
|
}
|
|
3171
4315
|
function escapeCsvField(value) {
|
|
3172
4316
|
const text = value === null || value === undefined
|
|
@@ -3204,15 +4348,6 @@ function isRecord(value) {
|
|
|
3204
4348
|
function tableWebUrl(tableIdOrSlug) {
|
|
3205
4349
|
return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
|
|
3206
4350
|
}
|
|
3207
|
-
function readRequiredResponseString(value, key) {
|
|
3208
|
-
const entry = value[key];
|
|
3209
|
-
if (typeof entry === "string" && entry.trim())
|
|
3210
|
-
return entry.trim();
|
|
3211
|
-
throw new OxygenError("invalid_response", `Oxygen API response is missing ${key}.`, {
|
|
3212
|
-
details: { key },
|
|
3213
|
-
exitCode: 1,
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
4351
|
function normalizeImportBatchSize(value) {
|
|
3217
4352
|
const batchSize = readPositiveInt(value) ?? 500;
|
|
3218
4353
|
if (batchSize > 5000) {
|
|
@@ -3226,17 +4361,6 @@ function normalizeImportBatchSize(value) {
|
|
|
3226
4361
|
function hashImportFile(path) {
|
|
3227
4362
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
3228
4363
|
}
|
|
3229
|
-
function buildImportIdempotencyKey(input) {
|
|
3230
|
-
return [
|
|
3231
|
-
"tables-import",
|
|
3232
|
-
input.table,
|
|
3233
|
-
input.format,
|
|
3234
|
-
input.mode,
|
|
3235
|
-
input.upsertKey ?? "none",
|
|
3236
|
-
String(input.batchSize),
|
|
3237
|
-
input.sourceHash,
|
|
3238
|
-
].join(":");
|
|
3239
|
-
}
|
|
3240
4364
|
function isTerminalTableIngestionStatus(status) {
|
|
3241
4365
|
return status === "completed"
|
|
3242
4366
|
|| status === "completed_with_errors"
|
|
@@ -3252,6 +4376,143 @@ function isTerminalTableActionRunStatus(status) {
|
|
|
3252
4376
|
function sleep(ms) {
|
|
3253
4377
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3254
4378
|
}
|
|
4379
|
+
async function resolveActiveProfileWithSource() {
|
|
4380
|
+
const resolution = await resolveActiveProfile();
|
|
4381
|
+
let source;
|
|
4382
|
+
if (globalProfileFlag)
|
|
4383
|
+
source = "flag";
|
|
4384
|
+
else if (INITIAL_OXYGEN_PROFILE_ENV)
|
|
4385
|
+
source = "env";
|
|
4386
|
+
else
|
|
4387
|
+
source = resolution.source === "env" ? "env" : resolution.source;
|
|
4388
|
+
return { resolution, source };
|
|
4389
|
+
}
|
|
4390
|
+
async function handleWhoamiAction(options) {
|
|
4391
|
+
try {
|
|
4392
|
+
const identity = await requestOxygen("/api/cli/whoami");
|
|
4393
|
+
const context = await resolveActiveProfileWithSource();
|
|
4394
|
+
if (context.resolution.exists) {
|
|
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;
|
|
4399
|
+
const userChanged = !cached || cached.user.id !== identity.user.id;
|
|
4400
|
+
if (existingCredentials && (orgChanged || userChanged)) {
|
|
4401
|
+
const refreshed = {
|
|
4402
|
+
token: existingCredentials.token,
|
|
4403
|
+
apiUrl: existingCredentials.apiUrl,
|
|
4404
|
+
activeOrganization: storedOrganizationFromOption(identity.organization),
|
|
4405
|
+
identity: identityFromWhoami(identity),
|
|
4406
|
+
};
|
|
4407
|
+
if (existingCredentials.authKind)
|
|
4408
|
+
refreshed.authKind = existingCredentials.authKind;
|
|
4409
|
+
await saveCredentials(refreshed, process.env, {
|
|
4410
|
+
profile: context.resolution.name,
|
|
4411
|
+
activate: false,
|
|
4412
|
+
}).catch(() => undefined);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
const data = {
|
|
4416
|
+
...identity,
|
|
4417
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
4418
|
+
profile_source: context.source,
|
|
4419
|
+
};
|
|
4420
|
+
if (options.json) {
|
|
4421
|
+
writeJson(success("whoami", data));
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
process.stdout.write(formatWhoami(identity, context));
|
|
4425
|
+
}
|
|
4426
|
+
catch (error) {
|
|
4427
|
+
const failure = toFailure("whoami", error);
|
|
4428
|
+
writeJson(failure);
|
|
4429
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4430
|
+
}
|
|
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
|
+
}
|
|
3255
4516
|
async function handleLoginAction(options) {
|
|
3256
4517
|
try {
|
|
3257
4518
|
const data = await login(options);
|
|
@@ -3267,18 +4528,24 @@ async function handleLoginAction(options) {
|
|
|
3267
4528
|
}
|
|
3268
4529
|
async function handleAuthUseTokenAction(options) {
|
|
3269
4530
|
try {
|
|
3270
|
-
const data = await
|
|
4531
|
+
const data = await applyAuthToken(options);
|
|
3271
4532
|
if (options.json) {
|
|
3272
4533
|
writeJson(success("auth use-token", {
|
|
3273
4534
|
logged_in: true,
|
|
3274
4535
|
profile: data.profile,
|
|
3275
4536
|
api_url: data.credentials.apiUrl,
|
|
3276
|
-
user: data.
|
|
3277
|
-
organization: data.
|
|
4537
|
+
user: data.loginIdentity.user,
|
|
4538
|
+
organization: data.loginIdentity.activeOrganization,
|
|
4539
|
+
organizations: data.loginIdentity.organizations,
|
|
4540
|
+
selection_required: data.loginIdentity.selectionRequired,
|
|
4541
|
+
renamed: data.renamed,
|
|
3278
4542
|
}));
|
|
3279
4543
|
return;
|
|
3280
4544
|
}
|
|
3281
|
-
process.stdout.write(
|
|
4545
|
+
process.stdout.write(formatLoginSuccessForResolved(data.loginIdentity, data.profile, { renamed: data.renamed }));
|
|
4546
|
+
const hint = await buildPostLoginHint(data.profile);
|
|
4547
|
+
if (hint)
|
|
4548
|
+
process.stdout.write(hint);
|
|
3282
4549
|
}
|
|
3283
4550
|
catch (error) {
|
|
3284
4551
|
const failure = toFailure("auth use-token", error);
|
|
@@ -3289,6 +4556,38 @@ async function handleAuthUseTokenAction(options) {
|
|
|
3289
4556
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3290
4557
|
}
|
|
3291
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
|
+
}
|
|
3292
4591
|
async function handleProfilesListAction(options) {
|
|
3293
4592
|
try {
|
|
3294
4593
|
const state = await listCredentialProfiles();
|
|
@@ -3311,6 +4610,8 @@ async function handleProfilesListAction(options) {
|
|
|
3311
4610
|
async function handleProfilesUseAction(profile, options) {
|
|
3312
4611
|
try {
|
|
3313
4612
|
const activeProfile = await switchCredentialProfile(profile);
|
|
4613
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
4614
|
+
const totalProfiles = state?.profiles.length ?? 1;
|
|
3314
4615
|
const data = {
|
|
3315
4616
|
active_profile: activeProfile.name,
|
|
3316
4617
|
profile: summarizeCredentialProfile(activeProfile, true),
|
|
@@ -3319,7 +4620,7 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
3319
4620
|
writeJson(success("profiles use", data));
|
|
3320
4621
|
return;
|
|
3321
4622
|
}
|
|
3322
|
-
process.stdout.write(formatProfileUseSuccess(data.profile));
|
|
4623
|
+
process.stdout.write(formatProfileUseSuccess(data.profile, { totalProfiles }));
|
|
3323
4624
|
}
|
|
3324
4625
|
catch (error) {
|
|
3325
4626
|
const failure = toFailure("profiles use", error);
|
|
@@ -3327,6 +4628,74 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
3327
4628
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3328
4629
|
}
|
|
3329
4630
|
}
|
|
4631
|
+
async function handleProfilesEnvAction(profile, options) {
|
|
4632
|
+
try {
|
|
4633
|
+
if (options.unset) {
|
|
4634
|
+
const data = {
|
|
4635
|
+
profile,
|
|
4636
|
+
unset: ["OXYGEN_PROFILE", "OXYGEN_API_URL"],
|
|
4637
|
+
};
|
|
4638
|
+
if (options.json) {
|
|
4639
|
+
writeJson(success("profiles env", data));
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
process.stdout.write("unset OXYGEN_PROFILE OXYGEN_API_URL\n");
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4645
|
+
const state = await listCredentialProfiles();
|
|
4646
|
+
const match = state.profiles.find((entry) => entry.name === profile);
|
|
4647
|
+
if (!match) {
|
|
4648
|
+
throw new OxygenError("profile_not_found", `Oxygen CLI profile "${profile}" is not stored.`, {
|
|
4649
|
+
details: { profile },
|
|
4650
|
+
exitCode: 1,
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
const env = {
|
|
4654
|
+
OXYGEN_PROFILE: match.name,
|
|
4655
|
+
OXYGEN_API_URL: match.apiUrl,
|
|
4656
|
+
};
|
|
4657
|
+
if (options.json) {
|
|
4658
|
+
writeJson(success("profiles env", { profile: match.name, env }));
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
process.stdout.write(`export OXYGEN_PROFILE=${shellQuote(match.name)}\n`);
|
|
4662
|
+
process.stdout.write(`export OXYGEN_API_URL=${shellQuote(match.apiUrl)}\n`);
|
|
4663
|
+
}
|
|
4664
|
+
catch (error) {
|
|
4665
|
+
const failure = toFailure("profiles env", error);
|
|
4666
|
+
writeJson(failure);
|
|
4667
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
async function handleProfilesCurrentAction(options) {
|
|
4671
|
+
try {
|
|
4672
|
+
const context = await resolveActiveProfileWithSource();
|
|
4673
|
+
const credentials = context.resolution.credentials;
|
|
4674
|
+
const data = {
|
|
4675
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
4676
|
+
profile_source: context.source,
|
|
4677
|
+
api_url: credentials?.apiUrl ?? null,
|
|
4678
|
+
organization: credentials?.identity?.organization ?? null,
|
|
4679
|
+
user: credentials?.identity?.user ?? null,
|
|
4680
|
+
stored: context.resolution.exists,
|
|
4681
|
+
};
|
|
4682
|
+
if (options.json) {
|
|
4683
|
+
writeJson(success("profiles current", data));
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
process.stdout.write(formatProfilesCurrent(context));
|
|
4687
|
+
}
|
|
4688
|
+
catch (error) {
|
|
4689
|
+
const failure = toFailure("profiles current", error);
|
|
4690
|
+
writeJson(failure);
|
|
4691
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
function shellQuote(value) {
|
|
4695
|
+
if (/^[A-Za-z0-9_.\-:\/@%+=]+$/.test(value))
|
|
4696
|
+
return value;
|
|
4697
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4698
|
+
}
|
|
3330
4699
|
async function handleLogoutAction(options) {
|
|
3331
4700
|
try {
|
|
3332
4701
|
const clearOptions = {};
|
|
@@ -3413,27 +4782,39 @@ async function login(options) {
|
|
|
3413
4782
|
browser: options.browser !== false,
|
|
3414
4783
|
json: Boolean(options.json),
|
|
3415
4784
|
});
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
4785
|
+
const loginIdentity = await resolveLoginIdentity(token, apiUrl);
|
|
4786
|
+
const credentials = loginIdentity.credentials;
|
|
4787
|
+
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
4788
|
+
let chosenProfile;
|
|
4789
|
+
let renamed = false;
|
|
4790
|
+
if (explicitProfile) {
|
|
4791
|
+
chosenProfile = explicitProfile;
|
|
4792
|
+
}
|
|
4793
|
+
else {
|
|
4794
|
+
const picked = await pickProfileForLogin(loginIdentity);
|
|
4795
|
+
chosenProfile = picked.name;
|
|
4796
|
+
renamed = picked.renamed;
|
|
4797
|
+
}
|
|
4798
|
+
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
4799
|
+
const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
|
|
3425
4800
|
if (!options.json) {
|
|
3426
|
-
process.stdout.write(
|
|
4801
|
+
process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
|
|
4802
|
+
const hint = await buildPostLoginHint(profile);
|
|
4803
|
+
if (hint)
|
|
4804
|
+
process.stdout.write(hint);
|
|
3427
4805
|
}
|
|
3428
4806
|
return {
|
|
3429
4807
|
logged_in: true,
|
|
3430
4808
|
profile,
|
|
3431
4809
|
api_url: credentials.apiUrl,
|
|
3432
|
-
user:
|
|
3433
|
-
organization:
|
|
4810
|
+
user: loginIdentity.user,
|
|
4811
|
+
organization: loginIdentity.activeOrganization,
|
|
4812
|
+
organizations: loginIdentity.organizations,
|
|
4813
|
+
selection_required: loginIdentity.selectionRequired,
|
|
4814
|
+
skills_install: skillsInstall,
|
|
3434
4815
|
};
|
|
3435
4816
|
}
|
|
3436
|
-
async function
|
|
4817
|
+
async function applyAuthToken(options) {
|
|
3437
4818
|
const token = readOption(options.token);
|
|
3438
4819
|
if (!token) {
|
|
3439
4820
|
throw new OxygenError("missing_token", "Pass a CLI API token with --token.", {
|
|
@@ -3444,18 +4825,131 @@ async function useAuthToken(options) {
|
|
|
3444
4825
|
token,
|
|
3445
4826
|
apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
|
|
3446
4827
|
};
|
|
4828
|
+
const loginIdentity = await resolveLoginIdentity(token, credentials.apiUrl);
|
|
4829
|
+
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
4830
|
+
let chosenProfile;
|
|
4831
|
+
let renamed = false;
|
|
4832
|
+
if (explicitProfile) {
|
|
4833
|
+
chosenProfile = explicitProfile;
|
|
4834
|
+
}
|
|
4835
|
+
else {
|
|
4836
|
+
const picked = await pickProfileForLogin(loginIdentity);
|
|
4837
|
+
chosenProfile = picked.name;
|
|
4838
|
+
renamed = picked.renamed;
|
|
4839
|
+
}
|
|
4840
|
+
const profile = await saveCredentials(loginIdentity.credentials, process.env, { profile: chosenProfile });
|
|
4841
|
+
return {
|
|
4842
|
+
identity: loginIdentity.whoami,
|
|
4843
|
+
loginIdentity,
|
|
4844
|
+
credentials: loginIdentity.credentials,
|
|
4845
|
+
api_url: loginIdentity.credentials.apiUrl,
|
|
4846
|
+
profile,
|
|
4847
|
+
renamed,
|
|
4848
|
+
};
|
|
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";
|
|
3447
4878
|
const identity = await requestOxygen("/api/cli/whoami", { credentials });
|
|
3448
|
-
const
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
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);
|
|
3452
4883
|
return {
|
|
3453
|
-
identity,
|
|
3454
4884
|
credentials,
|
|
3455
|
-
|
|
3456
|
-
|
|
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
|
+
}
|
|
4906
|
+
function readEnvProfileName() {
|
|
4907
|
+
const value = process.env.OXYGEN_PROFILE?.trim();
|
|
4908
|
+
return value ? value : null;
|
|
4909
|
+
}
|
|
4910
|
+
function identityFromWhoami(response) {
|
|
4911
|
+
return {
|
|
4912
|
+
organization: storedOrganizationFromOption(response.organization),
|
|
4913
|
+
user: {
|
|
4914
|
+
id: response.user.id,
|
|
4915
|
+
email: response.user.email ?? null,
|
|
4916
|
+
},
|
|
4917
|
+
capturedAt: new Date().toISOString(),
|
|
4918
|
+
};
|
|
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,
|
|
3457
4935
|
};
|
|
3458
4936
|
}
|
|
4937
|
+
function isUserSessionToken(token) {
|
|
4938
|
+
return token.startsWith("oxy_user_");
|
|
4939
|
+
}
|
|
4940
|
+
async function buildPostLoginHint(activeProfile) {
|
|
4941
|
+
if (output.isTTY !== true || process.env.NO_COLOR) {
|
|
4942
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
4943
|
+
if (!state || state.profiles.length < 2)
|
|
4944
|
+
return "";
|
|
4945
|
+
return `\n Pin this terminal: eval "$(oxygen profiles env ${activeProfile})"\n\n`;
|
|
4946
|
+
}
|
|
4947
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
4948
|
+
if (!state || state.profiles.length < 2)
|
|
4949
|
+
return "";
|
|
4950
|
+
const styles = ansi(true);
|
|
4951
|
+
return `\n ${styles.dim("Pin this terminal:")} ${styles.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
|
|
4952
|
+
}
|
|
3459
4953
|
async function promptForToken(options) {
|
|
3460
4954
|
const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
|
|
3461
4955
|
if (options.json) {
|
|
@@ -3541,7 +5035,7 @@ async function waitForBrowserToken(session) {
|
|
|
3541
5035
|
}
|
|
3542
5036
|
function createCliLoginUrl(apiUrl) {
|
|
3543
5037
|
const url = new URL("/auth/sign-in", apiUrl);
|
|
3544
|
-
url.searchParams.set("redirect_url", "/settings/
|
|
5038
|
+
url.searchParams.set("redirect_url", "/settings/cli");
|
|
3545
5039
|
url.searchParams.set("source", "oxygen_cli");
|
|
3546
5040
|
return url.toString();
|
|
3547
5041
|
}
|
|
@@ -3593,21 +5087,87 @@ function startOxygenSpinner(message) {
|
|
|
3593
5087
|
},
|
|
3594
5088
|
};
|
|
3595
5089
|
}
|
|
3596
|
-
function
|
|
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
|
+
}
|
|
5151
|
+
function formatLoginSuccess(identity, credentials, profile, options = {}) {
|
|
3597
5152
|
const email = identity.user.email ?? identity.user.id;
|
|
3598
|
-
const
|
|
5153
|
+
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
5154
|
+
const orgSlug = identity.organization.slug ?? null;
|
|
5155
|
+
const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
|
|
3599
5156
|
const key = identity.apiKey
|
|
3600
5157
|
? `${identity.apiKey.tokenPrefix}...${identity.apiKey.tokenSuffix}`
|
|
3601
5158
|
: "stored";
|
|
3602
5159
|
const fingerprint = createHash("sha256")
|
|
3603
5160
|
.update(`oxygen-cli:${credentials.token}`)
|
|
3604
5161
|
.digest("hex");
|
|
3605
|
-
const
|
|
5162
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5163
|
+
const profileLabel = options.renamed
|
|
5164
|
+
? `${profile} ${styles.dim("(auto-renamed to avoid collision)")}`
|
|
5165
|
+
: profile;
|
|
3606
5166
|
// skipcq: JS-0820 — not a React component; rule misfire on array of tuples
|
|
3607
5167
|
const rows = [
|
|
3608
5168
|
["Account", email],
|
|
3609
5169
|
["Organization", org],
|
|
3610
|
-
["Profile",
|
|
5170
|
+
["Profile", profileLabel],
|
|
3611
5171
|
["API", credentials.apiUrl],
|
|
3612
5172
|
["Token", key],
|
|
3613
5173
|
["Fingerprint", formatFingerprint(fingerprint)],
|
|
@@ -3615,17 +5175,18 @@ function formatLoginSuccess(identity, credentials, profile) {
|
|
|
3615
5175
|
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
3616
5176
|
const wordmark = renderBox([
|
|
3617
5177
|
"",
|
|
3618
|
-
...OXYGEN_WORDMARK.split("\n").map(line =>
|
|
5178
|
+
...OXYGEN_WORDMARK.split("\n").map(line => styles.green(line)),
|
|
3619
5179
|
"",
|
|
3620
5180
|
]);
|
|
3621
5181
|
return [
|
|
3622
5182
|
"",
|
|
3623
5183
|
wordmark,
|
|
3624
5184
|
"",
|
|
3625
|
-
`${
|
|
5185
|
+
`${styles.green("[OK]")} ${styles.bold("CLI connected")}`,
|
|
3626
5186
|
"",
|
|
3627
|
-
...rows.map(([label, value]) => ` ${
|
|
5187
|
+
...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
|
|
3628
5188
|
"",
|
|
5189
|
+
...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
|
|
3629
5190
|
].join("\n");
|
|
3630
5191
|
}
|
|
3631
5192
|
function formatFingerprint(hex) {
|
|
@@ -3637,6 +5198,9 @@ function summarizeCredentialProfile(profile, active) {
|
|
|
3637
5198
|
active,
|
|
3638
5199
|
api_url: profile.apiUrl,
|
|
3639
5200
|
token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
|
|
5201
|
+
organization: profile.activeOrganization ?? profile.identity?.organization ?? null,
|
|
5202
|
+
user: profile.identity?.user ?? null,
|
|
5203
|
+
identity_captured_at: profile.identity?.capturedAt ?? null,
|
|
3640
5204
|
};
|
|
3641
5205
|
}
|
|
3642
5206
|
function createCredentialFingerprint(token) {
|
|
@@ -3645,68 +5209,190 @@ function createCredentialFingerprint(token) {
|
|
|
3645
5209
|
.digest("hex");
|
|
3646
5210
|
}
|
|
3647
5211
|
function formatProfilesList(data) {
|
|
3648
|
-
const
|
|
5212
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
3649
5213
|
if (data.profiles.length === 0) {
|
|
3650
5214
|
return [
|
|
3651
5215
|
"",
|
|
3652
|
-
`${
|
|
5216
|
+
`${styles.dim("No stored Oxygen CLI profiles.")}`,
|
|
3653
5217
|
"",
|
|
3654
5218
|
].join("\n");
|
|
3655
5219
|
}
|
|
3656
|
-
const
|
|
3657
|
-
|
|
5220
|
+
const nameWidth = Math.max(...data.profiles.map((profile) => profile.name.length));
|
|
5221
|
+
const orgWidth = Math.max(...data.profiles.map((profile) => formatProfileOrgCell(profile).length));
|
|
5222
|
+
const lines = [
|
|
3658
5223
|
"",
|
|
3659
|
-
`${
|
|
5224
|
+
`${styles.bold("Oxygen CLI Profiles")}`,
|
|
3660
5225
|
"",
|
|
3661
5226
|
...data.profiles.map((profile) => {
|
|
3662
5227
|
const marker = profile.active ? "*" : " ";
|
|
5228
|
+
const orgCell = formatProfileOrgCell(profile);
|
|
3663
5229
|
return [
|
|
3664
|
-
`${marker} ${profile.name.padEnd(
|
|
3665
|
-
|
|
3666
|
-
|
|
5230
|
+
`${marker} ${profile.name.padEnd(nameWidth)}`,
|
|
5231
|
+
orgCell.padEnd(orgWidth),
|
|
5232
|
+
styles.dim(profile.api_url),
|
|
5233
|
+
styles.dim(profile.token_fingerprint),
|
|
3667
5234
|
].join(" ");
|
|
3668
5235
|
}),
|
|
3669
5236
|
"",
|
|
3670
|
-
]
|
|
5237
|
+
];
|
|
5238
|
+
if (data.profiles.length >= 2) {
|
|
5239
|
+
lines.push(` ${styles.dim("Pin a terminal:")} ${styles.bold('eval "$(oxygen profiles env <profile>)"')}`);
|
|
5240
|
+
lines.push("");
|
|
5241
|
+
}
|
|
5242
|
+
return lines.join("\n");
|
|
5243
|
+
}
|
|
5244
|
+
function formatProfileOrgCell(profile) {
|
|
5245
|
+
if (!profile.organization)
|
|
5246
|
+
return "(unknown org — run `oxygen whoami` to refresh)";
|
|
5247
|
+
const { slug, name } = profile.organization;
|
|
5248
|
+
if (slug && slug !== name)
|
|
5249
|
+
return `${name} (${slug})`;
|
|
5250
|
+
return name || slug || profile.organization.id;
|
|
5251
|
+
}
|
|
5252
|
+
function formatProfileUseSuccess(profile, options) {
|
|
5253
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5254
|
+
const lines = [
|
|
5255
|
+
"",
|
|
5256
|
+
`${styles.green("[OK]")} ${styles.bold("CLI profile selected")}`,
|
|
5257
|
+
"",
|
|
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}`,
|
|
5262
|
+
"",
|
|
5263
|
+
];
|
|
5264
|
+
if (options.totalProfiles >= 2) {
|
|
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})"`)}`);
|
|
5267
|
+
lines.push("");
|
|
5268
|
+
}
|
|
5269
|
+
return lines.join("\n");
|
|
3671
5270
|
}
|
|
3672
|
-
function
|
|
3673
|
-
const
|
|
5271
|
+
function formatWhoami(identity, context) {
|
|
5272
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5273
|
+
const email = identity.user.email ?? identity.user.id;
|
|
5274
|
+
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
5275
|
+
const orgSlug = identity.organization.slug ?? null;
|
|
5276
|
+
const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
|
|
5277
|
+
const sourceLabel = describeProfileSource(context.source);
|
|
5278
|
+
const profileName = context.resolution.exists ? context.resolution.name : "(no stored profile)";
|
|
5279
|
+
const apiUrl = context.resolution.credentials?.apiUrl ?? defaultApiUrl();
|
|
5280
|
+
const rows = [
|
|
5281
|
+
["Account", email],
|
|
5282
|
+
["Organization", org],
|
|
5283
|
+
["Profile", `${profileName} ${styles.dim(`(${sourceLabel})`)}`],
|
|
5284
|
+
["API", apiUrl],
|
|
5285
|
+
];
|
|
5286
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
5287
|
+
const lines = [
|
|
5288
|
+
"",
|
|
5289
|
+
...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
|
|
5290
|
+
"",
|
|
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");
|
|
5297
|
+
} // skipcq: JS-C1002
|
|
5298
|
+
function formatProfilesCurrent(context) {
|
|
5299
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5300
|
+
const sourceLabel = describeProfileSource(context.source);
|
|
5301
|
+
if (!context.resolution.exists) {
|
|
5302
|
+
return [
|
|
5303
|
+
"",
|
|
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.")}`,
|
|
5307
|
+
"",
|
|
5308
|
+
].join("\n");
|
|
5309
|
+
}
|
|
5310
|
+
const credentials = context.resolution.credentials;
|
|
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)
|
|
5324
|
+
: "(unknown — run `oxygen whoami` to refresh)";
|
|
3674
5325
|
return [
|
|
3675
5326
|
"",
|
|
3676
|
-
`${
|
|
5327
|
+
`${styles.bold("Active Oxygen CLI Profile")}`,
|
|
3677
5328
|
"",
|
|
3678
|
-
` ${
|
|
3679
|
-
` ${
|
|
3680
|
-
` ${
|
|
5329
|
+
` ${styles.dim("Profile")} ${context.resolution.name}`,
|
|
5330
|
+
` ${styles.dim("Source")} ${sourceLabel}`,
|
|
5331
|
+
` ${styles.dim("Organization")} ${orgCell}`,
|
|
5332
|
+
` ${styles.dim("API")} ${credentials.apiUrl}`,
|
|
3681
5333
|
"",
|
|
3682
5334
|
].join("\n");
|
|
3683
|
-
}
|
|
5335
|
+
}
|
|
5336
|
+
function describeProfileSource(source) {
|
|
5337
|
+
switch (source) {
|
|
5338
|
+
case "flag": return "from --profile flag";
|
|
5339
|
+
case "env": return "from OXYGEN_PROFILE";
|
|
5340
|
+
case "file": return "from stored active profile";
|
|
5341
|
+
case "default": return "default fallback";
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
3684
5344
|
function formatLogoutSuccess(result) {
|
|
3685
|
-
const
|
|
5345
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
3686
5346
|
const removed = result.removedProfile
|
|
3687
5347
|
? `profile "${result.removedProfile}" removed`
|
|
3688
5348
|
: "removed";
|
|
3689
5349
|
return [
|
|
3690
5350
|
"",
|
|
3691
|
-
`${
|
|
5351
|
+
`${styles.green("[OK]")} ${styles.bold("CLI logged out")}`,
|
|
3692
5352
|
"",
|
|
3693
|
-
` ${
|
|
3694
|
-
` ${
|
|
5353
|
+
` ${styles.dim("Credentials")} ${removed}`,
|
|
5354
|
+
` ${styles.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
|
|
3695
5355
|
"",
|
|
3696
5356
|
].join("\n");
|
|
3697
5357
|
}
|
|
3698
5358
|
function formatUpdateSuccess(result) {
|
|
3699
|
-
const
|
|
5359
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
3700
5360
|
return [
|
|
3701
5361
|
"",
|
|
3702
|
-
`${
|
|
5362
|
+
`${styles.green("[OK]")} ${styles.bold(result.dry_run ? "CLI update command ready" : "CLI update completed")}`,
|
|
3703
5363
|
"",
|
|
3704
|
-
` ${
|
|
3705
|
-
` ${
|
|
3706
|
-
` ${
|
|
5364
|
+
` ${styles.dim("Current")} ${result.current_version}`,
|
|
5365
|
+
` ${styles.dim("Package")} ${result.package}`,
|
|
5366
|
+
` ${styles.dim("Command")} ${result.command}`,
|
|
3707
5367
|
"",
|
|
5368
|
+
...formatAutomaticSkillsInstallStatusLines(result.skills_install),
|
|
3708
5369
|
].join("\n");
|
|
3709
5370
|
} // skipcq: JS-C1002
|
|
5371
|
+
function formatAutomaticSkillsInstallStatusLines(result) {
|
|
5372
|
+
if (!result)
|
|
5373
|
+
return [];
|
|
5374
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
5375
|
+
if (!result.attempted) {
|
|
5376
|
+
if (result.reason === "dry_run")
|
|
5377
|
+
return [];
|
|
5378
|
+
return [
|
|
5379
|
+
`${styles.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
|
|
5380
|
+
"",
|
|
5381
|
+
];
|
|
5382
|
+
}
|
|
5383
|
+
if (result.ok) {
|
|
5384
|
+
return [
|
|
5385
|
+
`${styles.green("[OK]")} ${styles.bold("Oxygen skills installed")}`,
|
|
5386
|
+
"",
|
|
5387
|
+
];
|
|
5388
|
+
}
|
|
5389
|
+
return [
|
|
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}`,
|
|
5393
|
+
"",
|
|
5394
|
+
];
|
|
5395
|
+
}
|
|
3710
5396
|
function renderBox(lines) {
|
|
3711
5397
|
const width = Math.max(...lines.map(visibleLength), 0);
|
|
3712
5398
|
const border = `+${"-".repeat(width + 2)}+`;
|
|
@@ -3725,6 +5411,7 @@ function ansi(enabled) {
|
|
|
3725
5411
|
bold: wrap(1, 22), // skipcq: JS-0117 // skipcq: JS-W1035
|
|
3726
5412
|
dim: wrap(2, 22),
|
|
3727
5413
|
green: wrap(32, 39),
|
|
5414
|
+
yellow: wrap(33, 39),
|
|
3728
5415
|
};
|
|
3729
5416
|
}
|
|
3730
5417
|
function readOption(value) {
|
|
@@ -3739,6 +5426,62 @@ function readCsvOption(value) {
|
|
|
3739
5426
|
.map((entry) => entry.trim())
|
|
3740
5427
|
.filter(Boolean);
|
|
3741
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
|
+
}
|
|
3742
5485
|
function contextAssetsQuery(options) {
|
|
3743
5486
|
const query = new URLSearchParams();
|
|
3744
5487
|
if (readOption(options.type))
|
|
@@ -3752,6 +5495,20 @@ function contextAssetsQuery(options) {
|
|
|
3752
5495
|
const value = query.toString();
|
|
3753
5496
|
return value ? `?${value}` : "";
|
|
3754
5497
|
}
|
|
5498
|
+
function buildContextResolveBody(options) {
|
|
5499
|
+
const assetTypes = readCsvOption(options.assetType);
|
|
5500
|
+
const tags = readCsvOption(options.tags);
|
|
5501
|
+
const maxAssets = readNonNegativeInt(options.maxAssets);
|
|
5502
|
+
return {
|
|
5503
|
+
...(readOption(options.purpose) ? { purpose: readOption(options.purpose) } : {}),
|
|
5504
|
+
...(assetTypes.length > 0 ? { asset_types: assetTypes } : {}),
|
|
5505
|
+
...(readOption(options.assetStatus) ? { asset_status: readOption(options.assetStatus) } : {}),
|
|
5506
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
5507
|
+
...(options.includeArchived ? { include_archived: true } : {}),
|
|
5508
|
+
...(maxAssets !== undefined ? { max_assets: maxAssets } : {}),
|
|
5509
|
+
...(options.requireReady ? { require_ready: true } : {}),
|
|
5510
|
+
};
|
|
5511
|
+
}
|
|
3755
5512
|
function buildContextAssetUpsertBody(options) {
|
|
3756
5513
|
const asset = options.assetJson ? parseJsonObject(options.assetJson) : {};
|
|
3757
5514
|
const tags = readCsvOption(options.tags);
|