@odeva/cli 0.0.5 → 0.0.6

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.
@@ -9,6 +9,7 @@ import { withErrorHandling } from "../../lib/run.js";
9
9
  import { isValidSlug, slugify } from "../../lib/slug.js";
10
10
  import { listTemplates, renderTemplate } from "../../lib/templates.js";
11
11
  import { ui } from "../../lib/ui.js";
12
+ import { detectShell, isCompletionInstalled } from "../autocomplete.js";
12
13
  const DEFAULT_TEMPLATE = "hono-bun";
13
14
  class AppInit extends Command {
14
15
  static description = "Scaffold a new Odeva app";
@@ -68,17 +69,23 @@ class AppInit extends Command {
68
69
  }
69
70
  });
70
71
  spinner.stop(`Wrote ${filesWritten} file${filesWritten === 1 ? "" : "s"} from template '${template}'.`);
71
- p.outro(
72
- [
73
- ui.ok(`Created ${ui.bold(displayName)}`),
72
+ const outroLines = [
73
+ ui.ok(`Created ${ui.bold(displayName)}`),
74
+ "",
75
+ " Next steps:",
76
+ ` ${ui.code(`cd ${basename(directory)}`)}`,
77
+ ` ${ui.code("bun install")}`,
78
+ ` ${ui.code("odeva app config link")} ${ui.dim("# register on Odeva")}`,
79
+ ` ${ui.code("odeva app dev")} ${ui.dim("# start local dev with a tunnel")}`
80
+ ];
81
+ const shell = detectShell();
82
+ if (shell && !isCompletionInstalled(shell, "odeva")) {
83
+ outroLines.push(
74
84
  "",
75
- " Next steps:",
76
- ` ${ui.code(`cd ${basename(directory)}`)}`,
77
- ` ${ui.code("bun install")}`,
78
- ` ${ui.code("odeva app config link")} ${ui.dim("# register on Odeva")}`,
79
- ` ${ui.code("odeva app dev")} ${ui.dim("# start local dev with a tunnel")}`
80
- ].join("\n")
81
- );
85
+ ` ${ui.dim("Tip:")} ${ui.code("odeva autocomplete --install")} ${ui.dim("to enable tab-completion.")}`
86
+ );
87
+ }
88
+ p.outro(outroLines.join("\n"));
82
89
  });
83
90
  }
84
91
  async resolveDirectory(nameArg, skipPrompts) {
@@ -1,36 +1,90 @@
1
- import { Args, Command } from "@oclif/core";
1
+ import { Args, Command, Flags } from "@oclif/core";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join } from "node:path";
2
5
  import { CliError } from "../lib/errors.js";
3
6
  import { withErrorHandling } from "../lib/run.js";
7
+ import { ui } from "../lib/ui.js";
4
8
  const SUPPORTED_SHELLS = ["fish", "zsh"];
5
9
  class Autocomplete extends Command {
6
- static description = "Print a shell completion script for the odeva CLI";
10
+ static description = "Print or install a shell completion script for the odeva CLI";
7
11
  static examples = [
8
- "$ odeva autocomplete fish > ~/.config/fish/completions/odeva.fish",
9
- "$ odeva autocomplete zsh > ~/.odeva/_odeva # then source it from .zshrc"
12
+ "$ odeva autocomplete --install # detect shell and install",
13
+ "$ odeva autocomplete fish --install # explicit shell",
14
+ "$ odeva autocomplete fish > ~/.config/fish/completions/odeva.fish"
10
15
  ];
11
16
  static args = {
12
17
  shell: Args.string({
13
- description: "Target shell",
18
+ description: "Target shell (auto-detected from $SHELL when omitted with --install)",
14
19
  options: [...SUPPORTED_SHELLS],
15
- required: true
20
+ required: false
21
+ })
22
+ };
23
+ static flags = {
24
+ install: Flags.boolean({
25
+ description: "Write the completion script to the conventional path for the shell",
26
+ default: false
16
27
  })
17
28
  };
18
29
  async run() {
19
- const { args } = await this.parse(Autocomplete);
30
+ const { args, flags } = await this.parse(Autocomplete);
20
31
  await withErrorHandling(this, async () => {
21
- const shell = args.shell;
22
- if (!SUPPORTED_SHELLS.includes(shell)) {
23
- throw new CliError(`Unsupported shell '${shell}'.`, {
24
- hint: `Supported: ${SUPPORTED_SHELLS.join(", ")}.`
32
+ const bin = this.config.bin;
33
+ const shell = args.shell ?? (flags.install ? detectShell() : void 0);
34
+ if (!shell) {
35
+ if (flags.install) {
36
+ throw new CliError("Couldn't detect shell from $SHELL.", {
37
+ hint: `Pass it explicitly: \`${bin} autocomplete <${SUPPORTED_SHELLS.join("|")}> --install\`.`
38
+ });
39
+ }
40
+ throw new CliError("A shell is required.", {
41
+ hint: `Usage: \`${bin} autocomplete <${SUPPORTED_SHELLS.join("|")}>\`. Add --install to write the script to the conventional path.`
25
42
  });
26
43
  }
27
- const bin = this.config.bin;
28
44
  const tree = buildCommandTree(this.config.commands, bin);
29
45
  const script = shell === "fish" ? renderFish(bin, tree) : renderZsh(bin, tree);
30
- process.stdout.write(script);
46
+ if (!flags.install) {
47
+ process.stdout.write(script);
48
+ return;
49
+ }
50
+ const target = completionInstallPath(shell, bin);
51
+ mkdirSync(dirname(target), { recursive: true });
52
+ writeFileSync(target, script, { mode: 420 });
53
+ this.log(ui.ok(`Wrote ${shell} completion to ${ui.code(target)}`));
54
+ if (shell === "fish") {
55
+ this.log(ui.dim(" Open a new fish shell \u2014 completions auto-load from this path."));
56
+ } else {
57
+ const fpathDir = dirname(target);
58
+ this.log("");
59
+ this.log(" Add this to your ~/.zshrc (if you haven't already):");
60
+ this.log("");
61
+ this.log(ui.code(` fpath=(${fpathDir} $fpath)`));
62
+ this.log(ui.code(` autoload -U compinit && compinit`));
63
+ this.log("");
64
+ this.log(ui.dim(" Then open a new zsh shell."));
65
+ }
31
66
  });
32
67
  }
33
68
  }
69
+ function detectShell(env = process.env) {
70
+ const shell = env["SHELL"];
71
+ if (!shell) return null;
72
+ const name = basename(shell);
73
+ if (name === "fish" || name === "zsh") return name;
74
+ return null;
75
+ }
76
+ function completionInstallPath(shell, bin, env = process.env) {
77
+ const home = env["HOME"] || homedir();
78
+ if (shell === "fish") {
79
+ const config = env["XDG_CONFIG_HOME"] || join(home, ".config");
80
+ return join(config, "fish", "completions", `${bin}.fish`);
81
+ }
82
+ const data = env["XDG_DATA_HOME"] || join(home, ".local", "share");
83
+ return join(data, bin, "completions", `_${bin}`);
84
+ }
85
+ function isCompletionInstalled(shell, bin, env = process.env) {
86
+ return existsSync(completionInstallPath(shell, bin, env));
87
+ }
34
88
  function buildCommandTree(commands, binName) {
35
89
  const root = { name: "", description: "", children: /* @__PURE__ */ new Map() };
36
90
  for (const cmd of commands) {
@@ -152,7 +206,10 @@ _${bin} "$@"
152
206
  }
153
207
  export {
154
208
  buildCommandTree,
209
+ completionInstallPath,
155
210
  Autocomplete as default,
211
+ detectShell,
212
+ isCompletionInstalled,
156
213
  renderFish,
157
214
  renderZsh
158
215
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odeva/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Build apps on the Odeva booking platform — scaffold, develop, deploy.",
5
5
  "license": "MIT",
6
6
  "type": "module",