@levr-one/cli 0.3.1 → 0.5.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 +95 -4
- package/dist/{addHandler-boWTd9yY.js → addHandler-K2a4rVv8.js} +14 -14
- package/dist/cli.js +171 -29
- package/dist/completionHandler-DtG_CsK0.js +3 -0
- package/dist/completionHandler-O2G_WZLf.js +95 -0
- package/dist/{currentHandler-Bm7GFMkj.js → currentHandler-CHydiFui.js} +1 -1
- package/dist/{env-NxtzJJPk.js → env-CHeKHu5S.js} +1 -1
- package/dist/importHandler-D27Xrx73.js +285 -0
- package/dist/{listHandler-7DZhE8jn.js → listHandler-8uqFClxw.js} +5 -5
- package/dist/{loginHandler-Dfg89AD2.js → loginHandler-BhvCWTd8.js} +4 -4
- package/dist/{logoutHandler-BC8laIAB.js → logoutHandler-BFutKash.js} +2 -2
- package/dist/{pushHandler-CC709aNJ.js → pushHandler-v4-lHK6A.js} +6 -6
- package/dist/{resolve-token-BRPrqdG-.js → resolve-token-DbQsmn03.js} +2 -2
- package/dist/{resolve-workspace-vJZku4FK.js → resolve-workspace-BnTDxvYq.js} +2 -2
- package/dist/{sdk-client-BWrIbeUg.js → sdk-client-DP7uCfXP.js} +412 -36
- package/dist/{selectHandler-nOUsPW9p.js → selectHandler-DeEZe6PU.js} +5 -5
- package/dist/{statusHandler-DljfUcC2.js → statusHandler-B6XRPg4k.js} +3 -3
- package/dist/{token-refresh-Cz-FqDtC.js → token-refresh-Cu5RpkLJ.js} +1 -1
- package/dist/{workspace-store-4hfvsEHS.js → workspace-store-DDOxnut1.js} +1 -1
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @levr-one/cli
|
|
2
2
|
|
|
3
3
|
The command-line interface for [Levr](https://www.levr.one). The binary is
|
|
4
|
-
`levr`, and it does
|
|
4
|
+
`levr`, and it does three jobs:
|
|
5
5
|
|
|
6
6
|
- **Connect your AI tools** — `levr mcp add` wires the Levr MCP server into
|
|
7
7
|
the AI clients on your machine (Claude Desktop, Claude Code, Cursor,
|
|
8
8
|
Windsurf, Zed) with one command.
|
|
9
9
|
- **Push test results** — `levr push` uploads results from any terminal or CI
|
|
10
10
|
pipeline.
|
|
11
|
+
- **Import test cases** — `levr import` brings existing cases in from CSV,
|
|
12
|
+
Excel, JSON, or Google Sheets.
|
|
11
13
|
|
|
12
14
|
## Install
|
|
13
15
|
|
|
@@ -129,6 +131,69 @@ the source to the workspace's default team, or pass `--team-id` to link it to a
|
|
|
129
131
|
specific team. Subsequent pushes with the same `--source` route to that team
|
|
130
132
|
automatically. Source names are normalized (lowercased, trimmed).
|
|
131
133
|
|
|
134
|
+
## Import test cases
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
levr import <file> --team-id <uuid> [options]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Brings existing test cases into Levr from CSV, Excel (`.xlsx`), JSON, or a
|
|
141
|
+
public Google Sheet. It runs in two phases: **preview** proposes a mapping from
|
|
142
|
+
your columns onto the Levr test-case schema (exact and fuzzy matching, with an
|
|
143
|
+
LLM assist for whatever is left over), you review and adjust it, then **commit**
|
|
144
|
+
writes the folders, tests, steps, and preconditions.
|
|
145
|
+
|
|
146
|
+
In a terminal, unmapped and low-confidence columns are walked with a picker. For
|
|
147
|
+
scripts and CI, pass `--yes` — and `--map` or `--mapping-file` to pin the
|
|
148
|
+
mapping so a replay can't drift.
|
|
149
|
+
|
|
150
|
+
**One column must map to `test_name`.** Interactive runs ask for it;
|
|
151
|
+
non-interactive runs exit 1 rather than import unnamed cases.
|
|
152
|
+
|
|
153
|
+
**Examples:**
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Interactive import from a CSV export
|
|
157
|
+
levr import ./testrail-export.csv --team-id <uuid>
|
|
158
|
+
|
|
159
|
+
# Excel, with one column pinned up front
|
|
160
|
+
levr import ./cases.xlsx --team-id <uuid> --map "Title=test_name"
|
|
161
|
+
|
|
162
|
+
# A public Google Sheet, no prompts
|
|
163
|
+
levr import --sheets-url "https://docs.google.com/spreadsheets/d/..." --team-id <uuid> --yes
|
|
164
|
+
|
|
165
|
+
# Save the confirmed mapping once, then replay it in CI
|
|
166
|
+
levr import ./cases.csv --team-id <uuid> --save-mapping mapping.json
|
|
167
|
+
levr import ./cases.csv --team-id <uuid> --mapping-file mapping.json --yes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Flags:**
|
|
171
|
+
|
|
172
|
+
| Flag | Alias | Description |
|
|
173
|
+
| ----------------------- | ----- | --------------------------------------------------------------------------------- |
|
|
174
|
+
| `--team-id <uuid>` | `-t` | Team the imported test cases belong to (required) |
|
|
175
|
+
| `--workspace-id <uuid>` | `-w` | Workspace ID (required for multi-workspace JWT auth) |
|
|
176
|
+
| `--sheets-url <url>` | | Public Google Sheets URL, used instead of a file argument |
|
|
177
|
+
| `--format <type>` | `-f` | Source format: `csv`, `xlsx`, `json` (auto-detected from the filename if omitted) |
|
|
178
|
+
| `--map <pair>` | `-m` | Column override, repeatable — three forms, below |
|
|
179
|
+
| `--mapping-file <path>` | | JSON file holding a saved confirmed mapping (from `--save-mapping`) |
|
|
180
|
+
| `--save-mapping <path>` | | Write the confirmed mapping to this JSON file for CI replay |
|
|
181
|
+
| `--yes` | `-y` | Accept the mapping without prompts (required for non-TTY runs) |
|
|
182
|
+
| `--verbose` | `-v` | Show detailed output |
|
|
183
|
+
|
|
184
|
+
`--map` accepts three forms:
|
|
185
|
+
|
|
186
|
+
| Form | Effect |
|
|
187
|
+
| ------------------------------- | ------------------------------------------------------------------------------ |
|
|
188
|
+
| `"Source Column=target_field"` | Map the column onto that Levr field |
|
|
189
|
+
| `"Source Column="` | Drop the column (empty target) |
|
|
190
|
+
| `"Source Column=labels:prefix"` | Import each value as the label `prefix:value`, keeping the column's provenance |
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Keep a TestRail "State" column as state:Draft / state:Approved labels
|
|
194
|
+
levr import ./cases.csv --team-id <uuid> --map "State=labels:state"
|
|
195
|
+
```
|
|
196
|
+
|
|
132
197
|
## CI/CD integration
|
|
133
198
|
|
|
134
199
|
### GitHub Actions
|
|
@@ -207,13 +272,39 @@ levr workspace current # show the active workspace
|
|
|
207
272
|
|
|
208
273
|
## Shell completion (optional)
|
|
209
274
|
|
|
210
|
-
|
|
275
|
+
`levr completion` prints a completion script to **stdout**. It never edits your
|
|
276
|
+
shell config — you choose where the script goes:
|
|
211
277
|
|
|
212
278
|
```bash
|
|
213
|
-
|
|
214
|
-
levr
|
|
279
|
+
# This session only
|
|
280
|
+
eval "$(levr completion bash)"
|
|
281
|
+
eval "$(levr completion zsh)"
|
|
282
|
+
|
|
283
|
+
# Persist it
|
|
284
|
+
levr completion bash >> ~/.bashrc
|
|
285
|
+
levr completion zsh >> ~/.zshrc
|
|
286
|
+
|
|
287
|
+
# Or system-wide (bash)
|
|
288
|
+
levr completion bash > /etc/bash_completion.d/levr
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**zsh:** load it _after_ `compinit`, which is what defines `compdef`:
|
|
292
|
+
|
|
293
|
+
```zsh
|
|
294
|
+
autoload -Uz compinit && compinit
|
|
295
|
+
eval "$(levr completion zsh)"
|
|
215
296
|
```
|
|
216
297
|
|
|
298
|
+
If `compinit` has not run, the script says so on stderr instead of failing with
|
|
299
|
+
a bare `command not found: compdef`.
|
|
300
|
+
|
|
301
|
+
Supported shells: **bash** and **zsh**. The shell is detected from `$SHELL` when
|
|
302
|
+
you omit the argument — pass it explicitly in scripts, or to generate a script
|
|
303
|
+
for a shell other than the one you are running. An unsupported or undetectable
|
|
304
|
+
shell writes an error to stderr and exits non-zero.
|
|
305
|
+
|
|
306
|
+
Nothing is installed automatically: the package ships no `postinstall` hook.
|
|
307
|
+
|
|
217
308
|
## Configuration
|
|
218
309
|
|
|
219
310
|
All configuration is via environment variables. Flags take precedence.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getApiUrl } from "./env-
|
|
1
|
+
import { getApiUrl } from "./env-CHeKHu5S.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { delimiter, dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
5
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
6
6
|
|
|
7
7
|
//#region ../mcp-harnesses/dist/catalog.js
|
|
@@ -356,17 +356,17 @@ function signalMatches(signal, env) {
|
|
|
356
356
|
return existsSync(signal.startsWith("~") ? expandTilde(signal, env.homedir) : signal);
|
|
357
357
|
}
|
|
358
358
|
/** Read a text file, or `null` if it doesn't exist / can't be read. */
|
|
359
|
-
function readTextOrNull(path
|
|
359
|
+
function readTextOrNull(path) {
|
|
360
360
|
try {
|
|
361
|
-
return readFileSync(path
|
|
361
|
+
return readFileSync(path, "utf8");
|
|
362
362
|
} catch {
|
|
363
363
|
return null;
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
/** Safe nested lookup over an unknown-typed parsed JSON value. */
|
|
367
|
-
function getAtPath(obj, path
|
|
367
|
+
function getAtPath(obj, path) {
|
|
368
368
|
let cur = obj;
|
|
369
|
-
for (const key of path
|
|
369
|
+
for (const key of path) {
|
|
370
370
|
if (cur === null || typeof cur !== "object") return void 0;
|
|
371
371
|
cur = cur[key];
|
|
372
372
|
}
|
|
@@ -430,8 +430,8 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
430
430
|
alreadyConfigured: false,
|
|
431
431
|
dryRun
|
|
432
432
|
};
|
|
433
|
-
const path
|
|
434
|
-
if (!path
|
|
433
|
+
const path = resolveConfigPath(harness, env);
|
|
434
|
+
if (!path) return {
|
|
435
435
|
ok: false,
|
|
436
436
|
wrote: false,
|
|
437
437
|
path: "",
|
|
@@ -440,7 +440,7 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
440
440
|
};
|
|
441
441
|
const entryValue = buildServerEntry(harness, mcpUrl)[SERVER_NAME];
|
|
442
442
|
const modPath = [harness.serverPropertyName, SERVER_NAME];
|
|
443
|
-
const existing = readTextOrNull(path
|
|
443
|
+
const existing = readTextOrNull(path);
|
|
444
444
|
const baseText = existing && existing.trim() ? existing : "{}";
|
|
445
445
|
const current = getAtPath(parse(baseText), modPath);
|
|
446
446
|
const alreadyConfigured = current !== void 0 && sameValue(current, entryValue);
|
|
@@ -448,7 +448,7 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
448
448
|
if (alreadyConfigured) return {
|
|
449
449
|
ok: true,
|
|
450
450
|
wrote: false,
|
|
451
|
-
path
|
|
451
|
+
path,
|
|
452
452
|
alreadyConfigured: true,
|
|
453
453
|
dryRun,
|
|
454
454
|
preview: nextText
|
|
@@ -456,18 +456,18 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
456
456
|
if (dryRun) return {
|
|
457
457
|
ok: true,
|
|
458
458
|
wrote: false,
|
|
459
|
-
path
|
|
459
|
+
path,
|
|
460
460
|
alreadyConfigured: false,
|
|
461
461
|
dryRun: true,
|
|
462
462
|
preview: nextText
|
|
463
463
|
};
|
|
464
|
-
mkdirSync(dirname(path
|
|
464
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
465
465
|
const finalText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
|
|
466
|
-
writeFileSync(path
|
|
466
|
+
writeFileSync(path, finalText, "utf8");
|
|
467
467
|
return {
|
|
468
468
|
ok: true,
|
|
469
469
|
wrote: true,
|
|
470
|
-
path
|
|
470
|
+
path,
|
|
471
471
|
alreadyConfigured: false,
|
|
472
472
|
dryRun: false,
|
|
473
473
|
preview: finalText
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { COMPLETION_SHELLS } from "./completionHandler-O2G_WZLf.js";
|
|
2
3
|
import { buildApplication, buildCommand, buildRouteMap, proposeCompletions, run, text_en } from "@stricli/core";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
4
|
import chalk from "chalk";
|
|
7
|
-
import { buildInstallCommand, buildUninstallCommand } from "@stricli/auto-complete";
|
|
8
5
|
|
|
9
6
|
//#region src/utils/logger.ts
|
|
10
7
|
var Logger = class {
|
|
@@ -41,9 +38,6 @@ var Logger = class {
|
|
|
41
38
|
function buildContext(process$1) {
|
|
42
39
|
return {
|
|
43
40
|
process: process$1,
|
|
44
|
-
os,
|
|
45
|
-
fs,
|
|
46
|
-
path,
|
|
47
41
|
logger: new Logger({
|
|
48
42
|
verbose: false,
|
|
49
43
|
stdout: process$1.stdout,
|
|
@@ -52,6 +46,45 @@ function buildContext(process$1) {
|
|
|
52
46
|
};
|
|
53
47
|
}
|
|
54
48
|
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/commands/completion.ts
|
|
51
|
+
const completionCommand = buildCommand({
|
|
52
|
+
docs: {
|
|
53
|
+
brief: "Print the shell completion script for bash or zsh",
|
|
54
|
+
fullDescription: `Prints a shell completion script to stdout. You decide where it goes —
|
|
55
|
+
this command never edits your shell config.
|
|
56
|
+
|
|
57
|
+
Load it for the current session:
|
|
58
|
+
eval "$(levr completion bash)"
|
|
59
|
+
eval "$(levr completion zsh)"
|
|
60
|
+
|
|
61
|
+
Or persist it:
|
|
62
|
+
levr completion bash >> ~/.bashrc
|
|
63
|
+
levr completion zsh >> ~/.zshrc
|
|
64
|
+
levr completion bash > /etc/bash_completion.d/levr # system-wide
|
|
65
|
+
|
|
66
|
+
The shell is detected from $SHELL when omitted; pass it explicitly in scripts
|
|
67
|
+
or when generating a script for a shell other than the one you are running.`
|
|
68
|
+
},
|
|
69
|
+
parameters: {
|
|
70
|
+
positional: {
|
|
71
|
+
kind: "tuple",
|
|
72
|
+
parameters: [{
|
|
73
|
+
parse: String,
|
|
74
|
+
proposeCompletions: (partial) => COMPLETION_SHELLS.filter((shell) => shell.startsWith(partial)),
|
|
75
|
+
brief: `Shell to emit (${COMPLETION_SHELLS.join("|")}); detected from $SHELL if omitted`,
|
|
76
|
+
placeholder: "shell",
|
|
77
|
+
optional: true
|
|
78
|
+
}]
|
|
79
|
+
},
|
|
80
|
+
flags: {}
|
|
81
|
+
},
|
|
82
|
+
loader: async () => {
|
|
83
|
+
const { completionHandler } = await import("./completionHandler-DtG_CsK0.js");
|
|
84
|
+
return completionHandler;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
55
88
|
//#endregion
|
|
56
89
|
//#region src/commands/mcp/add.ts
|
|
57
90
|
const mcpAddCommand = buildCommand({
|
|
@@ -109,7 +142,7 @@ Examples:
|
|
|
109
142
|
aliases: { y: "yes" }
|
|
110
143
|
},
|
|
111
144
|
loader: async () => {
|
|
112
|
-
const { mcpAddHandler } = await import("./addHandler-
|
|
145
|
+
const { mcpAddHandler } = await import("./addHandler-K2a4rVv8.js");
|
|
113
146
|
return mcpAddHandler;
|
|
114
147
|
}
|
|
115
148
|
});
|
|
@@ -147,7 +180,7 @@ Examples:
|
|
|
147
180
|
aliases: { d: "device-code" }
|
|
148
181
|
},
|
|
149
182
|
loader: async () => {
|
|
150
|
-
const { loginHandler } = await import("./loginHandler-
|
|
183
|
+
const { loginHandler } = await import("./loginHandler-BhvCWTd8.js");
|
|
151
184
|
return loginHandler;
|
|
152
185
|
}
|
|
153
186
|
});
|
|
@@ -166,7 +199,7 @@ Examples:
|
|
|
166
199
|
},
|
|
167
200
|
parameters: {},
|
|
168
201
|
loader: async () => {
|
|
169
|
-
const { logoutHandler } = await import("./logoutHandler-
|
|
202
|
+
const { logoutHandler } = await import("./logoutHandler-BFutKash.js");
|
|
170
203
|
return logoutHandler;
|
|
171
204
|
}
|
|
172
205
|
});
|
|
@@ -186,7 +219,7 @@ Examples:
|
|
|
186
219
|
},
|
|
187
220
|
parameters: {},
|
|
188
221
|
loader: async () => {
|
|
189
|
-
const { statusHandler } = await import("./statusHandler-
|
|
222
|
+
const { statusHandler } = await import("./statusHandler-B6XRPg4k.js");
|
|
190
223
|
return statusHandler;
|
|
191
224
|
}
|
|
192
225
|
});
|
|
@@ -297,11 +330,123 @@ Examples:
|
|
|
297
330
|
}
|
|
298
331
|
},
|
|
299
332
|
loader: async () => {
|
|
300
|
-
const { pushHandler } = await import("./pushHandler-
|
|
333
|
+
const { pushHandler } = await import("./pushHandler-v4-lHK6A.js");
|
|
301
334
|
return pushHandler;
|
|
302
335
|
}
|
|
303
336
|
});
|
|
304
337
|
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/commands/import.ts
|
|
340
|
+
const importCommand = buildCommand({
|
|
341
|
+
docs: {
|
|
342
|
+
brief: "Import test cases from CSV, Excel, JSON, or Google Sheets",
|
|
343
|
+
fullDescription: `Two-phase test-case import: preview proposes a column mapping to the
|
|
344
|
+
Levr test-case schema (exact/fuzzy matching with an LLM assist), you review
|
|
345
|
+
and adjust it, then commit writes folders, tests, steps, and preconditions.
|
|
346
|
+
|
|
347
|
+
Interactive mode (default in a terminal) walks unmapped and low-confidence
|
|
348
|
+
columns with a picker. For scripts/CI use --yes and/or --map.
|
|
349
|
+
|
|
350
|
+
Required-field rule: a column must map to test_name or the commit is
|
|
351
|
+
rejected — interactive mode will ask; non-interactive runs exit 1.
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
levr import ./testrail-export.csv --team-id <uuid>
|
|
355
|
+
levr import ./cases.xlsx --team-id <uuid> --map "Title=test_name"
|
|
356
|
+
levr import --sheets-url "https://docs.google.com/spreadsheets/d/..." --team-id <uuid> --yes
|
|
357
|
+
levr import ./cases.csv --team-id <uuid> --save-mapping mapping.json
|
|
358
|
+
levr import ./cases.csv --team-id <uuid> --mapping-file mapping.json --yes # CI replay`
|
|
359
|
+
},
|
|
360
|
+
parameters: {
|
|
361
|
+
positional: {
|
|
362
|
+
kind: "tuple",
|
|
363
|
+
parameters: [{
|
|
364
|
+
parse: String,
|
|
365
|
+
brief: "Path to the source file (.csv, .xlsx, .json)",
|
|
366
|
+
placeholder: "file",
|
|
367
|
+
optional: true
|
|
368
|
+
}]
|
|
369
|
+
},
|
|
370
|
+
flags: {
|
|
371
|
+
"workspace-id": {
|
|
372
|
+
kind: "parsed",
|
|
373
|
+
parse: String,
|
|
374
|
+
brief: "Workspace ID (required for multi-workspace JWT auth)",
|
|
375
|
+
placeholder: "uuid",
|
|
376
|
+
optional: true
|
|
377
|
+
},
|
|
378
|
+
"team-id": {
|
|
379
|
+
kind: "parsed",
|
|
380
|
+
parse: String,
|
|
381
|
+
brief: "Team the imported test cases will belong to",
|
|
382
|
+
placeholder: "uuid",
|
|
383
|
+
optional: false
|
|
384
|
+
},
|
|
385
|
+
"sheets-url": {
|
|
386
|
+
kind: "parsed",
|
|
387
|
+
parse: String,
|
|
388
|
+
brief: "Public Google Sheets URL (instead of a file)",
|
|
389
|
+
placeholder: "url",
|
|
390
|
+
optional: true
|
|
391
|
+
},
|
|
392
|
+
format: {
|
|
393
|
+
kind: "enum",
|
|
394
|
+
values: [
|
|
395
|
+
"csv",
|
|
396
|
+
"xlsx",
|
|
397
|
+
"json"
|
|
398
|
+
],
|
|
399
|
+
brief: "Source format (auto-detected from the filename if omitted)",
|
|
400
|
+
optional: true
|
|
401
|
+
},
|
|
402
|
+
map: {
|
|
403
|
+
kind: "parsed",
|
|
404
|
+
parse: String,
|
|
405
|
+
brief: "Column override (repeatable): \"Column=target_field\", \"Column=\" to drop, or \"Column=labels:prefix\"",
|
|
406
|
+
placeholder: "pair",
|
|
407
|
+
variadic: true,
|
|
408
|
+
optional: true
|
|
409
|
+
},
|
|
410
|
+
"mapping-file": {
|
|
411
|
+
kind: "parsed",
|
|
412
|
+
parse: String,
|
|
413
|
+
brief: "JSON file with a saved confirmed mapping (from --save-mapping)",
|
|
414
|
+
placeholder: "path",
|
|
415
|
+
optional: true
|
|
416
|
+
},
|
|
417
|
+
"save-mapping": {
|
|
418
|
+
kind: "parsed",
|
|
419
|
+
parse: String,
|
|
420
|
+
brief: "Write the confirmed mapping to this JSON file for CI replay",
|
|
421
|
+
placeholder: "path",
|
|
422
|
+
optional: true
|
|
423
|
+
},
|
|
424
|
+
yes: {
|
|
425
|
+
kind: "boolean",
|
|
426
|
+
default: false,
|
|
427
|
+
brief: "Accept the mapping without prompts (required for non-TTY runs)"
|
|
428
|
+
},
|
|
429
|
+
verbose: {
|
|
430
|
+
kind: "boolean",
|
|
431
|
+
default: false,
|
|
432
|
+
brief: "Show detailed output"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
aliases: {
|
|
436
|
+
w: "workspace-id",
|
|
437
|
+
t: "team-id",
|
|
438
|
+
f: "format",
|
|
439
|
+
m: "map",
|
|
440
|
+
y: "yes",
|
|
441
|
+
v: "verbose"
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
loader: async () => {
|
|
445
|
+
const { importHandler } = await import("./importHandler-D27Xrx73.js");
|
|
446
|
+
return importHandler;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
305
450
|
//#endregion
|
|
306
451
|
//#region src/commands/workspace/list.ts
|
|
307
452
|
const listCommand = buildCommand({
|
|
@@ -318,7 +463,7 @@ Examples:
|
|
|
318
463
|
},
|
|
319
464
|
parameters: {},
|
|
320
465
|
loader: async () => {
|
|
321
|
-
const { listHandler } = await import("./listHandler-
|
|
466
|
+
const { listHandler } = await import("./listHandler-8uqFClxw.js");
|
|
322
467
|
return listHandler;
|
|
323
468
|
}
|
|
324
469
|
});
|
|
@@ -351,7 +496,7 @@ Examples:
|
|
|
351
496
|
flags: {}
|
|
352
497
|
},
|
|
353
498
|
loader: async () => {
|
|
354
|
-
const { selectHandler } = await import("./selectHandler-
|
|
499
|
+
const { selectHandler } = await import("./selectHandler-DeEZe6PU.js");
|
|
355
500
|
return selectHandler;
|
|
356
501
|
}
|
|
357
502
|
});
|
|
@@ -368,14 +513,14 @@ Examples:
|
|
|
368
513
|
},
|
|
369
514
|
parameters: {},
|
|
370
515
|
loader: async () => {
|
|
371
|
-
const { currentHandler } = await import("./currentHandler-
|
|
516
|
+
const { currentHandler } = await import("./currentHandler-CHydiFui.js");
|
|
372
517
|
return currentHandler;
|
|
373
518
|
}
|
|
374
519
|
});
|
|
375
520
|
|
|
376
521
|
//#endregion
|
|
377
522
|
//#region package.json
|
|
378
|
-
var version = "0.
|
|
523
|
+
var version = "0.5.0";
|
|
379
524
|
|
|
380
525
|
//#endregion
|
|
381
526
|
//#region src/app.ts
|
|
@@ -404,16 +549,10 @@ const routes = buildRouteMap({
|
|
|
404
549
|
auth: authRoutes,
|
|
405
550
|
workspace: workspaceRoutes,
|
|
406
551
|
push: pushCommand,
|
|
407
|
-
|
|
408
|
-
|
|
552
|
+
import: importCommand,
|
|
553
|
+
completion: completionCommand
|
|
409
554
|
},
|
|
410
|
-
docs: {
|
|
411
|
-
brief: "The command-line interface for Levr",
|
|
412
|
-
hideRoute: {
|
|
413
|
-
install: true,
|
|
414
|
-
uninstall: true
|
|
415
|
-
}
|
|
416
|
-
}
|
|
555
|
+
docs: { brief: "The command-line interface for Levr" }
|
|
417
556
|
});
|
|
418
557
|
const app = buildApplication(routes, {
|
|
419
558
|
name: "levr",
|
|
@@ -432,10 +571,13 @@ const app = buildApplication(routes, {
|
|
|
432
571
|
//#region src/completion.ts
|
|
433
572
|
/**
|
|
434
573
|
* Compute shell tab-completion suggestions for the hidden `__complete` entrypoint
|
|
435
|
-
* (see src/bin/cli.ts).
|
|
436
|
-
* `levr __complete <
|
|
437
|
-
*
|
|
438
|
-
*
|
|
574
|
+
* (see src/bin/cli.ts). The scripts emitted by `levr completion bash|zsh`
|
|
575
|
+
* register a shell function that invokes `levr __complete <words…>` on each TAB,
|
|
576
|
+
* so `rawArgs` (process.argv.slice(2)) is
|
|
577
|
+
* `['__complete', <targetCommandName>, ...wordsBeingCompleted]` — note the
|
|
578
|
+
* command name is passed through and dropped here, which is why the shell
|
|
579
|
+
* scripts pass their FULL word array. A COMP_LINE ending in a space means the
|
|
580
|
+
* cursor is on a fresh (empty) word to complete.
|
|
439
581
|
*
|
|
440
582
|
* Never throws — completion must not surface an error to the user's shell.
|
|
441
583
|
*/
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/commands/completionHandler.ts
|
|
2
|
+
const COMPLETION_SHELLS = ["bash", "zsh"];
|
|
3
|
+
function isCompletionShell(value) {
|
|
4
|
+
return COMPLETION_SHELLS.includes(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort shell detection from `$SHELL`. Matches on the basename so login
|
|
8
|
+
* shells (`-bash`) and non-standard prefixes (`/usr/local/bin/zsh`) still
|
|
9
|
+
* resolve. Returns undefined when the shell is unset or unsupported — the
|
|
10
|
+
* caller must then fail loudly rather than guess.
|
|
11
|
+
*/
|
|
12
|
+
function detectShell(shellPath) {
|
|
13
|
+
const name = shellPath?.split("/").pop() ?? "";
|
|
14
|
+
if (name.includes("zsh")) return "zsh";
|
|
15
|
+
if (name.includes("bash")) return "bash";
|
|
16
|
+
}
|
|
17
|
+
const BASH_SCRIPT = `# levr bash completion.
|
|
18
|
+
# Load it from ~/.bashrc: eval "$(levr completion bash)"
|
|
19
|
+
__levr_complete() {
|
|
20
|
+
export COMP_LINE
|
|
21
|
+
local -a words=( "\${COMP_WORDS[@]}" )
|
|
22
|
+
# Drop a trailing empty word. COMP_LINE's trailing space is already the
|
|
23
|
+
# signal that the cursor is on a fresh word; passing the empty word too
|
|
24
|
+
# would make __complete see it twice and match nothing. Harmless when the
|
|
25
|
+
# last word is non-empty, so this is correct however bash populates
|
|
26
|
+
# COMP_WORDS.
|
|
27
|
+
if [[ \${#words[@]} -gt 0 && -z "\${words[\${#words[@]}-1]}" ]]; then
|
|
28
|
+
words=( "\${words[@]:0:\${#words[@]}-1}" )
|
|
29
|
+
fi
|
|
30
|
+
COMPREPLY=( $(levr __complete "\${words[@]}") )
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
complete -o default -o nospace -F __levr_complete levr
|
|
34
|
+
`;
|
|
35
|
+
const ZSH_SCRIPT = `# levr zsh completion.
|
|
36
|
+
# Load it from ~/.zshrc, AFTER your compinit call:
|
|
37
|
+
# autoload -Uz compinit && compinit
|
|
38
|
+
# eval "$(levr completion zsh)"
|
|
39
|
+
_levr_complete() {
|
|
40
|
+
local -a candidates args
|
|
41
|
+
local raw
|
|
42
|
+
local comp_line="\${(j: :)words}"
|
|
43
|
+
|
|
44
|
+
args=("\${(@)words}")
|
|
45
|
+
if [[ -z "\${args[-1]}" ]]; then
|
|
46
|
+
args=("\${(@)args[1,-2]}")
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Return before splitting when there is nothing to offer, so compadd is
|
|
50
|
+
# never reached with an empty candidate list.
|
|
51
|
+
raw="$(COMP_LINE="$comp_line" levr __complete "\${args[@]}" 2>/dev/null)"
|
|
52
|
+
[[ -n "$raw" ]] || return
|
|
53
|
+
|
|
54
|
+
candidates=(\${(f)raw})
|
|
55
|
+
# -S '' matches bash's \`complete -o nospace\`: no space is appended after a
|
|
56
|
+
# unique match, so subcommands can be chained without deleting one.
|
|
57
|
+
compadd -S '' -- "\${candidates[@]}"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# compdef only exists once compinit has run. Without this guard a .zshrc that
|
|
61
|
+
# eval's this before compinit dies with a bare "command not found: compdef" on
|
|
62
|
+
# every new shell; the explicit message says what to actually do.
|
|
63
|
+
if (( $+functions[compdef] )); then
|
|
64
|
+
compdef _levr_complete levr
|
|
65
|
+
else
|
|
66
|
+
print -u2 "levr: completion not registered — run \\\`autoload -Uz compinit && compinit\\\` before loading it."
|
|
67
|
+
fi
|
|
68
|
+
`;
|
|
69
|
+
function completionScript(shell) {
|
|
70
|
+
return shell === "zsh" ? ZSH_SCRIPT : BASH_SCRIPT;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Print the completion script for `shell` (or the detected shell) to stdout.
|
|
74
|
+
*
|
|
75
|
+
* stdout carries the script and NOTHING else, so `eval "$(levr completion zsh)"`
|
|
76
|
+
* is safe; diagnostics go to stderr and set a non-zero exit code. The command
|
|
77
|
+
* never writes to the filesystem — wiring it up is the user's call (ENG-2702).
|
|
78
|
+
*/
|
|
79
|
+
function completionHandler(_flags, shell) {
|
|
80
|
+
const requested = shell === void 0 ? detectShell(this.process.env["SHELL"]) : shell;
|
|
81
|
+
if (requested === void 0) {
|
|
82
|
+
this.process.stderr.write(`Could not detect your shell from \$SHELL. Pass one explicitly: levr completion <${COMPLETION_SHELLS.join("|")}>\n`);
|
|
83
|
+
this.process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!isCompletionShell(requested)) {
|
|
87
|
+
this.process.stderr.write(`Unsupported shell "${requested}". Supported shells: ${COMPLETION_SHELLS.join(", ")}.\n`);
|
|
88
|
+
this.process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.process.stdout.write(completionScript(requested));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
export { COMPLETION_SHELLS, completionHandler, completionScript, detectShell, isCompletionShell };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
5
|
//#region src/auth/credentials.ts
|
|
6
6
|
const CREDENTIALS_PATH = join(join(homedir(), ".config", "levr"), "credentials.json");
|