@oxygen-agent/cli 1.50.37 → 1.64.5
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/credentials.d.ts +25 -0
- package/dist/credentials.js +101 -3
- package/dist/index.js +746 -100
- package/dist/skills.d.ts +41 -0
- package/dist/skills.js +325 -0
- package/dist/update.d.ts +3 -0
- package/dist/update.js +7 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +2 -3
- 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/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.js +36 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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, resolveActiveProfile, saveCredentials, switchCredentialProfile, } 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 ]",
|
|
@@ -161,8 +164,10 @@ export function createProgram() {
|
|
|
161
164
|
.option("--profile <name>", "Use a stored CLI profile for this command.");
|
|
162
165
|
program.hook("preAction", () => {
|
|
163
166
|
const options = program.opts();
|
|
164
|
-
if (options.profile)
|
|
167
|
+
if (options.profile) {
|
|
168
|
+
globalProfileFlag = options.profile;
|
|
165
169
|
process.env.OXYGEN_PROFILE = options.profile;
|
|
170
|
+
}
|
|
166
171
|
});
|
|
167
172
|
program
|
|
168
173
|
.command("login")
|
|
@@ -209,6 +214,20 @@ export function createProgram() {
|
|
|
209
214
|
.option("--json", "Print a JSON envelope.")
|
|
210
215
|
.action(async (profile, options) => {
|
|
211
216
|
await handleLogoutAction({ ...options, profile });
|
|
217
|
+
}))
|
|
218
|
+
.addCommand(new Command("env")
|
|
219
|
+
.description("Print shell `export` lines that pin this terminal to a stored CLI profile.")
|
|
220
|
+
.argument("<profile>", "Stored profile name.")
|
|
221
|
+
.option("--unset", "Print `unset` lines instead of `export` lines.")
|
|
222
|
+
.option("--json", "Print a JSON envelope.")
|
|
223
|
+
.action(async (profile, options) => {
|
|
224
|
+
await handleProfilesEnvAction(profile, options);
|
|
225
|
+
}))
|
|
226
|
+
.addCommand(new Command("current")
|
|
227
|
+
.description("Show the profile the next command will use, and where it came from.")
|
|
228
|
+
.option("--json", "Print a JSON envelope.")
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
await handleProfilesCurrentAction(options);
|
|
212
231
|
}));
|
|
213
232
|
program
|
|
214
233
|
.command("logout")
|
|
@@ -263,7 +282,7 @@ export function createProgram() {
|
|
|
263
282
|
.description("Show the current Oxygen CLI identity.")
|
|
264
283
|
.option("--json", "Print a JSON envelope.")
|
|
265
284
|
.action(async (options) => {
|
|
266
|
-
await
|
|
285
|
+
await handleWhoamiAction(options);
|
|
267
286
|
});
|
|
268
287
|
program
|
|
269
288
|
.command("status")
|
|
@@ -527,6 +546,25 @@ export function createProgram() {
|
|
|
527
546
|
.option("--json", "Print a JSON envelope.")
|
|
528
547
|
.action(async (table, options) => {
|
|
529
548
|
await handleAsyncAction("tables export", options, async () => exportRows(table, options));
|
|
549
|
+
}))
|
|
550
|
+
.addCommand(new Command("export-bundle")
|
|
551
|
+
.description("Export a workspace table as a portable bundle (schema + every row) for cross-environment / cross-org copies.")
|
|
552
|
+
.argument("<table>", "Table id or slug to export.")
|
|
553
|
+
.option("--output <path>", "Write the bundle JSON to a file. Defaults to stdout.")
|
|
554
|
+
.option("--page-size <n>", "Rows per cursor-paginated request. Defaults to 500; hard cap is 1000.")
|
|
555
|
+
.option("--json", "Print a JSON envelope (omit row payload — use --output to keep the rows).")
|
|
556
|
+
.action(async (table, options) => {
|
|
557
|
+
await handleAsyncAction("tables export-bundle", options, async () => exportTableBundle(table, options));
|
|
558
|
+
}))
|
|
559
|
+
.addCommand(new Command("import-bundle")
|
|
560
|
+
.description("Recreate a workspace table from an export-bundle file in this org. Restores columns (incl. enrichment/tool definitions) and inserts every row.")
|
|
561
|
+
.requiredOption("--file <path>", "Bundle JSON file produced by `tables export-bundle`.")
|
|
562
|
+
.option("--name <name>", "Override the table display name. Defaults to the bundle's table name.")
|
|
563
|
+
.option("--project <project>", "Project id or slug for the new table. Defaults to General.")
|
|
564
|
+
.option("--batch-size <n>", "Rows per insert request. Defaults to 500; paid orgs may use up to 5000.")
|
|
565
|
+
.option("--json", "Print a JSON envelope.")
|
|
566
|
+
.action(async (options) => {
|
|
567
|
+
await handleAsyncAction("tables import-bundle", options, async () => importTableBundle(options));
|
|
530
568
|
}))
|
|
531
569
|
.addCommand(new Command("list")
|
|
532
570
|
.description("List workspace tables in the current tenant database.")
|
|
@@ -629,6 +667,23 @@ export function createProgram() {
|
|
|
629
667
|
method: "POST",
|
|
630
668
|
body: { table, project: options.project },
|
|
631
669
|
}));
|
|
670
|
+
}))
|
|
671
|
+
.addCommand(new Command("recover-pending")
|
|
672
|
+
.description("Re-poll BetterContact async enrichments that timed out mid-poll and write any newly-found phones back to the table.")
|
|
673
|
+
.argument("<table>", "Table id or slug.")
|
|
674
|
+
.option("--max-items <n>", "Max pending cache rows to poll in this run. Defaults to 1000.")
|
|
675
|
+
.option("--json", "Print a JSON envelope.")
|
|
676
|
+
.action(async (table, options) => {
|
|
677
|
+
await handleAsyncAction("tables recover-pending", options, async () => {
|
|
678
|
+
const maxItems = options.maxItems ? Number.parseInt(options.maxItems, 10) : undefined;
|
|
679
|
+
return requestOxygen("/api/cli/tables/recover-pending", {
|
|
680
|
+
method: "POST",
|
|
681
|
+
body: {
|
|
682
|
+
table,
|
|
683
|
+
...(Number.isFinite(maxItems) ? { max_items: maxItems } : {}),
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
});
|
|
632
687
|
}));
|
|
633
688
|
tablesCommand.addCommand(new Command("webhook")
|
|
634
689
|
.description("Create and manage direct table webhooks.")
|
|
@@ -661,6 +716,22 @@ export function createProgram() {
|
|
|
661
716
|
program
|
|
662
717
|
.command("context")
|
|
663
718
|
.description("Workspace-level GTM context commands.")
|
|
719
|
+
.addCommand(new Command("resolve")
|
|
720
|
+
.description("Resolve task-scoped workspace GTM context with readiness and revision provenance.")
|
|
721
|
+
.option("--purpose <purpose>", "general, lead_sourcing, qualification, outbound_copy, or workflow_design.")
|
|
722
|
+
.option("--asset-type <csv>", "Comma-separated context asset types to include.")
|
|
723
|
+
.option("--asset-status <status>", "draft, active, archived, or all. Defaults to active.")
|
|
724
|
+
.option("--tags <csv>", "Comma-separated asset tags that must be present.")
|
|
725
|
+
.option("--include-archived", "Include archived assets when no asset status is set.")
|
|
726
|
+
.option("--max-assets <n>", "Maximum assets to include. Defaults to 10, max 50.")
|
|
727
|
+
.option("--require-ready", "Exit with a conflict error when required context sections are missing.")
|
|
728
|
+
.option("--json", "Print a JSON envelope.")
|
|
729
|
+
.action(async (options) => {
|
|
730
|
+
await handleAsyncAction("context resolve", options, async () => requestOxygen("/api/cli/context/resolve", {
|
|
731
|
+
method: "POST",
|
|
732
|
+
body: buildContextResolveBody(options),
|
|
733
|
+
}));
|
|
734
|
+
}))
|
|
664
735
|
.addCommand(new Command("profile")
|
|
665
736
|
.description("Company profile and ICP memory.")
|
|
666
737
|
.addCommand(new Command("get")
|
|
@@ -872,15 +943,25 @@ export function createProgram() {
|
|
|
872
943
|
.addCommand(new Command("add")
|
|
873
944
|
.description("Add a nullable column to a workspace table.")
|
|
874
945
|
.argument("<table>", "Table id or slug.")
|
|
875
|
-
.
|
|
946
|
+
.option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
|
|
876
947
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
877
948
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
878
949
|
.option("--kind <kind>", "Column kind. Defaults to manual.")
|
|
879
950
|
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
880
951
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata.")
|
|
952
|
+
.option("--prompt-key <key>", "OXYGEN prompt-library key (e.g. email_draft_v1). Materializes prompt + output_schema and forces kind=ai.")
|
|
953
|
+
.option("--input-mapping <json>", "Required with --prompt-key. JSON object mapping prompt input names to column or literal refs.")
|
|
881
954
|
.option("--json", "Print a JSON envelope.")
|
|
882
955
|
.action(async (table, options) => {
|
|
883
|
-
|
|
956
|
+
if (!options.promptKey && !options.label) {
|
|
957
|
+
throw new OxygenError("invalid_request", "--label is required.", { exitCode: 1 });
|
|
958
|
+
}
|
|
959
|
+
if (options.promptKey && !options.inputMapping) {
|
|
960
|
+
throw new OxygenError("missing_input_mapping", "--input-mapping is required when --prompt-key is provided.", { exitCode: 1 });
|
|
961
|
+
}
|
|
962
|
+
const column = {};
|
|
963
|
+
if (options.label)
|
|
964
|
+
column.label = options.label;
|
|
884
965
|
if (options.key)
|
|
885
966
|
column.key = options.key;
|
|
886
967
|
if (options.dataType)
|
|
@@ -891,12 +972,14 @@ export function createProgram() {
|
|
|
891
972
|
column.semantic_type = options.semanticType;
|
|
892
973
|
if (options.definitionJson)
|
|
893
974
|
column.definition = parseJsonObject(options.definitionJson);
|
|
975
|
+
const body = { table, column };
|
|
976
|
+
if (options.promptKey)
|
|
977
|
+
body.prompt_key = options.promptKey;
|
|
978
|
+
if (options.inputMapping)
|
|
979
|
+
body.input_mapping = parseJsonObject(options.inputMapping);
|
|
894
980
|
await handleAsyncAction("columns add", options, async () => requestOxygen("/api/cli/tables/columns", {
|
|
895
981
|
method: "POST",
|
|
896
|
-
body
|
|
897
|
-
table,
|
|
898
|
-
column,
|
|
899
|
-
},
|
|
982
|
+
body,
|
|
900
983
|
}));
|
|
901
984
|
}))
|
|
902
985
|
.addCommand(new Command("run")
|
|
@@ -911,6 +994,7 @@ export function createProgram() {
|
|
|
911
994
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
912
995
|
.option("--background", "Create a durable background table action run instead of executing synchronously.")
|
|
913
996
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for a background run.")
|
|
997
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run. Defaults to 50.")
|
|
914
998
|
.option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
|
|
915
999
|
.option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
|
|
916
1000
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -918,6 +1002,7 @@ export function createProgram() {
|
|
|
918
1002
|
const limit = readPositiveInt(options.limit);
|
|
919
1003
|
const localConcurrency = readPositiveInt(options.localConcurrency);
|
|
920
1004
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
1005
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
921
1006
|
const filterSelection = readFilterSelectionOption(options.filterJson);
|
|
922
1007
|
const selectedModes = [
|
|
923
1008
|
Boolean(options.all),
|
|
@@ -967,6 +1052,7 @@ export function createProgram() {
|
|
|
967
1052
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
968
1053
|
...(options.background ? { background: true } : {}),
|
|
969
1054
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1055
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
970
1056
|
},
|
|
971
1057
|
});
|
|
972
1058
|
});
|
|
@@ -1129,10 +1215,12 @@ export function createProgram() {
|
|
|
1129
1215
|
.option("--force", "Run even when the target cell already has a value.")
|
|
1130
1216
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
1131
1217
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
1218
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
|
|
1132
1219
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
1133
1220
|
.option("--json", "Print a JSON envelope.")
|
|
1134
1221
|
.action(async (table, options) => {
|
|
1135
1222
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
1223
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
1136
1224
|
await handleAsyncAction("table-runs create", options, async () => requestOxygen("/api/cli/table-action-runs", {
|
|
1137
1225
|
method: "POST",
|
|
1138
1226
|
body: {
|
|
@@ -1142,6 +1230,7 @@ export function createProgram() {
|
|
|
1142
1230
|
...(options.force ? { force: true } : {}),
|
|
1143
1231
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
1144
1232
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1233
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1145
1234
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
1146
1235
|
},
|
|
1147
1236
|
}));
|
|
@@ -1427,7 +1516,7 @@ export function createProgram() {
|
|
|
1427
1516
|
}))
|
|
1428
1517
|
.addCommand(new Command("failures")
|
|
1429
1518
|
.description("List failed background action and ingestion items.")
|
|
1430
|
-
.option("--queue <queue>", "all, actions, or
|
|
1519
|
+
.option("--queue <queue>", "all, actions, ingestions, or bullmq. Defaults to all.")
|
|
1431
1520
|
.option("--limit <n>", "Maximum failed items per queue. Defaults to 25; server cap is 100.")
|
|
1432
1521
|
.option("--json", "Print a JSON envelope.")
|
|
1433
1522
|
.action(async (options) => {
|
|
@@ -1533,6 +1622,7 @@ export function createProgram() {
|
|
|
1533
1622
|
.addCommand(new Command("costs")
|
|
1534
1623
|
.description("Show provider costs (COGS) per workspace. Staff only.")
|
|
1535
1624
|
.option("--top <n>", "Limit number of workspace columns. Defaults to all.")
|
|
1625
|
+
.option("--credential-mode <mode>", "managed (default; real COGS), byok (customer-supplied credentials), or all.", "managed")
|
|
1536
1626
|
.option("--json", "Print a JSON envelope.")
|
|
1537
1627
|
.action(async (options) => {
|
|
1538
1628
|
await handleAsyncAction("admin costs", options, async () => {
|
|
@@ -1540,6 +1630,10 @@ export function createProgram() {
|
|
|
1540
1630
|
const top = readPositiveInt(options.top);
|
|
1541
1631
|
if (top)
|
|
1542
1632
|
params.set("top", String(top));
|
|
1633
|
+
const credentialMode = readOption(options.credentialMode);
|
|
1634
|
+
if (credentialMode && credentialMode !== "managed") {
|
|
1635
|
+
params.set("credential_mode", credentialMode);
|
|
1636
|
+
}
|
|
1543
1637
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
1544
1638
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
1545
1639
|
});
|
|
@@ -1621,6 +1715,27 @@ export function createProgram() {
|
|
|
1621
1715
|
program
|
|
1622
1716
|
.command("cells")
|
|
1623
1717
|
.description("Workspace cell provenance commands.")
|
|
1718
|
+
.addCommand(new Command("inspect")
|
|
1719
|
+
.description("Inspect one cell's value, row context, and recent history.")
|
|
1720
|
+
.argument("<table>", "Table id or slug.")
|
|
1721
|
+
.argument("<row_id>", "Workspace row UUID.")
|
|
1722
|
+
.argument("<column>", "Column id or key.")
|
|
1723
|
+
.option("--history-limit <n>", "Maximum recent cell changes to include. Defaults to 10; cap 50.")
|
|
1724
|
+
.option("--json", "Print a JSON envelope.")
|
|
1725
|
+
.action(async (table, rowId, column, options) => {
|
|
1726
|
+
await handleAsyncAction("cells inspect", options, async () => {
|
|
1727
|
+
const historyLimit = readPositiveInt(options.historyLimit);
|
|
1728
|
+
return requestOxygen("/api/cli/tables/cells/inspect", {
|
|
1729
|
+
method: "POST",
|
|
1730
|
+
body: {
|
|
1731
|
+
table,
|
|
1732
|
+
row_id: rowId,
|
|
1733
|
+
column,
|
|
1734
|
+
...(historyLimit ? { history_limit: historyLimit } : {}),
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
});
|
|
1738
|
+
}))
|
|
1624
1739
|
.addCommand(new Command("history")
|
|
1625
1740
|
.description("Show cell change history.")
|
|
1626
1741
|
.argument("<table>", "Table id or slug.")
|
|
@@ -1719,7 +1834,7 @@ export function createProgram() {
|
|
|
1719
1834
|
.addCommand(new Command("search")
|
|
1720
1835
|
.description("Search the tool catalog.")
|
|
1721
1836
|
.argument("[query]", "Search text.")
|
|
1722
|
-
.option("--verbosity <verbosity>", "summary or full. Defaults to summary.")
|
|
1837
|
+
.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
1838
|
.option("--only-runnable", "Only return tools runnable by the active organization.")
|
|
1724
1839
|
.option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
|
|
1725
1840
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -1831,13 +1946,16 @@ export function createProgram() {
|
|
|
1831
1946
|
.option("--selection-json <json>", "Full row selection object for server-side row selection.")
|
|
1832
1947
|
.option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
|
|
1833
1948
|
.option("--force", "Re-run rows with an existing target enrichment value.")
|
|
1949
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run. Defaults to 20.")
|
|
1834
1950
|
.option("--json", "Print a JSON envelope.")
|
|
1835
1951
|
.action(async (table, options) => {
|
|
1952
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
1836
1953
|
await handleAsyncAction("enrich-column run", options, async () => requestOxygen("/api/cli/enrich-column/run", {
|
|
1837
1954
|
method: "POST",
|
|
1838
1955
|
body: {
|
|
1839
1956
|
...buildEnrichColumnBody(table, options),
|
|
1840
1957
|
max_credits: readPositiveNumber(options.maxCredits),
|
|
1958
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1841
1959
|
...(options.force ? { force: true } : {}),
|
|
1842
1960
|
},
|
|
1843
1961
|
}));
|
|
@@ -2281,11 +2399,25 @@ export function createProgram() {
|
|
|
2281
2399
|
}));
|
|
2282
2400
|
program
|
|
2283
2401
|
.command("skills")
|
|
2284
|
-
.description("Agent skill installation commands.")
|
|
2402
|
+
.description("Agent skill discovery and installation commands.")
|
|
2403
|
+
.addCommand(new Command("list")
|
|
2404
|
+
.description("List Oxygen agent skills available from the public skill index.")
|
|
2405
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2406
|
+
.option("--json", "Print a JSON envelope.")
|
|
2407
|
+
.action(async (options) => {
|
|
2408
|
+
await handleAsyncAction("skills list", options, async () => listAgentSkills(options));
|
|
2409
|
+
}))
|
|
2410
|
+
.addCommand(new Command("doctor")
|
|
2411
|
+
.description("Check Oxygen skill index reachability and local installer prerequisites.")
|
|
2412
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2413
|
+
.option("--json", "Print a JSON envelope.")
|
|
2414
|
+
.action(async (options) => {
|
|
2415
|
+
await handleAsyncAction("skills doctor", options, async () => doctorAgentSkills(options));
|
|
2416
|
+
}))
|
|
2285
2417
|
.addCommand(new Command("install")
|
|
2286
2418
|
.description("Install Oxygen agent skills into local agent skill directories.")
|
|
2287
2419
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2288
|
-
.option("--agents <agents
|
|
2420
|
+
.option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
|
|
2289
2421
|
.option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
|
|
2290
2422
|
.option("--project", "Install into the current project instead of global agent scope.")
|
|
2291
2423
|
.option("--copy", "Copy skill files instead of symlinking when supported by npx skills.")
|
|
@@ -2721,58 +2853,6 @@ function normalizeSessionStepStatus(value) {
|
|
|
2721
2853
|
exitCode: 1,
|
|
2722
2854
|
});
|
|
2723
2855
|
}
|
|
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
2856
|
async function importRows(table, options) {
|
|
2777
2857
|
const format = normalizeRowsFormat(options.format, inferRowsFileFormat(options.file));
|
|
2778
2858
|
const parsedRows = await readRowsFile(options.file, format, options.sheet);
|
|
@@ -2922,6 +3002,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
|
|
|
2922
3002
|
},
|
|
2923
3003
|
});
|
|
2924
3004
|
const ingestionRunId = readRequiredResponseString(created, "id");
|
|
3005
|
+
const queueWait = readRecord(created, "queue_wait");
|
|
2925
3006
|
if (isTerminalTableIngestionStatus(readRecordString(created, "status"))) {
|
|
2926
3007
|
return {
|
|
2927
3008
|
ingestionRun: created,
|
|
@@ -2940,6 +3021,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
|
|
|
2940
3021
|
mode: upsertKey ? "upsert" : "insert",
|
|
2941
3022
|
upsertKey: upsertKey ?? null,
|
|
2942
3023
|
idempotencyKey,
|
|
3024
|
+
...(queueWait ? { queue_wait: queueWait } : {}),
|
|
2943
3025
|
};
|
|
2944
3026
|
}
|
|
2945
3027
|
let insertedItems = 0;
|
|
@@ -2961,6 +3043,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
|
|
|
2961
3043
|
duplicatePositions += readCount(appended.duplicatePositions);
|
|
2962
3044
|
latestRun = appended.run ?? latestRun;
|
|
2963
3045
|
}
|
|
3046
|
+
emitQueueWaitStderrNote(queueWait);
|
|
2964
3047
|
return {
|
|
2965
3048
|
ingestionRun: latestRun,
|
|
2966
3049
|
ingestionRunId,
|
|
@@ -2978,8 +3061,16 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
|
|
|
2978
3061
|
mode: upsertKey ? "upsert" : "insert",
|
|
2979
3062
|
upsertKey: upsertKey ?? null,
|
|
2980
3063
|
idempotencyKey,
|
|
3064
|
+
...(queueWait ? { queue_wait: queueWait } : {}),
|
|
2981
3065
|
};
|
|
2982
3066
|
}
|
|
3067
|
+
function emitQueueWaitStderrNote(queueWait) {
|
|
3068
|
+
if (!queueWait)
|
|
3069
|
+
return;
|
|
3070
|
+
const note = readRecordString(queueWait, "note");
|
|
3071
|
+
if (note)
|
|
3072
|
+
process.stderr.write(`note: ${note}\n`);
|
|
3073
|
+
}
|
|
2983
3074
|
async function waitForTableIngestionRun(runId, options) {
|
|
2984
3075
|
const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
|
|
2985
3076
|
?? TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS;
|
|
@@ -3079,6 +3170,246 @@ async function exportRows(table, options) {
|
|
|
3079
3170
|
...(formatted.rescuedCount > 0 ? { rescuedNumericCells: formatted.rescuedCount } : {}),
|
|
3080
3171
|
};
|
|
3081
3172
|
}
|
|
3173
|
+
const TABLE_BUNDLE_SCHEMA_VERSION = 1;
|
|
3174
|
+
const TABLE_BUNDLE_MAX_PAGE_SIZE = 1000;
|
|
3175
|
+
const TABLE_BUNDLE_DEFAULT_PAGE_SIZE = 500;
|
|
3176
|
+
async function exportTableBundle(table, options) {
|
|
3177
|
+
const pageSize = Math.min(readPositiveInt(options.pageSize) ?? TABLE_BUNDLE_DEFAULT_PAGE_SIZE, TABLE_BUNDLE_MAX_PAGE_SIZE);
|
|
3178
|
+
// Pull the canonical schema (incl. enrichment/tool definitions and
|
|
3179
|
+
// semantic types) from `describe`; the paginated query response only
|
|
3180
|
+
// surfaces the user-visible projection.
|
|
3181
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
|
|
3182
|
+
const tableMeta = describe.table ?? null;
|
|
3183
|
+
const columns = (describe.columns ?? []).map(toBundleColumn);
|
|
3184
|
+
const rows = [];
|
|
3185
|
+
let cursor = null;
|
|
3186
|
+
let expectedTotal = null;
|
|
3187
|
+
let pageCount = 0;
|
|
3188
|
+
let hasMoreFlag = false;
|
|
3189
|
+
do {
|
|
3190
|
+
const requestBody = { table, limit: pageSize };
|
|
3191
|
+
if (cursor)
|
|
3192
|
+
requestBody.cursor = cursor;
|
|
3193
|
+
const page = await requestOxygen("/api/cli/tables/query", { method: "POST", body: requestBody });
|
|
3194
|
+
pageCount += 1;
|
|
3195
|
+
if (expectedTotal === null) {
|
|
3196
|
+
expectedTotal = readBundleNumber(page.totalCount) ?? readBundleNumber(page.total_count) ?? null;
|
|
3197
|
+
}
|
|
3198
|
+
for (const row of page.rows ?? [])
|
|
3199
|
+
rows.push(row);
|
|
3200
|
+
cursor = typeof page.nextCursor === "string" && page.nextCursor.length > 0 ? page.nextCursor : null;
|
|
3201
|
+
hasMoreFlag = Boolean(page.hasMore);
|
|
3202
|
+
// Defensive: hasMore=false should always mean cursor=null. If a future
|
|
3203
|
+
// server change ever sets one without the other, stop iterating on
|
|
3204
|
+
// hasMore=false so we don't loop forever.
|
|
3205
|
+
if (!hasMoreFlag)
|
|
3206
|
+
cursor = null;
|
|
3207
|
+
} while (cursor);
|
|
3208
|
+
if (expectedTotal !== null && rows.length !== expectedTotal) {
|
|
3209
|
+
throw new OxygenError("bundle_export_incomplete", "Cursor pagination ended before every row was written. The bundle would be a silent under-export.", {
|
|
3210
|
+
details: {
|
|
3211
|
+
table,
|
|
3212
|
+
totalCount: expectedTotal,
|
|
3213
|
+
exportedCount: rows.length,
|
|
3214
|
+
missing: expectedTotal - rows.length,
|
|
3215
|
+
pages: pageCount,
|
|
3216
|
+
},
|
|
3217
|
+
exitCode: 1,
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
const tableId = readRecordString(tableMeta, "id");
|
|
3221
|
+
const tableSlug = readRecordString(tableMeta, "slug");
|
|
3222
|
+
const projectSlug = readRecordString(tableMeta, "projectSlug");
|
|
3223
|
+
const bundle = {
|
|
3224
|
+
schemaVersion: TABLE_BUNDLE_SCHEMA_VERSION,
|
|
3225
|
+
exportedAt: new Date().toISOString(),
|
|
3226
|
+
table: {
|
|
3227
|
+
id: tableId ?? null,
|
|
3228
|
+
slug: tableSlug ?? null,
|
|
3229
|
+
name: readRecordString(tableMeta, "displayName") ?? readRecordString(tableMeta, "name") ?? null,
|
|
3230
|
+
projectSlug: projectSlug ?? null,
|
|
3231
|
+
},
|
|
3232
|
+
columns,
|
|
3233
|
+
rows,
|
|
3234
|
+
totals: {
|
|
3235
|
+
rowCount: rows.length,
|
|
3236
|
+
...(expectedTotal !== null ? { sourceTotalCount: expectedTotal } : {}),
|
|
3237
|
+
pages: pageCount,
|
|
3238
|
+
pageSize,
|
|
3239
|
+
},
|
|
3240
|
+
};
|
|
3241
|
+
if (options.output) {
|
|
3242
|
+
writeFileSync(options.output, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
3243
|
+
}
|
|
3244
|
+
const summary = {
|
|
3245
|
+
table_id: tableId ?? null,
|
|
3246
|
+
table_slug: tableSlug ?? null,
|
|
3247
|
+
schema_version: TABLE_BUNDLE_SCHEMA_VERSION,
|
|
3248
|
+
column_count: columns.length,
|
|
3249
|
+
row_count: rows.length,
|
|
3250
|
+
pages: pageCount,
|
|
3251
|
+
page_size: pageSize,
|
|
3252
|
+
output: options.output ?? null,
|
|
3253
|
+
...(tableId ? { web_url: tableWebUrl(tableId) } : tableSlug ? { web_url: tableWebUrl(tableSlug) } : {}),
|
|
3254
|
+
};
|
|
3255
|
+
// When piping to stdout, also emit the full bundle so it can be redirected
|
|
3256
|
+
// into a file. With --output, the file is the source of truth; keep the
|
|
3257
|
+
// stdout response a small summary so it's readable.
|
|
3258
|
+
return options.output ? summary : { ...summary, bundle };
|
|
3259
|
+
}
|
|
3260
|
+
async function importTableBundle(options) {
|
|
3261
|
+
const path = options.file;
|
|
3262
|
+
if (!path || !path.trim()) {
|
|
3263
|
+
throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
|
|
3264
|
+
}
|
|
3265
|
+
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
3266
|
+
const raw = readFileSync(resolve(path), "utf8");
|
|
3267
|
+
const bundle = parseBundleFile(raw);
|
|
3268
|
+
const columns = bundle.columns.map(bundleColumnToCreateInput);
|
|
3269
|
+
if (columns.length === 0) {
|
|
3270
|
+
throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
|
|
3271
|
+
}
|
|
3272
|
+
const tableName = readOption(options.name) ?? bundle.tableName;
|
|
3273
|
+
if (!tableName) {
|
|
3274
|
+
throw new OxygenError("invalid_bundle", "Bundle has no table.name and no --name override was provided.", { exitCode: 1 });
|
|
3275
|
+
}
|
|
3276
|
+
const createResponse = await requestOxygen("/api/cli/tables", {
|
|
3277
|
+
method: "POST",
|
|
3278
|
+
body: {
|
|
3279
|
+
name: tableName,
|
|
3280
|
+
columns,
|
|
3281
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
3282
|
+
},
|
|
3283
|
+
});
|
|
3284
|
+
const createdTable = readRecord(createResponse, "table");
|
|
3285
|
+
const newTableId = readRecordString(createResponse, "table_id")
|
|
3286
|
+
?? (createdTable ? readRecordString(createdTable, "id") : null);
|
|
3287
|
+
const newTableSlug = readRecordString(createResponse, "table_slug")
|
|
3288
|
+
?? (createdTable ? readRecordString(createdTable, "slug") : null);
|
|
3289
|
+
if (!newTableId) {
|
|
3290
|
+
throw new OxygenError("invalid_response", "Table create response did not include an id; bundle import cannot proceed.", { exitCode: 1 });
|
|
3291
|
+
}
|
|
3292
|
+
const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
|
|
3293
|
+
const stagedRows = bundle.rows.map((row) => stripRowForImport(row, validColumnKeys));
|
|
3294
|
+
let inserted = 0;
|
|
3295
|
+
for (let offset = 0; offset < stagedRows.length; offset += batchSize) {
|
|
3296
|
+
const batch = stagedRows.slice(offset, offset + batchSize);
|
|
3297
|
+
if (batch.length === 0)
|
|
3298
|
+
continue;
|
|
3299
|
+
await requestOxygen("/api/cli/tables/rows", {
|
|
3300
|
+
method: "POST",
|
|
3301
|
+
body: { table: newTableId, rows: batch },
|
|
3302
|
+
});
|
|
3303
|
+
inserted += batch.length;
|
|
3304
|
+
}
|
|
3305
|
+
return {
|
|
3306
|
+
table_id: newTableId,
|
|
3307
|
+
table_slug: newTableSlug ?? null,
|
|
3308
|
+
column_count: columns.length,
|
|
3309
|
+
row_count: inserted,
|
|
3310
|
+
source_table: bundle.tableSummary,
|
|
3311
|
+
schema_version: bundle.schemaVersion,
|
|
3312
|
+
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
function toBundleColumn(column) {
|
|
3316
|
+
// Persist only the fields `tables create` knows how to ingest. Drop ids,
|
|
3317
|
+
// physical column names, archivedAt — those are tenant-local and would
|
|
3318
|
+
// collide on import.
|
|
3319
|
+
return {
|
|
3320
|
+
key: column.key,
|
|
3321
|
+
...(column.label !== undefined ? { label: column.label } : {}),
|
|
3322
|
+
...(column.dataType !== undefined ? { dataType: column.dataType } : {}),
|
|
3323
|
+
...(column.kind !== undefined ? { kind: column.kind } : {}),
|
|
3324
|
+
...(column.semanticType !== undefined ? { semanticType: column.semanticType } : {}),
|
|
3325
|
+
...(column.definition !== undefined ? { definition: column.definition } : {}),
|
|
3326
|
+
...(column.position !== undefined ? { position: column.position } : {}),
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
function bundleColumnToCreateInput(column) {
|
|
3330
|
+
const label = typeof column.label === "string" && column.label.trim()
|
|
3331
|
+
? column.label.trim()
|
|
3332
|
+
: typeof column.key === "string"
|
|
3333
|
+
? column.key
|
|
3334
|
+
: null;
|
|
3335
|
+
if (!label) {
|
|
3336
|
+
throw new OxygenError("invalid_bundle", "Bundle column is missing label/key.", {
|
|
3337
|
+
details: { column },
|
|
3338
|
+
exitCode: 1,
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
return {
|
|
3342
|
+
label,
|
|
3343
|
+
...(typeof column.key === "string" ? { key: column.key } : {}),
|
|
3344
|
+
...(typeof column.dataType === "string" ? { dataType: column.dataType } : {}),
|
|
3345
|
+
...(typeof column.kind === "string" ? { kind: column.kind } : {}),
|
|
3346
|
+
...(typeof column.semanticType === "string" ? { semanticType: column.semanticType } : {}),
|
|
3347
|
+
...(column.definition && typeof column.definition === "object" && !Array.isArray(column.definition)
|
|
3348
|
+
? { definition: column.definition }
|
|
3349
|
+
: {}),
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
function parseBundleFile(text) {
|
|
3353
|
+
let parsed;
|
|
3354
|
+
try {
|
|
3355
|
+
parsed = JSON.parse(text);
|
|
3356
|
+
}
|
|
3357
|
+
catch (error) {
|
|
3358
|
+
throw new OxygenError("invalid_bundle", "Bundle file is not valid JSON.", {
|
|
3359
|
+
details: { reason: error instanceof Error ? error.message : "unknown" },
|
|
3360
|
+
exitCode: 1,
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3364
|
+
throw new OxygenError("invalid_bundle", "Bundle file must be a JSON object.", { exitCode: 1 });
|
|
3365
|
+
}
|
|
3366
|
+
const record = parsed;
|
|
3367
|
+
const schemaVersion = typeof record.schemaVersion === "number" ? record.schemaVersion : 1;
|
|
3368
|
+
if (schemaVersion !== TABLE_BUNDLE_SCHEMA_VERSION) {
|
|
3369
|
+
throw new OxygenError("unsupported_bundle_version", `Bundle schema version ${schemaVersion} is not supported by this CLI.`, {
|
|
3370
|
+
details: { supported: TABLE_BUNDLE_SCHEMA_VERSION, got: schemaVersion },
|
|
3371
|
+
exitCode: 1,
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
const rawColumns = record.columns;
|
|
3375
|
+
if (!Array.isArray(rawColumns)) {
|
|
3376
|
+
throw new OxygenError("invalid_bundle", "Bundle is missing a columns array.", { exitCode: 1 });
|
|
3377
|
+
}
|
|
3378
|
+
const rawRows = record.rows;
|
|
3379
|
+
if (!Array.isArray(rawRows)) {
|
|
3380
|
+
throw new OxygenError("invalid_bundle", "Bundle is missing a rows array.", { exitCode: 1 });
|
|
3381
|
+
}
|
|
3382
|
+
const tableSummary = record.table && typeof record.table === "object" && !Array.isArray(record.table)
|
|
3383
|
+
? record.table
|
|
3384
|
+
: null;
|
|
3385
|
+
return {
|
|
3386
|
+
schemaVersion,
|
|
3387
|
+
tableName: tableSummary
|
|
3388
|
+
? (readRecordString(tableSummary, "name") ?? readRecordString(tableSummary, "displayName"))
|
|
3389
|
+
: null,
|
|
3390
|
+
tableSummary,
|
|
3391
|
+
columns: rawColumns.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
|
|
3392
|
+
rows: rawRows.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function stripRowForImport(row, validColumnKeys) {
|
|
3396
|
+
const out = {};
|
|
3397
|
+
for (const [key, value] of Object.entries(row)) {
|
|
3398
|
+
if (key === "_row_id" || key === "_created_at" || key === "_updated_at")
|
|
3399
|
+
continue;
|
|
3400
|
+
if (validColumnKeys.size > 0 && !validColumnKeys.has(key))
|
|
3401
|
+
continue;
|
|
3402
|
+
out[key] = value;
|
|
3403
|
+
}
|
|
3404
|
+
return out;
|
|
3405
|
+
}
|
|
3406
|
+
function readBundleNumber(value) {
|
|
3407
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
3408
|
+
return value;
|
|
3409
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value)))
|
|
3410
|
+
return Number(value);
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3082
3413
|
async function readRowsFile(path, format, sheet) {
|
|
3083
3414
|
return await parseRowsFileBuffer(readFileSync(path), format, sheet ? { sheet } : {});
|
|
3084
3415
|
}
|
|
@@ -3117,11 +3448,12 @@ function formatRows(rows, format, columns) {
|
|
|
3117
3448
|
...rows.flatMap((row) => Object.keys(row)),
|
|
3118
3449
|
].filter((key, index, all) => all.indexOf(key) === index && rows.some((row) => key in row));
|
|
3119
3450
|
if (format === "csv") {
|
|
3451
|
+
const csvBody = [
|
|
3452
|
+
keys.map(escapeCsvField).join(","),
|
|
3453
|
+
...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
|
|
3454
|
+
].join("\n");
|
|
3120
3455
|
return {
|
|
3121
|
-
content:
|
|
3122
|
-
keys.map(escapeCsvField).join(","),
|
|
3123
|
-
...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
|
|
3124
|
-
].join("\n") + "\n",
|
|
3456
|
+
content: `${csvBody}\n`,
|
|
3125
3457
|
rescuedCount: 0,
|
|
3126
3458
|
};
|
|
3127
3459
|
}
|
|
@@ -3152,21 +3484,27 @@ function formatRows(rows, format, columns) {
|
|
|
3152
3484
|
}
|
|
3153
3485
|
return Math.min(max, 60);
|
|
3154
3486
|
});
|
|
3155
|
-
const renderRow = (cells) =>
|
|
3156
|
-
|
|
3487
|
+
const renderRow = (cells) => {
|
|
3488
|
+
const padded = cells.map((cell, i) => {
|
|
3489
|
+
const w = widths[i] ?? 0;
|
|
3490
|
+
return clipCell(cell, w).padEnd(w);
|
|
3491
|
+
});
|
|
3492
|
+
return `| ${padded.join(" | ")} |`;
|
|
3493
|
+
};
|
|
3494
|
+
const separator = `|${widths.map((w) => "-".repeat(w + 2)).join("|")}|`;
|
|
3157
3495
|
const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
|
|
3158
3496
|
if (rescuedCount > 0) {
|
|
3159
3497
|
lines.push("");
|
|
3160
3498
|
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
3499
|
}
|
|
3162
|
-
return { content: lines.join("\n")
|
|
3500
|
+
return { content: `${lines.join("\n")}\n`, rescuedCount };
|
|
3163
3501
|
}
|
|
3164
3502
|
function clipCell(value, width) {
|
|
3165
3503
|
if (value.length <= width)
|
|
3166
3504
|
return value;
|
|
3167
3505
|
if (width <= 1)
|
|
3168
3506
|
return value.slice(0, width);
|
|
3169
|
-
return value.slice(0, width - 1)
|
|
3507
|
+
return `${value.slice(0, width - 1)}…`;
|
|
3170
3508
|
}
|
|
3171
3509
|
function escapeCsvField(value) {
|
|
3172
3510
|
const text = value === null || value === undefined
|
|
@@ -3252,6 +3590,54 @@ function isTerminalTableActionRunStatus(status) {
|
|
|
3252
3590
|
function sleep(ms) {
|
|
3253
3591
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3254
3592
|
}
|
|
3593
|
+
async function resolveActiveProfileWithSource() {
|
|
3594
|
+
const resolution = await resolveActiveProfile();
|
|
3595
|
+
let source;
|
|
3596
|
+
if (globalProfileFlag)
|
|
3597
|
+
source = "flag";
|
|
3598
|
+
else if (INITIAL_OXYGEN_PROFILE_ENV)
|
|
3599
|
+
source = "env";
|
|
3600
|
+
else
|
|
3601
|
+
source = resolution.source === "env" ? "env" : resolution.source;
|
|
3602
|
+
return { resolution, source };
|
|
3603
|
+
}
|
|
3604
|
+
async function handleWhoamiAction(options) {
|
|
3605
|
+
try {
|
|
3606
|
+
const identity = await requestOxygen("/api/cli/whoami");
|
|
3607
|
+
const context = await resolveActiveProfileWithSource();
|
|
3608
|
+
if (context.resolution.exists) {
|
|
3609
|
+
const cached = context.resolution.credentials?.identity;
|
|
3610
|
+
const orgChanged = !cached || cached.organization.id !== identity.organization.id;
|
|
3611
|
+
const userChanged = !cached || cached.user.id !== identity.user.id;
|
|
3612
|
+
if (orgChanged || userChanged) {
|
|
3613
|
+
const refreshed = {
|
|
3614
|
+
token: context.resolution.credentials.token,
|
|
3615
|
+
apiUrl: context.resolution.credentials.apiUrl,
|
|
3616
|
+
identity: identityFromWhoami(identity),
|
|
3617
|
+
};
|
|
3618
|
+
await saveCredentials(refreshed, process.env, {
|
|
3619
|
+
profile: context.resolution.name,
|
|
3620
|
+
activate: false,
|
|
3621
|
+
}).catch(() => undefined);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
const data = {
|
|
3625
|
+
...identity,
|
|
3626
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
3627
|
+
profile_source: context.source,
|
|
3628
|
+
};
|
|
3629
|
+
if (options.json) {
|
|
3630
|
+
writeJson(success("whoami", data));
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
process.stdout.write(formatWhoami(identity, context));
|
|
3634
|
+
}
|
|
3635
|
+
catch (error) {
|
|
3636
|
+
const failure = toFailure("whoami", error);
|
|
3637
|
+
writeJson(failure);
|
|
3638
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3255
3641
|
async function handleLoginAction(options) {
|
|
3256
3642
|
try {
|
|
3257
3643
|
const data = await login(options);
|
|
@@ -3275,10 +3661,14 @@ async function handleAuthUseTokenAction(options) {
|
|
|
3275
3661
|
api_url: data.credentials.apiUrl,
|
|
3276
3662
|
user: data.identity.user,
|
|
3277
3663
|
organization: data.identity.organization,
|
|
3664
|
+
renamed: data.renamed,
|
|
3278
3665
|
}));
|
|
3279
3666
|
return;
|
|
3280
3667
|
}
|
|
3281
|
-
process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile));
|
|
3668
|
+
process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile, { renamed: data.renamed }));
|
|
3669
|
+
const hint = await buildPostLoginHint(data.profile);
|
|
3670
|
+
if (hint)
|
|
3671
|
+
process.stdout.write(hint);
|
|
3282
3672
|
}
|
|
3283
3673
|
catch (error) {
|
|
3284
3674
|
const failure = toFailure("auth use-token", error);
|
|
@@ -3311,6 +3701,8 @@ async function handleProfilesListAction(options) {
|
|
|
3311
3701
|
async function handleProfilesUseAction(profile, options) {
|
|
3312
3702
|
try {
|
|
3313
3703
|
const activeProfile = await switchCredentialProfile(profile);
|
|
3704
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
3705
|
+
const totalProfiles = state?.profiles.length ?? 1;
|
|
3314
3706
|
const data = {
|
|
3315
3707
|
active_profile: activeProfile.name,
|
|
3316
3708
|
profile: summarizeCredentialProfile(activeProfile, true),
|
|
@@ -3319,7 +3711,7 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
3319
3711
|
writeJson(success("profiles use", data));
|
|
3320
3712
|
return;
|
|
3321
3713
|
}
|
|
3322
|
-
process.stdout.write(formatProfileUseSuccess(data.profile));
|
|
3714
|
+
process.stdout.write(formatProfileUseSuccess(data.profile, { totalProfiles }));
|
|
3323
3715
|
}
|
|
3324
3716
|
catch (error) {
|
|
3325
3717
|
const failure = toFailure("profiles use", error);
|
|
@@ -3327,6 +3719,74 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
3327
3719
|
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3328
3720
|
}
|
|
3329
3721
|
}
|
|
3722
|
+
async function handleProfilesEnvAction(profile, options) {
|
|
3723
|
+
try {
|
|
3724
|
+
if (options.unset) {
|
|
3725
|
+
const data = {
|
|
3726
|
+
profile,
|
|
3727
|
+
unset: ["OXYGEN_PROFILE", "OXYGEN_API_URL"],
|
|
3728
|
+
};
|
|
3729
|
+
if (options.json) {
|
|
3730
|
+
writeJson(success("profiles env", data));
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
process.stdout.write("unset OXYGEN_PROFILE OXYGEN_API_URL\n");
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
const state = await listCredentialProfiles();
|
|
3737
|
+
const match = state.profiles.find((entry) => entry.name === profile);
|
|
3738
|
+
if (!match) {
|
|
3739
|
+
throw new OxygenError("profile_not_found", `Oxygen CLI profile "${profile}" is not stored.`, {
|
|
3740
|
+
details: { profile },
|
|
3741
|
+
exitCode: 1,
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
const env = {
|
|
3745
|
+
OXYGEN_PROFILE: match.name,
|
|
3746
|
+
OXYGEN_API_URL: match.apiUrl,
|
|
3747
|
+
};
|
|
3748
|
+
if (options.json) {
|
|
3749
|
+
writeJson(success("profiles env", { profile: match.name, env }));
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
process.stdout.write(`export OXYGEN_PROFILE=${shellQuote(match.name)}\n`);
|
|
3753
|
+
process.stdout.write(`export OXYGEN_API_URL=${shellQuote(match.apiUrl)}\n`);
|
|
3754
|
+
}
|
|
3755
|
+
catch (error) {
|
|
3756
|
+
const failure = toFailure("profiles env", error);
|
|
3757
|
+
writeJson(failure);
|
|
3758
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async function handleProfilesCurrentAction(options) {
|
|
3762
|
+
try {
|
|
3763
|
+
const context = await resolveActiveProfileWithSource();
|
|
3764
|
+
const credentials = context.resolution.credentials;
|
|
3765
|
+
const data = {
|
|
3766
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
3767
|
+
profile_source: context.source,
|
|
3768
|
+
api_url: credentials?.apiUrl ?? null,
|
|
3769
|
+
organization: credentials?.identity?.organization ?? null,
|
|
3770
|
+
user: credentials?.identity?.user ?? null,
|
|
3771
|
+
stored: context.resolution.exists,
|
|
3772
|
+
};
|
|
3773
|
+
if (options.json) {
|
|
3774
|
+
writeJson(success("profiles current", data));
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
process.stdout.write(formatProfilesCurrent(context));
|
|
3778
|
+
}
|
|
3779
|
+
catch (error) {
|
|
3780
|
+
const failure = toFailure("profiles current", error);
|
|
3781
|
+
writeJson(failure);
|
|
3782
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
function shellQuote(value) {
|
|
3786
|
+
if (/^[A-Za-z0-9_.\-:\/@%+=]+$/.test(value))
|
|
3787
|
+
return value;
|
|
3788
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3789
|
+
}
|
|
3330
3790
|
async function handleLogoutAction(options) {
|
|
3331
3791
|
try {
|
|
3332
3792
|
const clearOptions = {};
|
|
@@ -3413,17 +3873,28 @@ async function login(options) {
|
|
|
3413
3873
|
browser: options.browser !== false,
|
|
3414
3874
|
json: Boolean(options.json),
|
|
3415
3875
|
});
|
|
3416
|
-
const credentials = {
|
|
3417
|
-
token,
|
|
3418
|
-
apiUrl,
|
|
3419
|
-
};
|
|
3876
|
+
const credentials = { token, apiUrl };
|
|
3420
3877
|
const identity = await requestOxygen("/api/cli/whoami", { credentials });
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3878
|
+
credentials.identity = identityFromWhoami(identity);
|
|
3879
|
+
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
3880
|
+
let chosenProfile;
|
|
3881
|
+
let renamed = false;
|
|
3882
|
+
if (explicitProfile) {
|
|
3883
|
+
chosenProfile = explicitProfile;
|
|
3884
|
+
}
|
|
3885
|
+
else {
|
|
3886
|
+
const seed = identity.organization.slug?.trim() || identity.organization.id;
|
|
3887
|
+
const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
|
|
3888
|
+
chosenProfile = picked.name;
|
|
3889
|
+
renamed = picked.renamed;
|
|
3890
|
+
}
|
|
3891
|
+
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
3892
|
+
const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
|
|
3425
3893
|
if (!options.json) {
|
|
3426
|
-
process.stdout.write(formatLoginSuccess(identity, credentials, profile));
|
|
3894
|
+
process.stdout.write(formatLoginSuccess(identity, credentials, profile, { renamed, skillsInstall }));
|
|
3895
|
+
const hint = await buildPostLoginHint(profile);
|
|
3896
|
+
if (hint)
|
|
3897
|
+
process.stdout.write(hint);
|
|
3427
3898
|
}
|
|
3428
3899
|
return {
|
|
3429
3900
|
logged_in: true,
|
|
@@ -3431,6 +3902,7 @@ async function login(options) {
|
|
|
3431
3902
|
api_url: credentials.apiUrl,
|
|
3432
3903
|
user: identity.user,
|
|
3433
3904
|
organization: identity.organization,
|
|
3905
|
+
skills_install: skillsInstall,
|
|
3434
3906
|
};
|
|
3435
3907
|
}
|
|
3436
3908
|
async function useAuthToken(options) {
|
|
@@ -3445,17 +3917,59 @@ async function useAuthToken(options) {
|
|
|
3445
3917
|
apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
|
|
3446
3918
|
};
|
|
3447
3919
|
const identity = await requestOxygen("/api/cli/whoami", { credentials });
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3920
|
+
credentials.identity = identityFromWhoami(identity);
|
|
3921
|
+
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
3922
|
+
let chosenProfile;
|
|
3923
|
+
let renamed = false;
|
|
3924
|
+
if (explicitProfile) {
|
|
3925
|
+
chosenProfile = explicitProfile;
|
|
3926
|
+
}
|
|
3927
|
+
else {
|
|
3928
|
+
const seed = identity.organization.slug?.trim() || identity.organization.id;
|
|
3929
|
+
const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
|
|
3930
|
+
chosenProfile = picked.name;
|
|
3931
|
+
renamed = picked.renamed;
|
|
3932
|
+
}
|
|
3933
|
+
const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
|
|
3452
3934
|
return {
|
|
3453
3935
|
identity,
|
|
3454
3936
|
credentials,
|
|
3455
3937
|
api_url: credentials.apiUrl,
|
|
3456
3938
|
profile,
|
|
3939
|
+
renamed,
|
|
3457
3940
|
};
|
|
3458
3941
|
}
|
|
3942
|
+
function readEnvProfileName() {
|
|
3943
|
+
const value = process.env.OXYGEN_PROFILE?.trim();
|
|
3944
|
+
return value ? value : null;
|
|
3945
|
+
}
|
|
3946
|
+
function identityFromWhoami(response) {
|
|
3947
|
+
return {
|
|
3948
|
+
organization: {
|
|
3949
|
+
id: response.organization.id,
|
|
3950
|
+
slug: response.organization.slug ?? null,
|
|
3951
|
+
name: response.organization.name,
|
|
3952
|
+
},
|
|
3953
|
+
user: {
|
|
3954
|
+
id: response.user.id,
|
|
3955
|
+
email: response.user.email ?? null,
|
|
3956
|
+
},
|
|
3957
|
+
capturedAt: new Date().toISOString(),
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
async function buildPostLoginHint(activeProfile) {
|
|
3961
|
+
if (output.isTTY !== true || process.env.NO_COLOR) {
|
|
3962
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
3963
|
+
if (!state || state.profiles.length < 2)
|
|
3964
|
+
return "";
|
|
3965
|
+
return `\n Pin this terminal: eval "$(oxygen profiles env ${activeProfile})"\n\n`;
|
|
3966
|
+
}
|
|
3967
|
+
const state = await listCredentialProfiles().catch(() => null);
|
|
3968
|
+
if (!state || state.profiles.length < 2)
|
|
3969
|
+
return "";
|
|
3970
|
+
const c = ansi(true);
|
|
3971
|
+
return `\n ${c.dim("Pin this terminal:")} ${c.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
|
|
3972
|
+
}
|
|
3459
3973
|
async function promptForToken(options) {
|
|
3460
3974
|
const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
|
|
3461
3975
|
if (options.json) {
|
|
@@ -3593,9 +4107,11 @@ function startOxygenSpinner(message) {
|
|
|
3593
4107
|
},
|
|
3594
4108
|
};
|
|
3595
4109
|
}
|
|
3596
|
-
function formatLoginSuccess(identity, credentials, profile) {
|
|
4110
|
+
function formatLoginSuccess(identity, credentials, profile, options = {}) {
|
|
3597
4111
|
const email = identity.user.email ?? identity.user.id;
|
|
3598
|
-
const
|
|
4112
|
+
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
4113
|
+
const orgSlug = identity.organization.slug ?? null;
|
|
4114
|
+
const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
|
|
3599
4115
|
const key = identity.apiKey
|
|
3600
4116
|
? `${identity.apiKey.tokenPrefix}...${identity.apiKey.tokenSuffix}`
|
|
3601
4117
|
: "stored";
|
|
@@ -3603,11 +4119,14 @@ function formatLoginSuccess(identity, credentials, profile) {
|
|
|
3603
4119
|
.update(`oxygen-cli:${credentials.token}`)
|
|
3604
4120
|
.digest("hex");
|
|
3605
4121
|
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4122
|
+
const profileLabel = options.renamed
|
|
4123
|
+
? `${profile} ${c.dim("(auto-renamed to avoid collision)")}`
|
|
4124
|
+
: profile;
|
|
3606
4125
|
// skipcq: JS-0820 — not a React component; rule misfire on array of tuples
|
|
3607
4126
|
const rows = [
|
|
3608
4127
|
["Account", email],
|
|
3609
4128
|
["Organization", org],
|
|
3610
|
-
["Profile",
|
|
4129
|
+
["Profile", profileLabel],
|
|
3611
4130
|
["API", credentials.apiUrl],
|
|
3612
4131
|
["Token", key],
|
|
3613
4132
|
["Fingerprint", formatFingerprint(fingerprint)],
|
|
@@ -3626,6 +4145,7 @@ function formatLoginSuccess(identity, credentials, profile) {
|
|
|
3626
4145
|
"",
|
|
3627
4146
|
...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
|
|
3628
4147
|
"",
|
|
4148
|
+
...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
|
|
3629
4149
|
].join("\n");
|
|
3630
4150
|
}
|
|
3631
4151
|
function formatFingerprint(hex) {
|
|
@@ -3637,6 +4157,9 @@ function summarizeCredentialProfile(profile, active) {
|
|
|
3637
4157
|
active,
|
|
3638
4158
|
api_url: profile.apiUrl,
|
|
3639
4159
|
token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
|
|
4160
|
+
organization: profile.identity?.organization ?? null,
|
|
4161
|
+
user: profile.identity?.user ?? null,
|
|
4162
|
+
identity_captured_at: profile.identity?.capturedAt ?? null,
|
|
3640
4163
|
};
|
|
3641
4164
|
}
|
|
3642
4165
|
function createCredentialFingerprint(token) {
|
|
@@ -3653,34 +4176,116 @@ function formatProfilesList(data) {
|
|
|
3653
4176
|
"",
|
|
3654
4177
|
].join("\n");
|
|
3655
4178
|
}
|
|
3656
|
-
const
|
|
3657
|
-
|
|
4179
|
+
const nameWidth = Math.max(...data.profiles.map((profile) => profile.name.length));
|
|
4180
|
+
const orgWidth = Math.max(...data.profiles.map((profile) => formatProfileOrgCell(profile).length));
|
|
4181
|
+
const lines = [
|
|
3658
4182
|
"",
|
|
3659
4183
|
`${c.bold("Oxygen CLI Profiles")}`,
|
|
3660
4184
|
"",
|
|
3661
4185
|
...data.profiles.map((profile) => {
|
|
3662
4186
|
const marker = profile.active ? "*" : " ";
|
|
4187
|
+
const orgCell = formatProfileOrgCell(profile);
|
|
3663
4188
|
return [
|
|
3664
|
-
`${marker} ${profile.name.padEnd(
|
|
4189
|
+
`${marker} ${profile.name.padEnd(nameWidth)}`,
|
|
4190
|
+
orgCell.padEnd(orgWidth),
|
|
3665
4191
|
c.dim(profile.api_url),
|
|
3666
4192
|
c.dim(profile.token_fingerprint),
|
|
3667
4193
|
].join(" ");
|
|
3668
4194
|
}),
|
|
3669
4195
|
"",
|
|
3670
|
-
]
|
|
4196
|
+
];
|
|
4197
|
+
if (data.profiles.length >= 2) {
|
|
4198
|
+
lines.push(` ${c.dim("Pin a terminal:")} ${c.bold(`eval "$(oxygen profiles env <profile>)"`)}`);
|
|
4199
|
+
lines.push("");
|
|
4200
|
+
}
|
|
4201
|
+
return lines.join("\n");
|
|
4202
|
+
}
|
|
4203
|
+
function formatProfileOrgCell(profile) {
|
|
4204
|
+
if (!profile.organization)
|
|
4205
|
+
return "(unknown org — run `oxygen whoami` to refresh)";
|
|
4206
|
+
const { slug, name } = profile.organization;
|
|
4207
|
+
if (slug && slug !== name)
|
|
4208
|
+
return `${name} (${slug})`;
|
|
4209
|
+
return name || slug || profile.organization.id;
|
|
3671
4210
|
}
|
|
3672
|
-
function formatProfileUseSuccess(profile) {
|
|
4211
|
+
function formatProfileUseSuccess(profile, options) {
|
|
3673
4212
|
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
3674
|
-
|
|
4213
|
+
const lines = [
|
|
3675
4214
|
"",
|
|
3676
4215
|
`${c.green("[OK]")} ${c.bold("CLI profile selected")}`,
|
|
3677
4216
|
"",
|
|
3678
|
-
` ${c.dim("Profile")}
|
|
3679
|
-
` ${c.dim("
|
|
3680
|
-
` ${c.dim("
|
|
4217
|
+
` ${c.dim("Profile")} ${profile.name}`,
|
|
4218
|
+
` ${c.dim("Organization")} ${formatProfileOrgCell(profile)}`,
|
|
4219
|
+
` ${c.dim("API")} ${profile.api_url}`,
|
|
4220
|
+
` ${c.dim("Fingerprint")} ${profile.token_fingerprint}`,
|
|
4221
|
+
"",
|
|
4222
|
+
];
|
|
4223
|
+
if (options.totalProfiles >= 2) {
|
|
4224
|
+
lines.push(` ${c.dim("Note: this changes the active profile for every shell.")}`);
|
|
4225
|
+
lines.push(` ${c.dim("To pin this terminal only, run:")} ${c.bold(`eval "$(oxygen profiles env ${profile.name})"`)}`);
|
|
4226
|
+
lines.push("");
|
|
4227
|
+
}
|
|
4228
|
+
return lines.join("\n");
|
|
4229
|
+
}
|
|
4230
|
+
function formatWhoami(identity, context) {
|
|
4231
|
+
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4232
|
+
const email = identity.user.email ?? identity.user.id;
|
|
4233
|
+
const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
|
|
4234
|
+
const orgSlug = identity.organization.slug ?? null;
|
|
4235
|
+
const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
|
|
4236
|
+
const sourceLabel = describeProfileSource(context.source);
|
|
4237
|
+
const profileName = context.resolution.exists ? context.resolution.name : "(no stored profile)";
|
|
4238
|
+
const apiUrl = context.resolution.credentials?.apiUrl ?? defaultApiUrl();
|
|
4239
|
+
const rows = [
|
|
4240
|
+
["Account", email],
|
|
4241
|
+
["Organization", org],
|
|
4242
|
+
["Profile", `${profileName} ${c.dim(`(${sourceLabel})`)}`],
|
|
4243
|
+
["API", apiUrl],
|
|
4244
|
+
];
|
|
4245
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
4246
|
+
return [
|
|
4247
|
+
"",
|
|
4248
|
+
...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
|
|
3681
4249
|
"",
|
|
3682
4250
|
].join("\n");
|
|
3683
4251
|
} // skipcq: JS-C1002
|
|
4252
|
+
function formatProfilesCurrent(context) {
|
|
4253
|
+
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4254
|
+
const sourceLabel = describeProfileSource(context.source);
|
|
4255
|
+
if (!context.resolution.exists) {
|
|
4256
|
+
return [
|
|
4257
|
+
"",
|
|
4258
|
+
`${c.dim("No stored profile resolves for this shell.")}`,
|
|
4259
|
+
` ${c.dim("Source:")} ${sourceLabel}`,
|
|
4260
|
+
` ${c.dim("Run")} ${c.bold("oxygen login")} ${c.dim("to create one.")}`,
|
|
4261
|
+
"",
|
|
4262
|
+
].join("\n");
|
|
4263
|
+
}
|
|
4264
|
+
const credentials = context.resolution.credentials;
|
|
4265
|
+
const orgCell = credentials.identity
|
|
4266
|
+
? (credentials.identity.organization.slug && credentials.identity.organization.slug !== credentials.identity.organization.name
|
|
4267
|
+
? `${credentials.identity.organization.name} (${credentials.identity.organization.slug})`
|
|
4268
|
+
: credentials.identity.organization.name)
|
|
4269
|
+
: "(unknown — run `oxygen whoami` to refresh)";
|
|
4270
|
+
return [
|
|
4271
|
+
"",
|
|
4272
|
+
`${c.bold("Active Oxygen CLI Profile")}`,
|
|
4273
|
+
"",
|
|
4274
|
+
` ${c.dim("Profile")} ${context.resolution.name}`,
|
|
4275
|
+
` ${c.dim("Source")} ${sourceLabel}`,
|
|
4276
|
+
` ${c.dim("Organization")} ${orgCell}`,
|
|
4277
|
+
` ${c.dim("API")} ${credentials.apiUrl}`,
|
|
4278
|
+
"",
|
|
4279
|
+
].join("\n");
|
|
4280
|
+
}
|
|
4281
|
+
function describeProfileSource(source) {
|
|
4282
|
+
switch (source) {
|
|
4283
|
+
case "flag": return "from --profile flag";
|
|
4284
|
+
case "env": return "from OXYGEN_PROFILE";
|
|
4285
|
+
case "file": return "from stored active profile";
|
|
4286
|
+
case "default": return "default fallback";
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
3684
4289
|
function formatLogoutSuccess(result) {
|
|
3685
4290
|
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
3686
4291
|
const removed = result.removedProfile
|
|
@@ -3705,8 +4310,34 @@ function formatUpdateSuccess(result) {
|
|
|
3705
4310
|
` ${c.dim("Package")} ${result.package}`,
|
|
3706
4311
|
` ${c.dim("Command")} ${result.command}`,
|
|
3707
4312
|
"",
|
|
4313
|
+
...formatAutomaticSkillsInstallStatusLines(result.skills_install),
|
|
3708
4314
|
].join("\n");
|
|
3709
4315
|
} // skipcq: JS-C1002
|
|
4316
|
+
function formatAutomaticSkillsInstallStatusLines(result) {
|
|
4317
|
+
if (!result)
|
|
4318
|
+
return [];
|
|
4319
|
+
const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
4320
|
+
if (!result.attempted) {
|
|
4321
|
+
if (result.reason === "dry_run")
|
|
4322
|
+
return [];
|
|
4323
|
+
return [
|
|
4324
|
+
`${c.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
|
|
4325
|
+
"",
|
|
4326
|
+
];
|
|
4327
|
+
}
|
|
4328
|
+
if (result.ok) {
|
|
4329
|
+
return [
|
|
4330
|
+
`${c.green("[OK]")} ${c.bold("Oxygen skills installed")}`,
|
|
4331
|
+
"",
|
|
4332
|
+
];
|
|
4333
|
+
}
|
|
4334
|
+
return [
|
|
4335
|
+
`${c.yellow("[WARN]")} Oxygen skills could not be installed; the CLI command still completed.`,
|
|
4336
|
+
` ${c.dim("Check")} ${"oxygen skills doctor --json"}`,
|
|
4337
|
+
` ${c.dim("Retry")} ${result.command}`,
|
|
4338
|
+
"",
|
|
4339
|
+
];
|
|
4340
|
+
}
|
|
3710
4341
|
function renderBox(lines) {
|
|
3711
4342
|
const width = Math.max(...lines.map(visibleLength), 0);
|
|
3712
4343
|
const border = `+${"-".repeat(width + 2)}+`;
|
|
@@ -3725,6 +4356,7 @@ function ansi(enabled) {
|
|
|
3725
4356
|
bold: wrap(1, 22), // skipcq: JS-0117 // skipcq: JS-W1035
|
|
3726
4357
|
dim: wrap(2, 22),
|
|
3727
4358
|
green: wrap(32, 39),
|
|
4359
|
+
yellow: wrap(33, 39),
|
|
3728
4360
|
};
|
|
3729
4361
|
}
|
|
3730
4362
|
function readOption(value) {
|
|
@@ -3752,6 +4384,20 @@ function contextAssetsQuery(options) {
|
|
|
3752
4384
|
const value = query.toString();
|
|
3753
4385
|
return value ? `?${value}` : "";
|
|
3754
4386
|
}
|
|
4387
|
+
function buildContextResolveBody(options) {
|
|
4388
|
+
const assetTypes = readCsvOption(options.assetType);
|
|
4389
|
+
const tags = readCsvOption(options.tags);
|
|
4390
|
+
const maxAssets = readNonNegativeInt(options.maxAssets);
|
|
4391
|
+
return {
|
|
4392
|
+
...(readOption(options.purpose) ? { purpose: readOption(options.purpose) } : {}),
|
|
4393
|
+
...(assetTypes.length > 0 ? { asset_types: assetTypes } : {}),
|
|
4394
|
+
...(readOption(options.assetStatus) ? { asset_status: readOption(options.assetStatus) } : {}),
|
|
4395
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
4396
|
+
...(options.includeArchived ? { include_archived: true } : {}),
|
|
4397
|
+
...(maxAssets !== undefined ? { max_assets: maxAssets } : {}),
|
|
4398
|
+
...(options.requireReady ? { require_ready: true } : {}),
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
3755
4401
|
function buildContextAssetUpsertBody(options) {
|
|
3756
4402
|
const asset = options.assetJson ? parseJsonObject(options.assetJson) : {};
|
|
3757
4403
|
const tags = readCsvOption(options.tags);
|