@levr-one/cli 0.3.0 → 0.4.0

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.
@@ -0,0 +1,285 @@
1
+ import { getApiUrl } from "./env-NxtzJJPk.js";
2
+ import { configureClient, testCaseImportCommitV1, testCaseImportPreviewV1 } from "./sdk-client-DyRTezC5.js";
3
+ import "./workspace-store-4hfvsEHS.js";
4
+ import { resolveWorkspace } from "./resolve-workspace-CO8d3IBz.js";
5
+ import "./token-refresh-Cz-FqDtC.js";
6
+ import { resolveToken } from "./resolve-token-Dq8cAuiu.js";
7
+ import { createReadStream } from "node:fs";
8
+ import { basename } from "node:path";
9
+ import chalk from "chalk";
10
+ import { readFile, writeFile } from "node:fs/promises";
11
+ import { createInterface } from "node:readline/promises";
12
+ import ora from "ora";
13
+
14
+ //#region src/utils/import-mapping.ts
15
+ const IMPORT_TARGETS = [
16
+ "row_type",
17
+ "test_id",
18
+ "test_key",
19
+ "folder_id",
20
+ "folder_path",
21
+ "folder_name",
22
+ "test_name",
23
+ "test_description",
24
+ "test_type",
25
+ "case_type_id",
26
+ "case_type_name",
27
+ "test_priority",
28
+ "estimate",
29
+ "is_automated",
30
+ "assignee_email",
31
+ "labels",
32
+ "attachment_filenames",
33
+ "data_set_names",
34
+ "sequence",
35
+ "item_id",
36
+ "method",
37
+ "expected_result",
38
+ "keyword",
39
+ "shared_step_id",
40
+ "shared_precondition_id",
41
+ "data_table",
42
+ "steps",
43
+ "preconditions"
44
+ ];
45
+ const REQUIRED_TARGETS = ["test_name"];
46
+ /**
47
+ * Parse a --map value: "Source Column=target", "Source Column=" (drop),
48
+ * or "Source Column=labels:prefix" (import-as-labels: each value becomes
49
+ * the label "prefix:value", preserving the column's provenance).
50
+ */
51
+ function parseMapFlag(pair) {
52
+ const separator = pair.indexOf("=");
53
+ if (separator <= 0) throw new Error(`Invalid --map "${pair}" — expected "Source Column=target_field" (or "Source Column=" to drop, or "Source Column=labels:prefix" to import values as prefixed labels).`);
54
+ const column = pair.slice(0, separator).trim();
55
+ const raw = pair.slice(separator + 1).trim();
56
+ if (raw === "") return {
57
+ column,
58
+ target: null
59
+ };
60
+ if (raw.startsWith("labels:")) {
61
+ const labelPrefix = raw.slice(7).trim();
62
+ if (!labelPrefix) throw new Error(`Invalid --map "${pair}" — "labels:" needs a prefix (e.g. "State=labels:state").`);
63
+ return {
64
+ column,
65
+ target: "labels",
66
+ labelPrefix
67
+ };
68
+ }
69
+ const target = IMPORT_TARGETS.find((t) => t === raw);
70
+ if (!target) throw new Error(`Invalid --map target "${raw}". Valid targets: ${IMPORT_TARGETS.join(", ")} (labels also accepts "labels:prefix")`);
71
+ return {
72
+ column,
73
+ target
74
+ };
75
+ }
76
+ /** Apply overrides (from --map / --mapping-file) onto the proposed mapping. */
77
+ function applyOverrides(mapping, overrides) {
78
+ const result = mapping.map((entry) => ({ ...entry }));
79
+ for (const override of overrides) {
80
+ const entry = result.find((m) => m.originalName === override.column);
81
+ if (!entry) throw new Error(`--map column "${override.column}" not found in the file. Columns: ${result.map((m) => m.originalName).join(", ")}`);
82
+ entry.targetProperty = override.target;
83
+ entry.matchType = override.target ? "manual" : "unmapped";
84
+ entry.confidence = override.target ? 1 : 0;
85
+ entry.labelPrefix = override.labelPrefix;
86
+ }
87
+ return result;
88
+ }
89
+ /** Required targets not covered by any mapped column. */
90
+ function missingRequired(mapping) {
91
+ const covered = new Set(mapping.map((m) => m.targetProperty).filter(Boolean));
92
+ return REQUIRED_TARGETS.filter((t) => !covered.has(t));
93
+ }
94
+ /** Columns worth walking interactively: unmapped, or LLM guesses < 0.7. */
95
+ function columnsNeedingReview(mapping) {
96
+ return mapping.filter((m) => m.targetProperty === null || m.matchType === "llm" && m.confidence < .7);
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/commands/importHandler.ts
101
+ const MATCH_COLORS = {
102
+ exact: chalk.green,
103
+ prefix: chalk.green,
104
+ preset: chalk.green,
105
+ fuzzy: chalk.yellow,
106
+ llm: chalk.magenta,
107
+ unmapped: chalk.red,
108
+ manual: chalk.green
109
+ };
110
+ /** Stream the file into a Blob — no readFileSync, no sync stall. */
111
+ async function fileToBlob(path$1) {
112
+ const chunks = [];
113
+ for await (const chunk of createReadStream(path$1)) chunks.push(chunk);
114
+ return new Blob(chunks);
115
+ }
116
+ function printMapping(logger, mapping) {
117
+ const width = Math.max(...mapping.map((m) => m.originalName.length), 13);
118
+ logger.info("");
119
+ logger.info(chalk.bold(` ${"SOURCE COLUMN".padEnd(width)} → ${"TARGET".padEnd(24)} MATCH`));
120
+ for (const m of mapping) {
121
+ const color = MATCH_COLORS[m.matchType] ?? chalk.white;
122
+ const target = m.targetProperty ?? "— unmapped —";
123
+ const badge = m.matchType === "unmapped" ? "unmapped" : `${m.matchType} ${Math.round(m.confidence * 100)}%`;
124
+ logger.info(` ${m.originalName.padEnd(width)} → ${target.padEnd(24)} ${color(badge)}`);
125
+ }
126
+ logger.info("");
127
+ }
128
+ async function pickTarget(rl, logger, column) {
129
+ logger.info(chalk.bold(`"${column.originalName}" is ${column.targetProperty ? `mapped to ${column.targetProperty} (low confidence)` : "unmapped"}.`));
130
+ const columns = 3;
131
+ const cellWidth = 26;
132
+ for (let i = 0; i < IMPORT_TARGETS.length; i += columns) logger.info(" " + IMPORT_TARGETS.slice(i, i + columns).map((t, j) => `${String(i + j + 1).padStart(2)}) ${t}`.padEnd(cellWidth)).join(""));
133
+ for (;;) {
134
+ const answer = (await rl.question("Target number or name (blank = skip/drop this column): ")).trim();
135
+ if (answer === "") return null;
136
+ const byNumber = IMPORT_TARGETS[Number(answer) - 1];
137
+ if (byNumber && String(Number(answer)) === answer) return byNumber;
138
+ const byName = IMPORT_TARGETS.find((t) => t === answer.toLowerCase());
139
+ if (byName) return byName;
140
+ logger.info(chalk.red(` "${answer}" is not a valid target — try again.`));
141
+ }
142
+ }
143
+ async function importHandler(flags, file) {
144
+ if (flags.verbose) this.logger.setVerbose(true);
145
+ if (!file && !flags["sheets-url"]) {
146
+ this.logger.error("Provide a file path or --sheets-url.");
147
+ this.process.exitCode = 1;
148
+ return;
149
+ }
150
+ if (file && flags["sheets-url"]) {
151
+ this.logger.error("Provide a file OR --sheets-url, not both.");
152
+ this.process.exitCode = 1;
153
+ return;
154
+ }
155
+ let auth;
156
+ try {
157
+ auth = await resolveToken();
158
+ } catch (err) {
159
+ this.logger.error(err instanceof Error ? err.message : "Authentication failed.");
160
+ this.process.exitCode = 1;
161
+ return;
162
+ }
163
+ configureClient(auth);
164
+ if (flags.verbose) {
165
+ this.logger.debug(`Auth: ${auth.type.toUpperCase()}`);
166
+ this.logger.debug(`API: ${getApiUrl()}`);
167
+ }
168
+ if (auth.type === "jwt") {
169
+ if (!await resolveWorkspace.call(this, flags["workspace-id"])) {
170
+ this.process.exitCode = 1;
171
+ return;
172
+ }
173
+ }
174
+ const spinner = ora("Uploading and analyzing the file…").start();
175
+ let preview;
176
+ try {
177
+ const body = {
178
+ team_id: flags["team-id"],
179
+ format: flags.format,
180
+ sheets_url: flags["sheets-url"]
181
+ };
182
+ if (file) {
183
+ const blob = await fileToBlob(file);
184
+ body["file"] = new File([blob], basename(file));
185
+ }
186
+ const result = await testCaseImportPreviewV1({
187
+ body,
188
+ requestValidator: void 0
189
+ });
190
+ if (result.error || !result.data) throw new Error(result.error?.message ?? `Preview failed (HTTP ${result.response?.status ?? "?"}).`);
191
+ preview = result.data;
192
+ spinner.succeed(`${preview.filename} (${preview.format}) — ${preview.row_count} data rows detected`);
193
+ } catch (err) {
194
+ spinner.fail(err instanceof Error ? err.message : "Preview failed.");
195
+ this.process.exitCode = 1;
196
+ return;
197
+ }
198
+ let mapping;
199
+ try {
200
+ const overrides = (flags.map ?? []).map(parseMapFlag);
201
+ if (flags["mapping-file"]) {
202
+ const saved = JSON.parse(await readFile(flags["mapping-file"], "utf8"));
203
+ overrides.push(...saved);
204
+ }
205
+ mapping = applyOverrides(preview.proposed_mapping, overrides);
206
+ } catch (err) {
207
+ this.logger.error(err instanceof Error ? err.message : String(err));
208
+ this.process.exitCode = 1;
209
+ return;
210
+ }
211
+ printMapping(this.logger, mapping);
212
+ if (!flags.yes && process.stdin.isTTY === true) {
213
+ const rl = createInterface({
214
+ input: process.stdin,
215
+ output: process.stdout
216
+ });
217
+ try {
218
+ for (const column of columnsNeedingReview(mapping)) {
219
+ const target = await pickTarget(rl, this.logger, column);
220
+ mapping = applyOverrides(mapping, [{
221
+ column: column.originalName,
222
+ target
223
+ }]);
224
+ }
225
+ while (missingRequired(mapping).length > 0) {
226
+ this.logger.info(chalk.red(`Required field(s) uncovered: ${missingRequired(mapping).join(", ")}. Every test case needs a name.`));
227
+ const source = (await rl.question(`Which source column holds the test name? (${mapping.map((m) => m.originalName).join(", ")}): `)).trim();
228
+ const entry = mapping.find((m) => m.originalName === source);
229
+ if (!entry) {
230
+ this.logger.info(chalk.red(` "${source}" is not a column — try again.`));
231
+ continue;
232
+ }
233
+ mapping = applyOverrides(mapping, [{
234
+ column: entry.originalName,
235
+ target: "test_name"
236
+ }]);
237
+ }
238
+ printMapping(this.logger, mapping);
239
+ const confirm = (await rl.question("Commit this import? [y/N]: ")).trim().toLowerCase();
240
+ if (confirm !== "y" && confirm !== "yes") {
241
+ this.logger.info("Aborted — nothing was written.");
242
+ return;
243
+ }
244
+ } finally {
245
+ rl.close();
246
+ }
247
+ } else if (missingRequired(mapping).length > 0) {
248
+ this.logger.error(`Required field(s) uncovered: ${missingRequired(mapping).join(", ")}. Map them with --map "Source Column=test_name" (non-interactive runs cannot prompt).`);
249
+ this.process.exitCode = 1;
250
+ return;
251
+ }
252
+ if (flags["save-mapping"]) {
253
+ await writeFile(flags["save-mapping"], JSON.stringify(mapping.map((m) => ({
254
+ column: m.originalName,
255
+ target: m.targetProperty
256
+ })), null, 2) + "\n");
257
+ this.logger.info(`Confirmed mapping saved to ${flags["save-mapping"]}`);
258
+ }
259
+ const commitSpinner = ora("Committing import…").start();
260
+ let outcome;
261
+ try {
262
+ const result = await testCaseImportCommitV1({
263
+ body: {
264
+ token: preview.token,
265
+ confirmed_mapping: mapping
266
+ },
267
+ requestValidator: void 0
268
+ });
269
+ if (result.error || !result.data) throw new Error(result.error?.message ?? `Commit failed (HTTP ${result.response?.status ?? "?"}).`);
270
+ outcome = result.data;
271
+ } catch (err) {
272
+ commitSpinner.fail(err instanceof Error ? err.message : "Commit failed.");
273
+ this.process.exitCode = 1;
274
+ return;
275
+ }
276
+ const { stats } = outcome;
277
+ if (outcome.status === "failed") commitSpinner.fail(`Import failed — 0 of ${stats.rows_processed} rows imported.`);
278
+ else commitSpinner.succeed(`Import ${outcome.status.replace(/_/g, " ")}: ${stats.tests_created} tests, ${stats.folders_created} folders, ${stats.steps_created} steps, ${stats.preconditions_created} preconditions (${stats.rows_failed} rows failed)`);
279
+ for (const warning of outcome.warnings) this.logger.info(chalk.yellow(` warning: ${warning.message}`));
280
+ for (const error of outcome.errors) this.logger.info(chalk.red(` error: ${error.message}`));
281
+ if (outcome.status === "failed") this.process.exitCode = 1;
282
+ }
283
+
284
+ //#endregion
285
+ export { importHandler };
@@ -1,8 +1,8 @@
1
1
  import "./env-NxtzJJPk.js";
2
- import { authGetSitesV1, configureClient } from "./sdk-client-BeE6mXns.js";
2
+ import { authGetSitesV1, configureClient } from "./sdk-client-DyRTezC5.js";
3
3
  import { loadWorkspace } from "./workspace-store-4hfvsEHS.js";
4
4
  import "./token-refresh-Cz-FqDtC.js";
5
- import { resolveToken } from "./resolve-token-BRPrqdG-.js";
5
+ import { resolveToken } from "./resolve-token-Dq8cAuiu.js";
6
6
 
7
7
  //#region src/commands/workspace/listHandler.ts
8
8
  async function listHandler() {
@@ -1,7 +1,7 @@
1
1
  import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-NxtzJJPk.js";
2
- import { configureClient } from "./sdk-client-BeE6mXns.js";
2
+ import { configureClient } from "./sdk-client-DyRTezC5.js";
3
3
  import "./workspace-store-4hfvsEHS.js";
4
- import { autoSelectWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
4
+ import { autoSelectWorkspace } from "./resolve-workspace-CO8d3IBz.js";
5
5
  import chalk from "chalk";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
7
  import { createServer } from "node:http";
@@ -1,9 +1,9 @@
1
1
  import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-NxtzJJPk.js";
2
- import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-BeE6mXns.js";
2
+ import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-DyRTezC5.js";
3
3
  import "./workspace-store-4hfvsEHS.js";
4
- import { resolveWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
4
+ import { resolveWorkspace } from "./resolve-workspace-CO8d3IBz.js";
5
5
  import "./token-refresh-Cz-FqDtC.js";
6
- import { resolveToken } from "./resolve-token-BRPrqdG-.js";
6
+ import { resolveToken } from "./resolve-token-Dq8cAuiu.js";
7
7
  import { readFileSync, statSync } from "node:fs";
8
8
  import { basename } from "node:path";
9
9
  import ora from "ora";
@@ -1,4 +1,4 @@
1
- import { authGetSitesV1 } from "./sdk-client-BeE6mXns.js";
1
+ import { authGetSitesV1 } from "./sdk-client-DyRTezC5.js";
2
2
  import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-4hfvsEHS.js";
3
3
 
4
4
  //#region src/workspace/resolve-workspace.ts