@levr-one/cli 0.1.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.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # @levr-one/cli
2
+
3
+ The command-line interface for [Levr](https://www.levr.one). Push test results
4
+ from any CI pipeline, manage authentication, and select workspaces. The binary
5
+ is `levr`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @levr-one/cli # global install for daily use
11
+ # or run once, no install:
12
+ npx @levr-one/cli --help
13
+ ```
14
+
15
+ The package is self-contained — no peer setup required.
16
+
17
+ ### Shell completion (optional)
18
+
19
+ Tab-completion is an explicit opt-in step (it is not installed automatically):
20
+
21
+ ```bash
22
+ levr install # add shell completion for the current shell
23
+ levr uninstall # remove it
24
+ ```
25
+
26
+ ## Authentication
27
+
28
+ The CLI supports three authentication modes.
29
+
30
+ ### Interactive (browser) — default
31
+
32
+ ```bash
33
+ levr auth login
34
+ ```
35
+
36
+ Opens a browser for PKCE-based OAuth login.
37
+
38
+ ### Device code (SSH / headless)
39
+
40
+ ```bash
41
+ levr auth login --device-code
42
+ ```
43
+
44
+ A code is displayed in the terminal. Open the provided URL on any device, enter
45
+ the code, and approve.
46
+
47
+ ### Personal Access Token (CI/CD)
48
+
49
+ Set the `LEVR_TOKEN` environment variable and the CLI uses it automatically —
50
+ no interactive login:
51
+
52
+ ```bash
53
+ export LEVR_TOKEN=<your-personal-access-token>
54
+ levr push ./results.xml
55
+ ```
56
+
57
+ ### Other auth commands
58
+
59
+ ```bash
60
+ levr auth status # show current authentication state
61
+ levr auth logout # clear stored credentials
62
+ ```
63
+
64
+ ## Workspaces
65
+
66
+ ```bash
67
+ levr workspace list # list the workspaces you belong to
68
+ levr workspace select # choose the active workspace
69
+ levr workspace current # show the active workspace
70
+ ```
71
+
72
+ ## Push test results
73
+
74
+ ```bash
75
+ levr push <file> [options]
76
+ ```
77
+
78
+ The backend auto-detects the file format. In CI, the automation source name and
79
+ run metadata are auto-detected. Team ID is optional — when omitted, the server
80
+ resolves the team from the automation source (if `--source` matches a known
81
+ source) or the workspace's default team.
82
+
83
+ **Examples:**
84
+
85
+ ```bash
86
+ # Basic push (server resolves the default team)
87
+ levr push ./test-results.xml
88
+
89
+ # With an explicit team
90
+ levr push ./test-results.xml --team-id <uuid>
91
+
92
+ # With a custom source name and run name
93
+ levr push ./results.xml --source "backend-unit-tests" --run-name "nightly"
94
+ ```
95
+
96
+ **Flags:**
97
+
98
+ | Flag | Alias | Description |
99
+ | --------------------------- | ----- | ------------------------------------------------------------------------------------------ |
100
+ | `--team-id <uuid>` | `-t` | Team ID (optional; server resolves default if omitted. Or set `LEVR_TEAM_ID`) |
101
+ | `--source <name>` | `-s` | Automation source name — groups recurring imports and remembers team (auto-detected in CI) |
102
+ | `--run-name <name>` | `-r` | Name for the test run |
103
+ | `--format <type>` | `-f` | File format: `junit`, `gherkin`, `cucumber-json` (auto-detected if omitted) |
104
+ | `--parent-folder-id <uuid>` | | Destination folder ID |
105
+ | `--create-run` | | Force run creation for structure-only imports |
106
+ | `--update-mode <mode>` | | `update` (default) or `create_new` |
107
+ | `--verbose` | `-v` | Show detailed output |
108
+
109
+ ### Automation sources
110
+
111
+ An automation source groups recurring imports from the same CI pipeline or test
112
+ suite. When you pass `--source`, the server creates the source on first use and
113
+ remembers which team it belongs to. On the first push, omit `--team-id` to link
114
+ the source to the workspace's default team, or pass `--team-id` to link it to a
115
+ specific team. Subsequent pushes with the same `--source` route to that team
116
+ automatically. Source names are normalized (lowercased, trimmed).
117
+
118
+ ## CI/CD integration
119
+
120
+ ### GitHub Actions
121
+
122
+ ```yaml
123
+ - name: Push test results to Levr
124
+ env:
125
+ LEVR_TOKEN: ${{ secrets.LEVR_TOKEN }}
126
+ # LEVR_TEAM_ID is optional — server resolves from automation source or workspace default
127
+ run: npx @levr-one/cli push ./test-results.xml
128
+ ```
129
+
130
+ ### GitLab CI
131
+
132
+ ```yaml
133
+ push-results:
134
+ script:
135
+ - npx @levr-one/cli push ./test-results.xml
136
+ variables:
137
+ LEVR_TOKEN: $LEVR_TOKEN
138
+ ```
139
+
140
+ ### Jenkins
141
+
142
+ ```groovy
143
+ withEnv(["LEVR_TOKEN=${LEVR_TOKEN}"]) {
144
+ sh 'npx @levr-one/cli push ./test-results.xml'
145
+ }
146
+ ```
147
+
148
+ ## Configuration
149
+
150
+ All configuration is via environment variables. Flags take precedence.
151
+
152
+ | Variable | Description | Default |
153
+ | --------------- | ---------------------------------------------------------------------------------------- | ----------------------- |
154
+ | `LEVR_TOKEN` | Personal Access Token (for CI / headless) | |
155
+ | `LEVR_URL` | API base URL | `https://api.levr.one` |
156
+ | `LEVR_AUTH_URL` | Auth server URL | `https://auth.levr.one` |
157
+ | `LEVR_TEAM_ID` | Default team ID (optional; server resolves from automation source or workspace default) | |
158
+ | `LEVR_SOURCE` | Automation source name override (groups imports, remembers team) | |
159
+
160
+ ## Troubleshooting
161
+
162
+ **`Authentication required. Run: levr auth login`** — No valid credentials
163
+ found. Run `levr auth login` or set `LEVR_TOKEN`.
164
+
165
+ **`Token expired`** — Run `levr auth login` to re-authenticate.
166
+
167
+ **Browser doesn't open during `levr auth login`** — Use
168
+ `levr auth login --device-code` for headless environments.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,350 @@
1
+ import { buildApplication, buildCommand, buildRouteMap, text_en } from "@stricli/core";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+ import { buildInstallCommand, buildUninstallCommand } from "@stricli/auto-complete";
7
+
8
+ //#region src/utils/logger.ts
9
+ var Logger = class {
10
+ verbose;
11
+ stdout;
12
+ stderr;
13
+ constructor(options) {
14
+ this.verbose = options.verbose ?? false;
15
+ this.stdout = options.stdout ?? process.stdout;
16
+ this.stderr = options.stderr ?? process.stderr;
17
+ }
18
+ info(message) {
19
+ this.stdout.write(`${chalk.blue("info")} ${message}\n`);
20
+ }
21
+ success(message) {
22
+ this.stdout.write(`${chalk.green("ok")} ${message}\n`);
23
+ }
24
+ error(message) {
25
+ this.stderr.write(`${chalk.red("error")} ${message}\n`);
26
+ }
27
+ warning(message) {
28
+ this.stdout.write(`${chalk.yellow("warn")} ${message}\n`);
29
+ }
30
+ debug(message) {
31
+ if (this.verbose) this.stdout.write(`${chalk.gray("debug")} ${message}\n`);
32
+ }
33
+ setVerbose(verbose) {
34
+ this.verbose = verbose;
35
+ }
36
+ };
37
+
38
+ //#endregion
39
+ //#region src/context.ts
40
+ function buildContext(process$1) {
41
+ return {
42
+ process: process$1,
43
+ os,
44
+ fs,
45
+ path,
46
+ logger: new Logger({
47
+ verbose: false,
48
+ stdout: process$1.stdout,
49
+ stderr: process$1.stderr
50
+ })
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ //#region src/commands/auth/login.ts
56
+ const loginCommand = buildCommand({
57
+ docs: {
58
+ brief: "Authenticate with Levr",
59
+ fullDescription: `Authenticate with Levr using OAuth.
60
+
61
+ By default, opens a browser for PKCE-based authentication.
62
+ For headless environments (SSH, containers), use --device-code
63
+ to authenticate via a code displayed in the terminal.
64
+
65
+ Examples:
66
+ levr auth login # Browser-based PKCE login
67
+ levr auth login --device-code # Device flow for SSH/headless`
68
+ },
69
+ parameters: {
70
+ flags: { "device-code": {
71
+ kind: "boolean",
72
+ default: false,
73
+ brief: "Use device code flow (for SSH/headless environments)"
74
+ } },
75
+ aliases: { d: "device-code" }
76
+ },
77
+ loader: async () => {
78
+ const { loginHandler } = await import("./loginHandler-C8D6DgjW.js");
79
+ return loginHandler;
80
+ }
81
+ });
82
+
83
+ //#endregion
84
+ //#region src/commands/auth/logout.ts
85
+ const logoutCommand = buildCommand({
86
+ docs: {
87
+ brief: "Log out of Levr",
88
+ fullDescription: `Remove stored credentials.
89
+
90
+ Note: If using LEVR_TOKEN environment variable, it will remain set.
91
+
92
+ Examples:
93
+ levr auth logout`
94
+ },
95
+ parameters: {},
96
+ loader: async () => {
97
+ const { logoutHandler } = await import("./logoutHandler-BBtdpxHX.js");
98
+ return logoutHandler;
99
+ }
100
+ });
101
+
102
+ //#endregion
103
+ //#region src/commands/auth/status.ts
104
+ const statusCommand = buildCommand({
105
+ docs: {
106
+ brief: "Check authentication status",
107
+ fullDescription: `Check the current authentication status.
108
+
109
+ Shows whether you are authenticated, the auth method (PAT or JWT),
110
+ and tests API reachability.
111
+
112
+ Examples:
113
+ levr auth status`
114
+ },
115
+ parameters: {},
116
+ loader: async () => {
117
+ const { statusHandler } = await import("./statusHandler-DQDmEOGI.js");
118
+ return statusHandler;
119
+ }
120
+ });
121
+
122
+ //#endregion
123
+ //#region src/commands/push.ts
124
+ const pushCommand = buildCommand({
125
+ docs: {
126
+ brief: "Push test results to Levr",
127
+ fullDescription: `Upload a test result file to Levr.
128
+
129
+ The backend auto-detects the file format (JUnit XML, Gherkin, Cucumber JSON).
130
+ In CI environments, the automation source name and CI metadata are auto-detected.
131
+
132
+ Team ID is optional. When omitted, the server resolves the team from:
133
+ 1. The existing automation source's team (if --source matches a known source)
134
+ 2. The workspace's default team
135
+
136
+ Examples:
137
+ levr push ./test-results.xml
138
+ levr push ./results.xml --source "backend-unit-tests"
139
+ levr push ./report.json --team-id <uuid> # explicit team
140
+ levr push ./report.json # uses LEVR_TOKEN env var, default team`
141
+ },
142
+ parameters: {
143
+ positional: {
144
+ kind: "tuple",
145
+ parameters: [{
146
+ parse: String,
147
+ brief: "Path to test result file (.xml, .feature, .json)",
148
+ placeholder: "file",
149
+ optional: false
150
+ }]
151
+ },
152
+ flags: {
153
+ "workspace-id": {
154
+ kind: "parsed",
155
+ parse: String,
156
+ brief: "Workspace ID (required for multi-workspace JWT auth)",
157
+ placeholder: "uuid",
158
+ optional: true
159
+ },
160
+ "team-id": {
161
+ kind: "parsed",
162
+ parse: String,
163
+ brief: "Team ID (optional; server resolves default if omitted)",
164
+ placeholder: "uuid",
165
+ optional: true
166
+ },
167
+ source: {
168
+ kind: "parsed",
169
+ parse: String,
170
+ brief: "Automation source name (auto-detected in CI)",
171
+ placeholder: "name",
172
+ optional: true
173
+ },
174
+ "automation-source": {
175
+ kind: "parsed",
176
+ parse: String,
177
+ brief: "Automation source UUID. When set, routes to POST /v1/automation-run/ingest (synchronous, bypasses ImportJob queue) instead of POST /v1/imports.",
178
+ placeholder: "uuid",
179
+ optional: true
180
+ },
181
+ "run-name": {
182
+ kind: "parsed",
183
+ parse: String,
184
+ brief: "Name for the test run",
185
+ placeholder: "name",
186
+ optional: true
187
+ },
188
+ format: {
189
+ kind: "enum",
190
+ values: [
191
+ "junit",
192
+ "gherkin",
193
+ "cucumber-json"
194
+ ],
195
+ brief: "File format (auto-detected if omitted)",
196
+ optional: true
197
+ },
198
+ "parent-folder-id": {
199
+ kind: "parsed",
200
+ parse: String,
201
+ brief: "Destination folder ID",
202
+ placeholder: "uuid",
203
+ optional: true
204
+ },
205
+ "update-mode": {
206
+ kind: "enum",
207
+ values: ["update", "create_new"],
208
+ default: "update",
209
+ brief: "How to handle existing tests"
210
+ },
211
+ verbose: {
212
+ kind: "boolean",
213
+ default: false,
214
+ brief: "Show detailed output"
215
+ }
216
+ },
217
+ aliases: {
218
+ w: "workspace-id",
219
+ t: "team-id",
220
+ s: "source",
221
+ a: "automation-source",
222
+ r: "run-name",
223
+ f: "format",
224
+ v: "verbose"
225
+ }
226
+ },
227
+ loader: async () => {
228
+ const { pushHandler } = await import("./pushHandler-DkCrbV7y.js");
229
+ return pushHandler;
230
+ }
231
+ });
232
+
233
+ //#endregion
234
+ //#region src/commands/workspace/list.ts
235
+ const listCommand = buildCommand({
236
+ docs: {
237
+ brief: "List available workspaces",
238
+ fullDescription: `List all workspaces you have access to.
239
+
240
+ The current workspace (if selected) is marked with an asterisk (*).
241
+
242
+ Requires JWT authentication (levr auth login).
243
+
244
+ Examples:
245
+ levr workspace list`
246
+ },
247
+ parameters: {},
248
+ loader: async () => {
249
+ const { listHandler } = await import("./listHandler-BiwIo88i.js");
250
+ return listHandler;
251
+ }
252
+ });
253
+
254
+ //#endregion
255
+ //#region src/commands/workspace/select.ts
256
+ const selectCommand = buildCommand({
257
+ docs: {
258
+ brief: "Select a workspace",
259
+ fullDescription: `Select a workspace by ID.
260
+
261
+ The selected workspace is used for all subsequent commands.
262
+ Use 'levr workspace list' to see available workspaces.
263
+
264
+ Requires JWT authentication (levr auth login).
265
+
266
+ Examples:
267
+ levr workspace select <workspace-id>`
268
+ },
269
+ parameters: {
270
+ positional: {
271
+ kind: "tuple",
272
+ parameters: [{
273
+ parse: String,
274
+ brief: "Workspace ID",
275
+ placeholder: "workspace-id",
276
+ optional: false
277
+ }]
278
+ },
279
+ flags: {}
280
+ },
281
+ loader: async () => {
282
+ const { selectHandler } = await import("./selectHandler-C2UDAWOl.js");
283
+ return selectHandler;
284
+ }
285
+ });
286
+
287
+ //#endregion
288
+ //#region src/commands/workspace/current.ts
289
+ const currentCommand = buildCommand({
290
+ docs: {
291
+ brief: "Show current workspace",
292
+ fullDescription: `Show the currently selected workspace.
293
+
294
+ Examples:
295
+ levr workspace current`
296
+ },
297
+ parameters: {},
298
+ loader: async () => {
299
+ const { currentHandler } = await import("./currentHandler-oXFeNNgd.js");
300
+ return currentHandler;
301
+ }
302
+ });
303
+
304
+ //#endregion
305
+ //#region src/app.ts
306
+ const routes = buildRouteMap({
307
+ routes: {
308
+ auth: buildRouteMap({
309
+ routes: {
310
+ login: loginCommand,
311
+ logout: logoutCommand,
312
+ status: statusCommand
313
+ },
314
+ docs: { brief: "Manage authentication" }
315
+ }),
316
+ workspace: buildRouteMap({
317
+ routes: {
318
+ list: listCommand,
319
+ select: selectCommand,
320
+ current: currentCommand
321
+ },
322
+ docs: { brief: "Manage workspace selection" }
323
+ }),
324
+ push: pushCommand,
325
+ install: buildInstallCommand("levr", { bash: "__levr_bash_complete" }),
326
+ uninstall: buildUninstallCommand("levr", { bash: true })
327
+ },
328
+ docs: {
329
+ brief: "The command-line interface for Levr",
330
+ hideRoute: {
331
+ install: true,
332
+ uninstall: true
333
+ }
334
+ }
335
+ });
336
+ const app = buildApplication(routes, {
337
+ name: "levr",
338
+ versionInfo: { currentVersion: "0.1.0" },
339
+ localization: { loadText: () => ({
340
+ ...text_en,
341
+ exceptionWhileParsingArguments: (exc, ansiColor) => {
342
+ const base = text_en.exceptionWhileParsingArguments(exc, ansiColor);
343
+ const hint = "Run `levr <command> --help` for usage information.";
344
+ return ansiColor ? `${base}\n\x1b[2m${hint}\x1b[22m` : `${base}\n${hint}`;
345
+ }
346
+ }) }
347
+ });
348
+
349
+ //#endregion
350
+ export { app, buildContext };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { app, buildContext } from "./app-0F0Zyyt7.js";
3
+ import { proposeCompletions } from "@stricli/core";
4
+
5
+ //#region src/bin/bash-complete.ts
6
+ const inputs = process.argv.slice(3);
7
+ if (process.env["COMP_LINE"]?.endsWith(" ")) inputs.push("");
8
+ try {
9
+ for (const { completion } of await proposeCompletions(app, inputs, buildContext(process))) process.stdout.write(`${completion}\n`);
10
+ } catch {}
11
+
12
+ //#endregion
13
+ export { };
package/dist/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { app, buildContext } from "./app-0F0Zyyt7.js";
3
+ import { run } from "@stricli/core";
4
+
5
+ //#region src/bin/cli.ts
6
+ let savedExitCode;
7
+ const processProxy = new Proxy(process, { set(target, prop, value) {
8
+ if (prop === "exitCode" && typeof value === "number" && value !== 0) savedExitCode = value;
9
+ return Reflect.set(target, prop, value);
10
+ } });
11
+ await run(app, process.argv.slice(2), buildContext(processProxy));
12
+ if (savedExitCode !== void 0) process.exitCode = savedExitCode;
13
+
14
+ //#endregion
15
+ export { };
@@ -0,0 +1,54 @@
1
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ //#region src/utils/env.ts
6
+ const DEFAULT_API_URL = "https://api.levr.one";
7
+ const DEFAULT_AUTH_URL = "https://auth.levr.one";
8
+ /** OAuth client ID for the CLI (seeded in oauth_clients table) */
9
+ const CLI_CLIENT_ID = "3";
10
+ function getApiUrl() {
11
+ return process.env["LEVR_URL"] || DEFAULT_API_URL;
12
+ }
13
+ function getAuthUrl() {
14
+ return process.env["LEVR_AUTH_URL"] || DEFAULT_AUTH_URL;
15
+ }
16
+ function getTeamId(flagValue) {
17
+ return flagValue || process.env["LEVR_TEAM_ID"] || void 0;
18
+ }
19
+ function getPatToken() {
20
+ return process.env["LEVR_TOKEN"];
21
+ }
22
+ function getSourceOverride() {
23
+ return process.env["LEVR_SOURCE"];
24
+ }
25
+ function getAutomationSourceIdOverride() {
26
+ return process.env["LEVR_AUTOMATION_SOURCE_ID"];
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/auth/credentials.ts
31
+ const CREDENTIALS_PATH = join(join(homedir(), ".config", "levr"), "credentials.json");
32
+ function readCredentials() {
33
+ try {
34
+ const raw = readFileSync(CREDENTIALS_PATH, "utf8");
35
+ return JSON.parse(raw);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ function writeCredentials(creds) {
41
+ mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
42
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
43
+ }
44
+ function deleteCredentials() {
45
+ try {
46
+ unlinkSync(CREDENTIALS_PATH);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ //#endregion
54
+ export { CLI_CLIENT_ID, deleteCredentials, getApiUrl, getAuthUrl, getAutomationSourceIdOverride, getPatToken, getSourceOverride, getTeamId, readCredentials, writeCredentials };
@@ -0,0 +1,11 @@
1
+ import { loadWorkspace } from "./workspace-store-BcyMJAht.js";
2
+
3
+ //#region src/commands/workspace/currentHandler.ts
4
+ function currentHandler() {
5
+ const workspaceId = loadWorkspace();
6
+ if (workspaceId) this.process.stdout.write(`Current workspace: ${workspaceId}\n`);
7
+ else this.process.stdout.write("No workspace selected.\n");
8
+ }
9
+
10
+ //#endregion
11
+ export { currentHandler };
@@ -0,0 +1,44 @@
1
+ import "./credentials-CfHLkU7k.js";
2
+ import { authGetSitesV1, configureClient } from "./sdk-client-DunBmYLR.js";
3
+ import { loadWorkspace } from "./workspace-store-BcyMJAht.js";
4
+ import "./token-refresh-waF23pyw.js";
5
+ import { resolveToken } from "./resolve-token-BL8vL_ok.js";
6
+
7
+ //#region src/commands/workspace/listHandler.ts
8
+ async function listHandler() {
9
+ let auth;
10
+ try {
11
+ auth = await resolveToken();
12
+ } catch {
13
+ this.logger.error("Not authenticated. Run 'levr auth login' first.");
14
+ this.process.exitCode = 1;
15
+ return;
16
+ }
17
+ if (auth.type === "pat") {
18
+ this.logger.error("Workspace listing requires JWT auth. Run 'levr auth login'.");
19
+ this.process.exitCode = 1;
20
+ return;
21
+ }
22
+ configureClient(auth);
23
+ const result = await authGetSitesV1();
24
+ if (result.error) {
25
+ this.logger.error("Failed to list workspaces.");
26
+ this.process.exitCode = 1;
27
+ return;
28
+ }
29
+ const sites = result.data.sites;
30
+ if (sites.length === 0) {
31
+ this.logger.info("No workspaces available.");
32
+ return;
33
+ }
34
+ const currentWs = loadWorkspace();
35
+ this.process.stdout.write("\nWorkspaces:\n\n");
36
+ for (const site of sites) {
37
+ const indicator = site.workspace_id === currentWs ? " *" : "";
38
+ this.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
39
+ }
40
+ this.process.stdout.write("\n");
41
+ }
42
+
43
+ //#endregion
44
+ export { listHandler };