@oxygen-agent/cli 1.1014.985 → 1.1014.1087
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-families/program.d.ts +2 -0
- package/dist/command-families/program.js +241 -0
- package/dist/command-families/tables/automation.d.ts +22 -0
- package/dist/command-families/tables/automation.js +250 -0
- package/dist/command-families/tables/bundle-export.d.ts +7 -0
- package/dist/command-families/tables/bundle-export.js +198 -0
- package/dist/command-families/tables/bundle-format.d.ts +66 -0
- package/dist/command-families/tables/bundle-format.js +274 -0
- package/dist/command-families/tables/bundle-import.d.ts +20 -0
- package/dist/command-families/tables/bundle-import.js +260 -0
- package/dist/command-families/tables/display.d.ts +107 -0
- package/dist/command-families/tables/display.js +229 -0
- package/dist/command-families/tables/import-batches.d.ts +26 -0
- package/dist/command-families/tables/import-batches.js +94 -0
- package/dist/command-families/tables/import-columns.d.ts +24 -0
- package/dist/command-families/tables/import-columns.js +131 -0
- package/dist/command-families/tables/import-upload.d.ts +30 -0
- package/dist/command-families/tables/import-upload.js +203 -0
- package/dist/command-families/tables/import-values.d.ts +10 -0
- package/dist/command-families/tables/import-values.js +60 -0
- package/dist/command-families/tables/imports.d.ts +19 -0
- package/dist/command-families/tables/imports.js +200 -0
- package/dist/command-families/tables/options.d.ts +314 -0
- package/dist/command-families/tables/options.js +1 -0
- package/dist/command-families/tables/register-automation.d.ts +2 -0
- package/dist/command-families/tables/register-automation.js +127 -0
- package/dist/command-families/tables/register-external-imports.d.ts +2 -0
- package/dist/command-families/tables/register-external-imports.js +293 -0
- package/dist/command-families/tables/register-files.d.ts +2 -0
- package/dist/command-families/tables/register-files.js +72 -0
- package/dist/command-families/tables/register-followers.d.ts +2 -0
- package/dist/command-families/tables/register-followers.js +73 -0
- package/dist/command-families/tables/register-lifecycle.d.ts +2 -0
- package/dist/command-families/tables/register-lifecycle.js +114 -0
- package/dist/command-families/tables/register-reads.d.ts +2 -0
- package/dist/command-families/tables/register-reads.js +187 -0
- package/dist/command-families/tables/register-relations.d.ts +2 -0
- package/dist/command-families/tables/register-relations.js +244 -0
- package/dist/command-families/tables/register-row-actions.d.ts +2 -0
- package/dist/command-families/tables/register-row-actions.js +53 -0
- package/dist/command-families/tables/register-row-writes.d.ts +2 -0
- package/dist/command-families/tables/register-row-writes.js +140 -0
- package/dist/command-families/tables/register-views.d.ts +2 -0
- package/dist/command-families/tables/register-views.js +83 -0
- package/dist/command-families/tables/register-watchers.d.ts +2 -0
- package/dist/command-families/tables/register-watchers.js +65 -0
- package/dist/command-families/tables/register-webhooks.d.ts +2 -0
- package/dist/command-families/tables/register-webhooks.js +126 -0
- package/dist/command-families/tables/registration-context.d.ts +8 -0
- package/dist/command-families/tables/registration-context.js +1 -0
- package/dist/command-families/tables/registration.d.ts +3 -0
- package/dist/command-families/tables/registration.js +30 -0
- package/dist/command-families/tables/row-selection.d.ts +5 -0
- package/dist/command-families/tables/row-selection.js +82 -0
- package/dist/command-families/tables/send.d.ts +6 -0
- package/dist/command-families/tables/send.js +111 -0
- package/dist/command-manifest.js +1 -0
- package/dist/commands/copilot.js +8 -0
- package/dist/commands/deliverability.js +4 -2
- package/dist/commands/sequences.js +30 -2
- package/dist/commands/tables.d.ts +1 -3
- package/dist/commands/tables.js +1 -3505
- package/dist/credentials.js +1 -1
- package/dist/customer-operation-commands.js +62 -3
- package/dist/help.js +1 -1
- package/dist/index.d.ts +1 -99
- package/dist/index.js +5 -337
- package/dist/knowledge-mirror.js +1 -1
- package/dist/skills.js +4 -2
- package/dist/update.js +2 -1
- package/dist/workspace-placement-commands.d.ts +3 -0
- package/dist/workspace-placement-commands.js +32 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +11 -3
- package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/agents-enabled.js +51 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +39 -2
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +17 -15
- package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/index.js +0 -6
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +6 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +13 -8
- package/node_modules/@oxygen/shared/dist/teammates.js +20 -11
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +78 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +149 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +101 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readPositiveInt, readJsonObjectOption, readNonNegativeInt } from "../../cli-values.js";
|
|
2
|
+
import { writeListCapNotice, readFilterJsonOption, readFormulaValuesOption, readCsvOption } from "../../command-support.js";
|
|
3
|
+
import { addContractFlags, readContractFlags } from "../../contract-flags.js";
|
|
4
|
+
import { requestOxygen } from "../../http-client.js";
|
|
5
|
+
import { writeExclusiveJson } from "../../json-output.js";
|
|
6
|
+
import { readOption } from "../../util.js";
|
|
7
|
+
import { OxygenError, success } from "@oxygen/shared/cli-result";
|
|
8
|
+
import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/contracts/table-list";
|
|
9
|
+
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
10
|
+
import { TABLE_QUERY_CLI_DEFAULT_LIMIT, TABLE_QUERY_CLI_DEFAULT_MAX_CHARS, tableQueryRawResultFits, tableQueryRawRecovery } from "@oxygen/shared/contracts/table-query-raw";
|
|
11
|
+
import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import { handleTablesTidySuggestAction } from "./display.js";
|
|
14
|
+
import { readSortJsonOption } from "./automation.js";
|
|
15
|
+
export function registerReads({ tablesCommand, handleAsyncAction }) {
|
|
16
|
+
tablesCommand.addCommand(addContractFlags(new Command("list").description("List workspace tables in the current tenant database."), TABLE_LIST_QUERY_CONTRACT, {
|
|
17
|
+
project: { help: "Project id or slug to filter by." },
|
|
18
|
+
tag: { help: "Only tables carrying this workspace tag (see `oxygen tags list`)." },
|
|
19
|
+
include_archived: {
|
|
20
|
+
help: "Also list archived, delete-scheduled and purging tables (the trash; each carries a lifecycle with purge_scheduled_at, and one with purgeStartedAt is being purged and can no longer be restored).",
|
|
21
|
+
},
|
|
22
|
+
limit: { help: "Maximum tables to list. Defaults to 200; hard cap is 1000.", valueName: "<n>" },
|
|
23
|
+
})
|
|
24
|
+
.option("--all", "List every table instead of the newest window.")
|
|
25
|
+
.option("--json", "Print a JSON envelope.")
|
|
26
|
+
.addHelpText("after", "\nLists the 200 newest tables by default. Follow nextCursor with --cursor and the same filters, or pass --all. --query searches names across every table; --projection summary keeps results compact. rowCount is null when not reconciled; measure unknown counts with `tables describe <table> --stats`.\n")
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
await handleAsyncAction("tables list", options, async () => {
|
|
29
|
+
const params = tableListQueryParams(readContractFlags(options, TABLE_LIST_QUERY_CONTRACT));
|
|
30
|
+
if (options.all)
|
|
31
|
+
params.set("all", "true");
|
|
32
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
33
|
+
const data = await requestOxygen(`/api/cli/tables${qs}`);
|
|
34
|
+
// A capped list is where a blind agent filtered 200 rows locally to find
|
|
35
|
+
// one table by name (acceptance run, 2026-09-18); name the native lookup.
|
|
36
|
+
if (!options.json)
|
|
37
|
+
writeListCapNotice(data, "table(s)", "continue with --cursor <nextCursor>, or find a name with --query <text>");
|
|
38
|
+
return data;
|
|
39
|
+
});
|
|
40
|
+
}));
|
|
41
|
+
tablesCommand.addCommand(addContractFlags(new Command("query"), TABLE_QUERY_GRID_CONTRACT, {
|
|
42
|
+
format: { valueName: "<format>", help: "Grid cell previews/state: default 50, cap 200, optional stats/fill/window. Omit for raw filters, fields, search, sorts or offsets. JSON cells are null; their digests live in __oxygen_grid_cell_previews. Use tables rows inspect --projection value for complete values." },
|
|
43
|
+
max_chars: { help: "Maximum compact JSON envelope characters for raw or grid reads (1,000-100,000). Raw defaults to 12,000." },
|
|
44
|
+
})
|
|
45
|
+
.description("Read raw Table rows or --format grid cell previews/state with grid.nextCursor, in a JSON envelope. Use tables preview --summary-only for fill rates; tables export <table> --format table renders raw rows. Grid supports stats/fill/window options and refuses raw selectors.")
|
|
46
|
+
.argument("<table>", "Table id or slug.")
|
|
47
|
+
.option("--limit <n>", "Raw: default 20, native cap 2000; --full restores default 100. Grid: default 50, cap 200.")
|
|
48
|
+
.option("--cursor <cursor>", "Pagination cursor returned by a previous query. Walks forward one page at a time; mutually exclusive with --offset.")
|
|
49
|
+
.option("--offset <n>", "Skip this many rows and start there — jump straight to a row without paging to it. Mutually exclusive with --cursor.")
|
|
50
|
+
.option("--fields <columns>", "Comma-separated column keys or ids to include.")
|
|
51
|
+
.option("--filter-json <json>", "Legacy filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'. Mutually exclusive with --filter-tree-json/--sort-json.")
|
|
52
|
+
.option("--filter-tree-json <json>", `Airtable-style filter group, e.g. '{\"type\":\"group\",\"conjunction\":\"and\",\"children\":[{\"type\":\"leaf\",\"columnKey\":\"stage\",\"operator\":\"is\",\"value\":\"won\"}]}'. Add \"path\":\"a.b\" to a leaf to match a key inside a JSON cell, e.g. the rows one Function run touched: {\"columnKey\":\"<function_column>\",\"path\":\"action_run_id\",\"operator\":\"is\",\"value\":\"<run_id>\"}. Leaf operators: ${FILTER_TREE_OPERATORS.join(", ")} (is_true/is_false need a checkbox column; --filter-json's is_null/is_not_null are is_empty/is_not_empty here). Mutually exclusive with --filter-json.`)
|
|
53
|
+
.option("--search <text>", "Find rows by one piece of text, matched against every text, number, date and checkbox column at once — the grid's magnifier. Combines with --filter-tree-json (both must hold). Use --filter-tree-json when you know which column to match.")
|
|
54
|
+
.option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note. A fill-empty field a bundle adds (definition fill: \"empty\", such as Headcount or Job title) is stored, not live: its cell is the value, so 'materialized' filters it exactly.")
|
|
55
|
+
.option("--sort-json <json>", "Ordered sort rules, e.g. '[{\"columnKey\":\"_created_at\",\"direction\":\"desc\"}]'. Earlier rules dominate. Mutually exclusive with --filter-json.")
|
|
56
|
+
.option("--no-system-fields", "Omit _row_id, _created_at, and _updated_at from returned rows (included by default).")
|
|
57
|
+
.option("--full", "Read legacy full rows without the default character limit.")
|
|
58
|
+
.option("--output <path>", "Write full JSON for this page to a new local file; print a small receipt.")
|
|
59
|
+
.option("--json", "Print a JSON envelope.")
|
|
60
|
+
.action(async (table, options) => {
|
|
61
|
+
await handleAsyncAction("tables query", options, async () => {
|
|
62
|
+
const gridArgs = readContractFlags(options, TABLE_QUERY_GRID_CONTRACT);
|
|
63
|
+
if (gridArgs.format !== "grid" && Object.keys(gridArgs).some((key) => key !== "max_chars")) {
|
|
64
|
+
throw new OxygenError("invalid_request", "Grid options require --format grid.", { exitCode: 1 });
|
|
65
|
+
}
|
|
66
|
+
const full = options.full || Boolean(options.output);
|
|
67
|
+
if (full && options.format === "grid")
|
|
68
|
+
throw new OxygenError("invalid_request", "--full/--output require raw rows; omit --format grid.", { exitCode: 2 });
|
|
69
|
+
const limit = readPositiveInt(options.limit) ?? (options.format !== "grid" && !full ? TABLE_QUERY_CLI_DEFAULT_LIMIT : undefined);
|
|
70
|
+
if (options.format !== "grid" && !full && gridArgs.max_chars === undefined)
|
|
71
|
+
gridArgs.max_chars = TABLE_QUERY_CLI_DEFAULT_MAX_CHARS;
|
|
72
|
+
const filters = readFilterJsonOption(options.filterJson);
|
|
73
|
+
const filterTree = readJsonObjectOption(options.filterTreeJson);
|
|
74
|
+
const formulaValues = readFormulaValuesOption(options.formulaValues);
|
|
75
|
+
const sorts = readSortJsonOption(options.sortJson);
|
|
76
|
+
const offset = readNonNegativeInt(options.offset);
|
|
77
|
+
if (filters && (filterTree || sorts)) {
|
|
78
|
+
throw new OxygenError("invalid_filter", "Pass either --filter-json (legacy) or --filter-tree-json/--sort-json, not both.", { exitCode: 1 });
|
|
79
|
+
}
|
|
80
|
+
if (formulaValues && !filters && !filterTree) {
|
|
81
|
+
throw new OxygenError("invalid_filter", "--formula-values requires --filter-json or --filter-tree-json.", { exitCode: 1 });
|
|
82
|
+
}
|
|
83
|
+
if (readOption(options.cursor) && offset !== undefined) {
|
|
84
|
+
throw new OxygenError("invalid_request", "Pass either --cursor or --offset, not both.", { exitCode: 1 });
|
|
85
|
+
}
|
|
86
|
+
if (options.format === "grid" && (readCsvOption(options.fields).length > 0 || filters?.length || filterTree ||
|
|
87
|
+
readOption(options.search) || formulaValues || sorts?.length || (offset !== undefined && offset > 0))) {
|
|
88
|
+
throw new OxygenError("invalid_request", '--format grid takes only table, --limit, --cursor and optional --include-stats/--include-fill; omit --format for filters, fields, search, sorts or offsets.', { exitCode: 1 });
|
|
89
|
+
}
|
|
90
|
+
const data = await requestOxygen("/api/cli/tables/query", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: {
|
|
93
|
+
table,
|
|
94
|
+
...gridArgs,
|
|
95
|
+
...(limit ? { limit } : {}),
|
|
96
|
+
...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
|
|
97
|
+
...(offset !== undefined ? { offset } : {}),
|
|
98
|
+
...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
|
|
99
|
+
...(filters ? { filters } : {}),
|
|
100
|
+
...(filterTree ? { filterTree } : {}),
|
|
101
|
+
...(readOption(options.search) ? { search: readOption(options.search) } : {}),
|
|
102
|
+
...(formulaValues ? { formula_values: formulaValues } : {}),
|
|
103
|
+
...(sorts ? { sorts } : {}),
|
|
104
|
+
...(options.systemFields === false ? { include_system_fields: false } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
// Enforce the window even against an older compatible serving API.
|
|
108
|
+
if (options.format !== "grid" && !tableQueryRawResultFits(data, typeof gridArgs.max_chars === "number" ? gridArgs.max_chars : undefined)) {
|
|
109
|
+
const firstRow = Array.isArray(data.rows) ? data.rows[0] : undefined;
|
|
110
|
+
throw new OxygenError("table_query_result_too_large", "Table rows exceed the requested result window.", {
|
|
111
|
+
details: { max_chars: gridArgs.max_chars, ...tableQueryRawRecovery(table, typeof firstRow?._row_id === "string" ? firstRow._row_id : undefined) }, exitCode: 2,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!options.output)
|
|
115
|
+
return data;
|
|
116
|
+
const output = await writeExclusiveJson(options.output, success("tables query", data), process.argv.includes("--pretty"));
|
|
117
|
+
return { output, returned_count: data.returned_count ?? data.returnedCount, total_count: data.total_count ?? data.totalCount, hasMore: data.hasMore, nextCursor: data.nextCursor, web_url: data.web_url };
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
tablesCommand.addCommand(new Command("preview")
|
|
121
|
+
.description("Preview a workspace table with projected rows and summary stats.")
|
|
122
|
+
.argument("<table>", "Table id or slug.")
|
|
123
|
+
.option("--limit <n>", "Maximum preview rows to return. Defaults to 25; hard cap is 1000.")
|
|
124
|
+
.option("--fields <columns>", "Comma-separated column keys or ids to include.")
|
|
125
|
+
.option("--include-system-fields", "Include _row_id, _created_at, and _updated_at in preview rows.")
|
|
126
|
+
.option("--include-cell-states", "Include per-cell run state in the preview response.")
|
|
127
|
+
.option("--summary-only", "Return per-column fill stats plus table metadata, without row JSON. Stats cover the whole table independent of --limit: exact up to 20,000 rows, then a 2,000-row sample labelled summary.statsSampled; rowCount stays exact. Higher fidelity than `tables describe --stats`, which samples at 100. fillRate counts non-null cells, nonEmptyRate also excludes blank strings. 0 credits.")
|
|
128
|
+
.option("--json", "Print a JSON envelope.")
|
|
129
|
+
.action(async (table, options) => {
|
|
130
|
+
await handleAsyncAction("tables preview", options, () => {
|
|
131
|
+
const limit = readPositiveInt(options.limit);
|
|
132
|
+
return requestOxygen("/api/cli/tables/preview", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: {
|
|
135
|
+
table,
|
|
136
|
+
...(limit ? { limit } : {}),
|
|
137
|
+
...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
|
|
138
|
+
...(options.includeSystemFields ? { include_system_fields: true } : {}),
|
|
139
|
+
...(options.includeCellStates ? { include_cell_states: true } : {}),
|
|
140
|
+
...(options.summaryOnly ? { summary_only: true } : {}),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}));
|
|
145
|
+
tablesCommand.addCommand(new Command("describe")
|
|
146
|
+
.alias("get")
|
|
147
|
+
.description("Describe a workspace table and its columns. Add --stats for row count and per-column fill rates (0 credits). Next: `tools search --for-table <table>` lists what these columns can be enriched with and what each costs per row (0 credits).")
|
|
148
|
+
.argument("<table>", "Table id or slug.")
|
|
149
|
+
.option("--include-archived", "Include archived columns, and read a table that is archived, scheduled for deletion or being purged.")
|
|
150
|
+
.option("--stats", "Include stats.rowCount (also summary.rowCount) and per-column fill rates. Row count is exact; above 100 rows fill rates use a 100-row sample (stats.statsSampled). For whole-table fill rates on a bigger list use `tables preview <table> --summary-only` \u2014 exact up to 20,000 rows, 2,000-row sample above. 0 credits.")
|
|
151
|
+
.option("--column <key>", "Describe just this one column (its kind, definition and whether auto-run includes it) instead of every column.")
|
|
152
|
+
.option("--json", "Print a JSON envelope.")
|
|
153
|
+
.action(async (table, options) => {
|
|
154
|
+
await handleAsyncAction("tables describe", options, () => requestOxygen("/api/cli/tables/describe", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body: {
|
|
157
|
+
table,
|
|
158
|
+
...(options.includeArchived ? { include_archived: true } : {}),
|
|
159
|
+
...(options.stats ? { include_stats: true } : {}),
|
|
160
|
+
...(readOption(options.column) ? { column: readOption(options.column) } : {}),
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
}));
|
|
164
|
+
tablesCommand.addCommand(new Command("tidy-suggest")
|
|
165
|
+
.description("Read-only tidy analysis for a workspace table: duplicate columns, formatting issues, hide candidates, and value-sanity findings.")
|
|
166
|
+
.argument("<table>", "Table id or slug.")
|
|
167
|
+
.option("--limit <n>", "Maximum rows to sample for the analysis. Defaults to and is capped at 5000.")
|
|
168
|
+
.option("--categories <csv>", "Comma-separated subset of duplicates,formatting,hide,valueSanity. Defaults to all four.")
|
|
169
|
+
.option("--json", "Print a JSON envelope.")
|
|
170
|
+
.action(async (table, options) => {
|
|
171
|
+
await handleTablesTidySuggestAction(table, options);
|
|
172
|
+
}));
|
|
173
|
+
tablesCommand.addCommand(new Command("activity")
|
|
174
|
+
.description("Show background runs active on a workspace table right now, with a worker-queue health rollup.")
|
|
175
|
+
.argument("<table>", "Table id or slug.")
|
|
176
|
+
.option("--limit <n>", "Maximum active runs to return (1-100). Defaults to 20.")
|
|
177
|
+
.option("--json", "Print a JSON envelope.")
|
|
178
|
+
.action(async (table, options) => {
|
|
179
|
+
await handleAsyncAction("tables activity", options, () => {
|
|
180
|
+
const params = new URLSearchParams({ table });
|
|
181
|
+
const limit = readPositiveInt(options.limit);
|
|
182
|
+
if (limit)
|
|
183
|
+
params.set("limit", String(limit));
|
|
184
|
+
return requestOxygen(`/api/cli/tables/activity?${params.toString()}`);
|
|
185
|
+
});
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { emitCliFailure, emitSuccess } from "../../cli-output.js";
|
|
2
|
+
import { readPositiveInt } from "../../cli-values.js";
|
|
3
|
+
import { requestOxygen } from "../../http-client.js";
|
|
4
|
+
import { readOption } from "../../util.js";
|
|
5
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { writeTableLinkPreview } from "./display.js";
|
|
8
|
+
import { normalizePromotePolicy, parsePromoteMapOption } from "./automation.js";
|
|
9
|
+
export function registerRelations({ tablesCommand, handleAsyncAction }) {
|
|
10
|
+
tablesCommand.addCommand(new Command("relate")
|
|
11
|
+
.description("Link two tables (the web's \"Link to another table\"): creates an empty linked-record column on BOTH tables, so links show and can be removed from either side; matching values are not required. `oxygen tables relate contacts --to accounts --live` names them from the tables (account on contacts, contacts on accounts) and re-running it finds the same link. Then use `oxygen tables link` to populate row-to-row edges. Works on any workspace table; plain tables stay plain and are never registered as CRM objects. Defaults to dry-run.")
|
|
12
|
+
.argument("<table>", "Source table id or slug.")
|
|
13
|
+
.argument("[slug]", "Relation slug — also the relation column key on the source, such as client. Omit it to name the relation from the target table (or reuse the one relation that already links them).")
|
|
14
|
+
.option("--to <table>", "Table to link to: id or slug. May also be a CRM object table (companies, people, deals).")
|
|
15
|
+
.option("--target-table <table>", "Same as --to.")
|
|
16
|
+
.option("--display-name <name>", "Display name for the relation column. Defaults to the target table's name, or the slug in Title Case when you name the slug.")
|
|
17
|
+
.option("--cardinality <cardinality>", "one_to_one, one_to_many, many_to_one, or many_to_many (source:target). Default many_to_many. one_to_many means each source row links many target rows and each target row links back to one source.")
|
|
18
|
+
.option("--inverse-slug <slug>", "Key of the linked-record column on the target table. Defaults to a name from the source table; a standard CRM object target (companies, people, deals) gets no column there.")
|
|
19
|
+
.option("--inverse-display-name <name>", "Display name for the inverse side.")
|
|
20
|
+
.option("--dry-run", "Preview the relation plan without writing.")
|
|
21
|
+
.option("--live", "Apply the relation. Default is dry-run.")
|
|
22
|
+
.option("--json", "Print a JSON envelope.")
|
|
23
|
+
.action(async (table, slug, options) => {
|
|
24
|
+
await handleAsyncAction("tables relate", options, () => {
|
|
25
|
+
const to = readOption(options.to);
|
|
26
|
+
const targetTable = readOption(options.targetTable);
|
|
27
|
+
if (to && targetTable && to !== targetTable) {
|
|
28
|
+
throw new OxygenError("invalid_request", "--to and --target-table name different tables; pass one.", { exitCode: 1 });
|
|
29
|
+
}
|
|
30
|
+
if (!to && !targetTable) {
|
|
31
|
+
throw new OxygenError("invalid_request", "Name the table to link to with --to <table>.", { exitCode: 1 });
|
|
32
|
+
}
|
|
33
|
+
return requestOxygen("/api/cli/tables/relations", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body: {
|
|
36
|
+
table,
|
|
37
|
+
relationship: {
|
|
38
|
+
...(readOption(slug) ? { slug: readOption(slug) } : {}),
|
|
39
|
+
target_table: to ?? targetTable,
|
|
40
|
+
...(options.displayName ? { display_name: options.displayName } : {}),
|
|
41
|
+
...(options.cardinality ? { cardinality: options.cardinality } : {}),
|
|
42
|
+
...(options.inverseSlug ? { inverse_slug: options.inverseSlug } : {}),
|
|
43
|
+
...(options.inverseDisplayName ? { inverse_display_name: options.inverseDisplayName } : {}),
|
|
44
|
+
},
|
|
45
|
+
mode: options.live ? "live" : "dry_run",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}))
|
|
50
|
+
.addCommand(new Command("relations")
|
|
51
|
+
.description("Inspect and manage native Tables-owned relation definitions.")
|
|
52
|
+
.addCommand(new Command("list")
|
|
53
|
+
.description("List the native relations visible from one table, including cardinality, target, and active link count.")
|
|
54
|
+
.argument("<table>", "Table id or slug.")
|
|
55
|
+
.option("--json", "Print a JSON envelope.")
|
|
56
|
+
.action(async (table, options) => {
|
|
57
|
+
await handleAsyncAction("tables relations list", options, () => {
|
|
58
|
+
const params = new URLSearchParams({ table });
|
|
59
|
+
return requestOxygen(`/api/cli/tables/relations?${params.toString()}`);
|
|
60
|
+
});
|
|
61
|
+
}))
|
|
62
|
+
.addCommand(new Command("update")
|
|
63
|
+
.description("Rename either side of a relation or change its cardinality. Endpoints and slugs stay immutable. Previews by default.")
|
|
64
|
+
.argument("<table>", "Table id or slug.")
|
|
65
|
+
.argument("<relation>", "Relation slug as seen from this table.")
|
|
66
|
+
.option("--display-name <name>", "New display name on this table.")
|
|
67
|
+
.option("--inverse-display-name <name>", "New display name on the other table.")
|
|
68
|
+
.option("--cardinality <cardinality>", "one_to_one, one_to_many, many_to_one, or many_to_many from this table's perspective.")
|
|
69
|
+
.option("--approved", "Apply after inspecting the preview.")
|
|
70
|
+
.option("--dry-run", "Preview without writing. This is the default.")
|
|
71
|
+
.option("--json", "Print a JSON envelope.")
|
|
72
|
+
.action(async (table, relation, options) => {
|
|
73
|
+
await handleAsyncAction("tables relations update", options, () => requestOxygen("/api/cli/tables/relations", {
|
|
74
|
+
method: "PATCH",
|
|
75
|
+
body: {
|
|
76
|
+
table,
|
|
77
|
+
relation,
|
|
78
|
+
...(readOption(options.displayName) ? { display_name: readOption(options.displayName) } : {}),
|
|
79
|
+
...(readOption(options.inverseDisplayName)
|
|
80
|
+
? { inverse_display_name: readOption(options.inverseDisplayName) }
|
|
81
|
+
: {}),
|
|
82
|
+
...(readOption(options.cardinality) ? { cardinality: readOption(options.cardinality) } : {}),
|
|
83
|
+
mode: options.approved && !options.dryRun ? "live" : "dry_run",
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
}))
|
|
87
|
+
.addCommand(new Command("archive")
|
|
88
|
+
.description("Archive a native relation, its active links, and both relation columns while preserving history. Previews by default.")
|
|
89
|
+
.argument("<table>", "Table id or slug.")
|
|
90
|
+
.argument("<relation>", "Relation slug as seen from this table.")
|
|
91
|
+
.option("--approved", "Apply after inspecting the preview.")
|
|
92
|
+
.option("--dry-run", "Preview without writing. This is the default.")
|
|
93
|
+
.option("--json", "Print a JSON envelope.")
|
|
94
|
+
.action(async (table, relation, options) => {
|
|
95
|
+
await handleAsyncAction("tables relations archive", options, () => requestOxygen("/api/cli/tables/relations", {
|
|
96
|
+
method: "DELETE",
|
|
97
|
+
body: {
|
|
98
|
+
table,
|
|
99
|
+
relation,
|
|
100
|
+
mode: options.approved && !options.dryRun ? "live" : "dry_run",
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
})))
|
|
104
|
+
.addCommand(new Command("unlink")
|
|
105
|
+
.description("Unlink one exact pair of rows by archiving their relation edge. The relation definition and both rows remain. Previews by default.")
|
|
106
|
+
.argument("<table>", "Source table id or slug.")
|
|
107
|
+
.argument("<row_id>", "Source row id.")
|
|
108
|
+
.requiredOption("--relation <slug>", "Relation slug as seen from the source table.")
|
|
109
|
+
.requiredOption("--target-row-id <row_id>", "Linked row id to remove.")
|
|
110
|
+
.option("--approved", "Apply after inspecting the preview.")
|
|
111
|
+
.option("--live", "Alias for --approved.")
|
|
112
|
+
.option("--dry-run", "Preview without writing. This is the default.")
|
|
113
|
+
.option("--json", "Print a JSON envelope.")
|
|
114
|
+
.action(async (table, rowId, options) => {
|
|
115
|
+
await handleAsyncAction("tables unlink", options, () => requestOxygen("/api/cli/tables/relations/unlink", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: {
|
|
118
|
+
table,
|
|
119
|
+
row_id: rowId,
|
|
120
|
+
relation: options.relation,
|
|
121
|
+
target_row_id: options.targetRowId,
|
|
122
|
+
mode: (options.approved || options.live) && !options.dryRun ? "live" : "dry_run",
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
}))
|
|
126
|
+
.addCommand(new Command("link")
|
|
127
|
+
.description("Link two tables: Oxygen finds the column that joins them, works out the direction, shows you how many rows will match, and on approval links every row. Free. Previews by default; --approved queues a background run and records each row's outcome in a system link_status_<relation> column. Wait for that run with `oxygen table-runs wait <run_id>`. (Also links one row to one row — see --relation.)")
|
|
128
|
+
.argument("<table>", "Table id or slug to link FROM.")
|
|
129
|
+
.argument("[row_id]", "Row form only: the source row id to link. Omit for the whole-table form.")
|
|
130
|
+
.option("--to <table>", "Table id or slug to link TO. May be a CRM object table (companies, people, deals).")
|
|
131
|
+
.option("--on <column>", "Pin one table's column by name or key. Oxygen must still find a compatible column on the other table: generic text needs a strongly similar label, while domains, emails, LinkedIn URLs, and external IDs pair by semantic role.")
|
|
132
|
+
.option("--approved", "Queue the bulk link after inspecting the preview; then wait with `oxygen table-runs wait <run_id>`.")
|
|
133
|
+
.option("--create-missing", "Also create target rows for unmatched source keys (CRM records when filling an existing CRM relationship). Free.")
|
|
134
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for the run (1-1000; a ceiling shared with worker and provider capacity). Defaults to 250; native --create-missing runs serialize target-row creation safely.")
|
|
135
|
+
.option("--undo <run_id>", "Undo a previous link run, archiving only the links that run created.")
|
|
136
|
+
.option("--relation <slug>", "Row form: relation slug defined on the source table, such as client.")
|
|
137
|
+
.option("--target-row-id <row_id>", "Row form: target row id in the related table.")
|
|
138
|
+
.option("--dry-run", "Preview without writing. This is the default.")
|
|
139
|
+
.option("--live", "Row form: apply the single-row link.")
|
|
140
|
+
.option("--json", "Print a JSON envelope.")
|
|
141
|
+
.action(async (table, rowId, options) => {
|
|
142
|
+
const wholeTableForm = Boolean(readOption(options.to) || readOption(options.undo));
|
|
143
|
+
const rowForm = Boolean(rowId || readOption(options.relation) || readOption(options.targetRowId));
|
|
144
|
+
// Usage errors go through emitCliFailure like every other command, NOT a
|
|
145
|
+
// bare throw: a raw throw escapes commander's action handler as an
|
|
146
|
+
// uncaught exception, which prints a Node stack trace to stderr instead
|
|
147
|
+
// of the `{ok:false,error}` envelope every other command emits, and lets
|
|
148
|
+
// Node's generic exit code override the error's own. This is the first
|
|
149
|
+
// thing a user sees when they half-remember the command, so it has to be
|
|
150
|
+
// the standard shape.
|
|
151
|
+
const usageError = (message) => new OxygenError("invalid_request", message, { exitCode: 1 });
|
|
152
|
+
// Neither shape is complete: print BOTH rather than guessing which the
|
|
153
|
+
// user meant. A bare `oxygen tables link people` is far more likely a
|
|
154
|
+
// half-remembered whole-table command than a malformed row link.
|
|
155
|
+
if (!wholeTableForm && !rowForm) {
|
|
156
|
+
emitCliFailure("tables link", usageError([
|
|
157
|
+
"Say which table to link to, or which two rows to link.",
|
|
158
|
+
"",
|
|
159
|
+
` Link two tables: oxygen tables link ${table} --to <table>`,
|
|
160
|
+
` Link two rows: oxygen tables link ${table} <row_id> --relation <slug> --target-row-id <id>`,
|
|
161
|
+
].join("\n")));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (wholeTableForm && rowForm) {
|
|
165
|
+
emitCliFailure("tables link", usageError("Use either the whole-table form (--to) or the row form (<row_id> --relation --target-row-id), not both."));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (rowForm) {
|
|
169
|
+
if (!rowId || !readOption(options.relation) || !readOption(options.targetRowId)) {
|
|
170
|
+
emitCliFailure("tables link", usageError(`Linking two rows needs all three: oxygen tables link ${table} <row_id> --relation <slug> --target-row-id <id>`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await handleAsyncAction("tables link", options, () => requestOxygen("/api/cli/tables/relations/link", {
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: {
|
|
176
|
+
table,
|
|
177
|
+
row_id: rowId,
|
|
178
|
+
relation: options.relation,
|
|
179
|
+
target_row_id: options.targetRowId,
|
|
180
|
+
// --approved is accepted as an alias for --live so one verb does
|
|
181
|
+
// not teach two different confirm words.
|
|
182
|
+
mode: options.live || options.approved ? "live" : "dry_run",
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
188
|
+
const undoRunId = readOption(options.undo);
|
|
189
|
+
const approved = Boolean(options.approved) && !options.dryRun;
|
|
190
|
+
const result = await requestOxygen("/api/cli/tables/link", {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: {
|
|
193
|
+
table,
|
|
194
|
+
...(undoRunId ? { undo_run_id: undoRunId } : { to: readOption(options.to) }),
|
|
195
|
+
...(readOption(options.on) ? { on: readOption(options.on) } : {}),
|
|
196
|
+
...(approved ? { approved: true } : { dry_run: true }),
|
|
197
|
+
...(options.createMissing ? { create_missing: true } : {}),
|
|
198
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
199
|
+
},
|
|
200
|
+
}).catch((error) => {
|
|
201
|
+
emitCliFailure("tables link", error);
|
|
202
|
+
return null;
|
|
203
|
+
});
|
|
204
|
+
if (!result)
|
|
205
|
+
return;
|
|
206
|
+
if (options.json) {
|
|
207
|
+
emitSuccess("tables link", result, options);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
writeTableLinkPreview(table, result, options);
|
|
211
|
+
}))
|
|
212
|
+
.addCommand(new Command("promote")
|
|
213
|
+
.description("Write bound rows' columns onto their matched CRM records (Records<->Tables). Free, but a truth write: previews by default, then re-run with --approved to write. Approved runs queue as a background run — watch with `oxygen table-runs wait <run_id>`.")
|
|
214
|
+
.argument("<table>", "Table id or slug whose bound rows to promote.")
|
|
215
|
+
.option("--object <slug>", "CRM object to promote onto (companies, people, deals, ...). Inferred when the table has exactly one bind column.")
|
|
216
|
+
.requiredOption("--map <pairs>", "Column-to-attribute mapping as column=attribute pairs (e.g. job_title=job_title,seniority=seniority) or a JSON array/object.")
|
|
217
|
+
.option("--policy <policy>", "Conflict policy: fill-empty-only (default), overwrite, or skip-conflicts.", "fill-empty-only")
|
|
218
|
+
.option("--row-id <id...>", "Promote only these row ids. Defaults to every bound row.")
|
|
219
|
+
.option("--dry-run", "Preview per-field would_fill/would_overwrite/conflicts without writing (the default when --approved is absent).")
|
|
220
|
+
.option("--approved", "Confirm the truth write onto CRM records after inspecting the preview.")
|
|
221
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for the run (1-1000; a ceiling shared with worker and provider capacity). Defaults to 50.")
|
|
222
|
+
.option("--json", "Print a JSON envelope.")
|
|
223
|
+
.action(async (table, options) => {
|
|
224
|
+
if (options.dryRun && options.approved) {
|
|
225
|
+
throw new OxygenError("invalid_promote", "Pass either --dry-run or --approved, not both.", { exitCode: 1 });
|
|
226
|
+
}
|
|
227
|
+
const mappings = parsePromoteMapOption(options.map);
|
|
228
|
+
const policy = normalizePromotePolicy(options.policy);
|
|
229
|
+
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
230
|
+
await handleAsyncAction("tables promote", options, () => requestOxygen("/api/cli/tables/promote", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: {
|
|
233
|
+
table,
|
|
234
|
+
...(readOption(options.object) ? { object: readOption(options.object) } : {}),
|
|
235
|
+
mappings,
|
|
236
|
+
policy,
|
|
237
|
+
...(options.rowId && options.rowId.length > 0 ? { row_ids: options.rowId } : {}),
|
|
238
|
+
// Preview by default; --approved is the only path that writes.
|
|
239
|
+
...(options.approved ? { approved: true } : { dry_run: true }),
|
|
240
|
+
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
241
|
+
},
|
|
242
|
+
}));
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { emitCliFailure, emitSuccess } from "../../cli-output.js";
|
|
2
|
+
import { collectRepeatable } from "../../command-support.js";
|
|
3
|
+
import { requestOxygen } from "../../http-client.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { buildDedupeRequest } from "./automation.js";
|
|
6
|
+
import { buildTablesSendRequest, writeTablesSendHint } from "./send.js";
|
|
7
|
+
export function registerRowActions({ tablesCommand, handleAsyncAction }) {
|
|
8
|
+
tablesCommand.addCommand(new Command("dedupe")
|
|
9
|
+
.description("Collapse duplicate rows by normalized (email/domain/linkedin) or fuzzy-label keys, or cross-check a column against another table. Previews by default; --apply --approved deletes losers (kept in row history).")
|
|
10
|
+
.argument("<table>", "Table id or slug to dedupe.")
|
|
11
|
+
.requiredOption("--on <cols>", "Key column(s): one column, or two comma-separated for a composite key (e.g. email or first_name,company).")
|
|
12
|
+
.option("--normalize <mode>", "Match mode: exact (default), email, domain, linkedin, or fuzzy-label. Fuzzy allowed only as a single key.", "exact")
|
|
13
|
+
.option("--keep <policy>", "Survivor per duplicate group: oldest (default), newest, or most-complete.", "oldest")
|
|
14
|
+
.option("--merge-values <mode>", "With --apply, fill the survivor's blank cells from the losers first: fill-empty.")
|
|
15
|
+
.option("--scan-limit <n>", "Max rows to scan. Defaults to 50000, capped at 200000.")
|
|
16
|
+
.option("--apply", "Delete the duplicate rows (losers). Without this flag the command only previews.")
|
|
17
|
+
.option("--approved", "Confirm the deletion (required with --apply); without it the API returns the preview to inspect first.")
|
|
18
|
+
.option("--against <table>", "Cross-table check: report rows whose --on value already exists in this other table (read-only).")
|
|
19
|
+
.option("--against-column <col>", "Column in the --against table to match against. Required with --against.")
|
|
20
|
+
.option("--json", "Print a JSON envelope.")
|
|
21
|
+
.action(async (table, options) => {
|
|
22
|
+
await handleAsyncAction("tables dedupe", options, () => {
|
|
23
|
+
const request = buildDedupeRequest(table, options);
|
|
24
|
+
return requestOxygen(request.endpoint, { method: "POST", body: request.body });
|
|
25
|
+
});
|
|
26
|
+
}));
|
|
27
|
+
tablesCommand.addCommand(new Command("send")
|
|
28
|
+
.description("Copy rows from one table into another as a durable background run (free, resumable). Map columns with --map (or --automap), optionally flatten an array column into one row per element, and insert or upsert. --dry-run previews the resolved mapping, row estimate, and sample rows without writing.")
|
|
29
|
+
.argument("<source>", "Source table id or slug.")
|
|
30
|
+
.argument("<target>", "Target table id or slug (must be a different table).")
|
|
31
|
+
.option("--map <pairs>", "target=source column pairs, comma-separated and/or repeated (e.g. --map company=company_name,website=domain).", collectRepeatable, [])
|
|
32
|
+
.option("--mapping <json>", 'Full mapping JSON array for literals and dot-paths, e.g. [{"target":"email","source":{"type":"column","key":"$element"},"path":"email"}]. Escape hatch when --map is not enough.')
|
|
33
|
+
.option("--automap", "Match columns automatically (exact key, then case-insensitive label). The resolved mapping is echoed back.")
|
|
34
|
+
.option("--flatten <column>", 'Source column whose array value emits one target row per element (the element is mapping source key "$element"); non-array values pass through as one row.')
|
|
35
|
+
.option("--flatten-path <path>", "Dot-path to the array inside the --flatten column's value.")
|
|
36
|
+
.option("--filter <json>", 'Source-row filters JSON, e.g. [{"column":"domain","op":"is_not_null"}]. Ops: eq, neq, is_null, is_not_null, in, not_in.')
|
|
37
|
+
.option("--mode <mode>", "insert (append, default) or upsert (requires --upsert-key).", "insert")
|
|
38
|
+
.option("--upsert-key <key>", "Mapped target column that identifies existing rows for upsert.")
|
|
39
|
+
.option("--page-size <n>", "Rows copied per durable page. Defaults to 500, max 2000.")
|
|
40
|
+
.option("--dry-run", "Validate and preview only; nothing is written.")
|
|
41
|
+
.option("--json", "Print a JSON envelope.")
|
|
42
|
+
.action(async (source, target, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const body = buildTablesSendRequest(source, target, options);
|
|
45
|
+
const data = await requestOxygen("/api/cli/tables/send", { method: "POST", body });
|
|
46
|
+
emitSuccess("tables send", data, options);
|
|
47
|
+
writeTablesSendHint(data);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
emitCliFailure("tables send", error);
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
}
|