@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 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 two jobs:
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
- Tab-completion is an explicit opt-in step (it is not installed automatically):
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
- levr install # add shell completion for the current shell
214
- levr uninstall # remove it
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-NxtzJJPk.js";
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$1) {
359
+ function readTextOrNull(path) {
360
360
  try {
361
- return readFileSync(path$1, "utf8");
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$1) {
367
+ function getAtPath(obj, path) {
368
368
  let cur = obj;
369
- for (const key of path$1) {
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$1 = resolveConfigPath(harness, env);
434
- if (!path$1) return {
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$1);
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: path$1,
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: path$1,
459
+ path,
460
460
  alreadyConfigured: false,
461
461
  dryRun: true,
462
462
  preview: nextText
463
463
  };
464
- mkdirSync(dirname(path$1), { recursive: true });
464
+ mkdirSync(dirname(path), { recursive: true });
465
465
  const finalText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
466
- writeFileSync(path$1, finalText, "utf8");
466
+ writeFileSync(path, finalText, "utf8");
467
467
  return {
468
468
  ok: true,
469
469
  wrote: true,
470
- path: path$1,
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-boWTd9yY.js");
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-Dfg89AD2.js");
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-BC8laIAB.js");
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-DljfUcC2.js");
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-CC709aNJ.js");
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-7DZhE8jn.js");
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-nOUsPW9p.js");
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-Bm7GFMkj.js");
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.3.1";
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
- install: buildInstallCommand("levr", { bash: "levr __complete" }),
408
- uninstall: buildUninstallCommand("levr", { bash: true })
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). `levr install` registers a bash function that invokes
436
- * `levr __complete <COMP_LINE>` on each TAB, so `rawArgs` (process.argv.slice(2))
437
- * is `['__complete', <targetCommandName>, ...wordsBeingCompleted]`. A COMP_LINE
438
- * ending in a space means the cursor is on a fresh (empty) word to complete.
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,3 @@
1
+ import { COMPLETION_SHELLS, completionHandler, completionScript, detectShell, isCompletionShell } from "./completionHandler-O2G_WZLf.js";
2
+
3
+ export { completionHandler };
@@ -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,4 +1,4 @@
1
- import { loadWorkspace } from "./workspace-store-4hfvsEHS.js";
1
+ import { loadWorkspace } from "./workspace-store-DDOxnut1.js";
2
2
 
3
3
  //#region src/commands/workspace/currentHandler.ts
4
4
  function currentHandler() {
@@ -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");