qwik 0.1.0__py3-none-any.whl

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.
qwik/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """A friendly CLI alias manager."""
2
+
3
+ __version__ = "0.1.0"
qwik/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running ``python -m qwik``."""
2
+
3
+ from qwik.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
qwik/cli.py ADDED
@@ -0,0 +1,142 @@
1
+ """Main Typer CLI application wiring all subcommands together."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from qwik import __version__
9
+ from qwik.commands.add import add_command
10
+ from qwik.commands.doctor import doctor_command
11
+ from qwik.commands.edit import edit_command
12
+ from qwik.commands.enable_disable import disable_command, enable_command
13
+ from qwik.commands.exporter import export_command
14
+ from qwik.commands.importer import import_command
15
+ from qwik.commands.init_shell import init_shell_command
16
+ from qwik.commands.list import list_command
17
+ from qwik.commands.pick import pick_command
18
+ from qwik.commands.remove import remove_command
19
+ from qwik.commands.rename import rename_command
20
+ from qwik.commands.run import run_command
21
+ from qwik.commands.search import search_command
22
+ from qwik.commands.show import show_command
23
+ from qwik.commands.tag import tag_command, untag_command
24
+
25
+ __all__ = ["app"]
26
+
27
+
28
+ def _version_callback(value: bool) -> None:
29
+ """Print version and exit."""
30
+ if value:
31
+ Console().print(f"qwik {__version__}")
32
+ raise typer.Exit()
33
+
34
+
35
+ app = typer.Typer(
36
+ name="qwik",
37
+ help="A friendly CLI alias manager.",
38
+ no_args_is_help=False,
39
+ add_completion=True,
40
+ )
41
+
42
+ # Register subcommands
43
+ app.command("add")(add_command)
44
+ app.command("list")(list_command)
45
+ app.command("show")(show_command)
46
+ app.command("edit")(edit_command)
47
+ app.command("rename")(rename_command)
48
+ app.command("rm")(remove_command)
49
+ app.command("enable")(enable_command)
50
+ app.command("disable")(disable_command)
51
+ app.command("run")(run_command)
52
+ app.command("search")(search_command)
53
+ app.command("pick")(pick_command)
54
+ app.command("tag")(tag_command)
55
+ app.command("untag")(untag_command)
56
+ app.command("export")(export_command)
57
+ app.command("import")(import_command)
58
+ app.command("init")(init_shell_command)
59
+ app.command("doctor")(doctor_command)
60
+
61
+
62
+ @app.callback(invoke_without_command=True)
63
+ def main(
64
+ ctx: typer.Context,
65
+ run_alias: str | None = typer.Option(
66
+ None,
67
+ "--run",
68
+ "-r",
69
+ help="Run an alias (qwik -r <name> [args...]).",
70
+ ),
71
+ list_flag: bool = typer.Option(False, "--list", "-l", help="Quick list."),
72
+ search_query: str | None = typer.Option(
73
+ None,
74
+ "--search",
75
+ "-s",
76
+ help="Quick search.",
77
+ ),
78
+ version: bool = typer.Option(
79
+ False,
80
+ "--version",
81
+ "-v",
82
+ help="Show version and exit.",
83
+ callback=_version_callback,
84
+ is_eager=True,
85
+ ),
86
+ help_flag: bool = typer.Option(
87
+ False,
88
+ "--help",
89
+ "-h",
90
+ help="Show this message and exit.",
91
+ is_eager=True,
92
+ ),
93
+ ) -> None:
94
+ """Top-level callback implementing shortcut flags and bare invocation.
95
+
96
+ When invoked with no subcommand and no flags, the fuzzy picker opens.
97
+ """
98
+ # If a subcommand is already being handled, do nothing.
99
+ if ctx.invoked_subcommand is not None:
100
+ return
101
+
102
+ if help_flag:
103
+ Console().print(ctx.get_help())
104
+ raise typer.Exit()
105
+
106
+ if list_flag:
107
+ list_command()
108
+ raise typer.Exit()
109
+
110
+ if search_query is not None:
111
+ search_command(search_query)
112
+ raise typer.Exit()
113
+
114
+ if run_alias is not None:
115
+ # Need to capture remaining args after -r ... tricky in Typer callback.
116
+ # We use sys.argv but only look at the FIRST occurrence (index 0/1 are
117
+ # the Python/qwik executable), so alias names containing "-r" are safe
118
+ # unless they appear before the flag.
119
+ import sys
120
+
121
+ # Find first occurrence in argv (after the program name)
122
+ idx = None
123
+ for i, arg in enumerate(sys.argv[1:], start=1):
124
+ if arg == "-r" or arg == "--run":
125
+ idx = i
126
+ break
127
+ if idx is None:
128
+ Console().print(
129
+ "[qwik.error]Could not locate -r in arguments.[/qwik.error]"
130
+ )
131
+ raise typer.Exit(1)
132
+ remainder = sys.argv[idx + 1 :]
133
+ if not remainder:
134
+ Console().print("[qwik.error]Usage: qwik -r <name> [args...][/qwik.error]")
135
+ raise typer.Exit(1)
136
+ name = remainder[0]
137
+ args = remainder[1:]
138
+ run_command(name, args)
139
+ raise typer.Exit()
140
+
141
+ # Bare invocation → fuzzy picker
142
+ pick_command()
qwik/commands/add.py ADDED
@@ -0,0 +1,110 @@
1
+ """``qwik add`` — create a new alias."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from qwik.core.conflicts import ConflictChecker
12
+ from qwik.core.models import Alias
13
+ from qwik.core.store import get_store
14
+ from qwik.ui.prompts import (
15
+ print_error,
16
+ print_info,
17
+ print_success,
18
+ print_warning,
19
+ prompt_confirm,
20
+ prompt_text,
21
+ )
22
+
23
+ __all__ = ["add_command"]
24
+
25
+
26
+ def add_command(
27
+ name: Optional[str] = typer.Argument(None, help="Alias name."),
28
+ command: list[str] = typer.Argument(None, help="Command the alias expands to."),
29
+ tag: Optional[str] = typer.Option(
30
+ None, "--tag", "-t", help="Comma-separated tags."
31
+ ),
32
+ description: Optional[str] = typer.Option(
33
+ None, "--description", "-d", help="Optional description."
34
+ ),
35
+ force: bool = typer.Option(
36
+ False, "--force", "-f", help="Overwrite if alias already exists."
37
+ ),
38
+ global_install: bool = typer.Option(
39
+ False, "--global", "-g", help="(Reserved) install for all users."
40
+ ),
41
+ ) -> None:
42
+ """Create a new alias.
43
+
44
+ When *name* or *command* are omitted, the command runs interactively.
45
+ """
46
+ del global_install # reserved for future use
47
+ store = get_store()
48
+ store_data = store.load()
49
+ console = Console()
50
+
51
+ # Interactive mode if name missing
52
+ if name is None:
53
+ name = prompt_text("? Alias name", console=console, allow_empty=False)
54
+ if not command:
55
+ cmd_input = prompt_text("? Command", console=console, allow_empty=False)
56
+ command = [cmd_input]
57
+
58
+ full_command = " ".join(command)
59
+
60
+ # Conflict checks
61
+ checker = ConflictChecker(store_data)
62
+ result = checker.check(name)
63
+
64
+ if not result.valid_syntax:
65
+ print_error(
66
+ result.name, suggestion="Names must match ^[A-Za-z_][A-Za-z0-9_-]*$"
67
+ )
68
+ raise typer.Exit(1)
69
+
70
+ if result.existing_alias and not force:
71
+ print_error(
72
+ f'Alias "{name}" already exists.',
73
+ suggestion=f"qwik edit {name} to change the command\n"
74
+ f"qwik rm {name} to remove it\n"
75
+ f"--force to overwrite",
76
+ )
77
+ raise typer.Exit(1)
78
+
79
+ if result.is_builtin and not force:
80
+ print_error(
81
+ f'"{name}" is a shell builtin. Shadowing it can break your shell.',
82
+ suggestion="Use --force only if you know what you're doing.",
83
+ )
84
+ raise typer.Exit(1)
85
+
86
+ if result.needs_warning:
87
+ print_warning(
88
+ f'"{name}" shadows {result.path_location}.',
89
+ console=console,
90
+ )
91
+ if not prompt_confirm("Continue?", default=False, console=console):
92
+ raise typer.Exit(0)
93
+
94
+ alias = Alias(
95
+ command=full_command,
96
+ tag=tag or "",
97
+ description=description or "",
98
+ created_at=datetime.now(timezone.utc),
99
+ updated_at=datetime.now(timezone.utc),
100
+ )
101
+
102
+ try:
103
+ store_data.add(name, alias, force=force)
104
+ except KeyError as exc:
105
+ print_error(str(exc))
106
+ raise typer.Exit(1)
107
+
108
+ store.save_with_backup(store_data)
109
+ print_success(f'Added "{name}" → {full_command!r}', console=console)
110
+ print_info("Run `source <rc>` or open a new terminal to use it.", console=console)
@@ -0,0 +1,153 @@
1
+ """``qwik doctor`` — diagnose environment and store health."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from qwik.core.store import get_store
13
+ from qwik.shells.base import SUPPORTED_SHELLS
14
+ from qwik.ui.prompts import print_error, print_success, print_warning
15
+
16
+ __all__ = ["doctor_command"]
17
+
18
+
19
+ def doctor_command() -> None:
20
+ """Diagnose shell, hook status, store readability, and conflicts."""
21
+ console = Console()
22
+ store = get_store()
23
+
24
+ checks_ok = 0
25
+ checks_warn = 0
26
+ checks_err = 0
27
+
28
+ # Detect shell
29
+ shell = _detect_shell()
30
+ console.print(f"[bold]Shell detected:[/bold] {shell or 'unknown'}")
31
+ if shell in SUPPORTED_SHELLS:
32
+ print_success(f"{shell} is supported.", console=console)
33
+ checks_ok += 1
34
+ elif shell:
35
+ print_warning(f"{shell} support is best-effort.", console=console)
36
+ checks_warn += 1
37
+
38
+ # Hook installed?
39
+ hook_installed = _hook_installed(shell)
40
+ if hook_installed:
41
+ print_success("Shell hook appears installed.", console=console)
42
+ checks_ok += 1
43
+ else:
44
+ print_warning(
45
+ "Shell hook not detected. Run `qwik init <shell> --install`.",
46
+ console=console,
47
+ )
48
+ checks_warn += 1
49
+
50
+ # Store readable?
51
+ data = None
52
+ try:
53
+ data = store.load()
54
+ print_success(f"Store readable ({len(data.aliases)} aliases).", console=console)
55
+ checks_ok += 1
56
+ except Exception as exc:
57
+ print_error(f"Store unreadable: {exc}", console=console)
58
+ checks_err += 1
59
+
60
+ # Conflicts with system commands
61
+ if data is not None:
62
+ if data.aliases:
63
+ conflicts = [n for n in data.aliases if shutil.which(n)]
64
+ if conflicts:
65
+ print_warning(
66
+ f"Aliases shadowing PATH binaries: {', '.join(conflicts)}",
67
+ console=console,
68
+ )
69
+ checks_warn += 1
70
+ else:
71
+ print_success("No alias shadows a system binary.", console=console)
72
+ checks_ok += 1
73
+ else:
74
+ print_success("No aliases — nothing to shadow.", console=console)
75
+ checks_ok += 1
76
+
77
+ # Summary
78
+ console.print()
79
+ total = checks_ok + checks_warn + checks_err
80
+ console.print(
81
+ f"[bold]Summary:[/bold] {checks_ok}/{total} passed, "
82
+ f"{checks_warn} warning(s), {checks_err} error(s)"
83
+ )
84
+ if checks_err:
85
+ raise typer.Exit(1)
86
+
87
+
88
+ def _detect_shell() -> str | None:
89
+ """Attempt to detect the current user's shell.
90
+
91
+ Returns:
92
+ A lowercase shell name (e.g. ``bash``, ``zsh``), or ``None``.
93
+ """
94
+ # Prefer the $SHELL environment variable.
95
+ shell_env = os.environ.get("SHELL", "").lower()
96
+ if "bash" in shell_env:
97
+ return "bash"
98
+ if "zsh" in shell_env:
99
+ return "zsh"
100
+ if "fish" in shell_env:
101
+ return "fish"
102
+ if "pwsh" in shell_env or "powershell" in shell_env:
103
+ return "pwsh"
104
+
105
+ # Fallback: inspect parent process via /proc on Linux.
106
+ try:
107
+ with Path("/proc/self/status").open(encoding="utf-8") as f:
108
+ for line in f:
109
+ if line.startswith("PPid:"):
110
+ ppid = line.split()[1]
111
+ exe_link = Path(f"/proc/{ppid}/exe")
112
+ if exe_link.exists():
113
+ name = exe_link.resolve().name.lower()
114
+ if "bash" in name:
115
+ return "bash"
116
+ if "zsh" in name:
117
+ return "zsh"
118
+ if "fish" in name:
119
+ return "fish"
120
+ if "pwsh" in name or "powershell" in name:
121
+ return "pwsh"
122
+ except Exception:
123
+ pass
124
+ return None
125
+
126
+
127
+ def _hook_installed(shell: str | None) -> bool:
128
+ """Crude heuristic: grep rc file for 'qwik init'.
129
+
130
+ Args:
131
+ shell: Detected shell name.
132
+
133
+ Returns:
134
+ ``True`` if the rc file contains an ``qwik init`` invocation.
135
+ """
136
+ if shell is None:
137
+ return False
138
+ rc_map = {
139
+ "bash": Path.home() / ".bashrc",
140
+ "zsh": Path.home() / ".zshrc",
141
+ "fish": Path.home() / ".config" / "fish" / "config.fish",
142
+ "pwsh": Path.home()
143
+ / ".config"
144
+ / "powershell"
145
+ / "Microsoft.PowerShell_profile.ps1",
146
+ }
147
+ rc = rc_map.get(shell)
148
+ if rc is None or not rc.exists():
149
+ return False
150
+ try:
151
+ return "qwik init" in rc.read_text(encoding="utf-8")
152
+ except Exception:
153
+ return False
qwik/commands/edit.py ADDED
@@ -0,0 +1,110 @@
1
+ """``qwik edit`` — open alias in ``$EDITOR``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from qwik.core.models import Alias
15
+ from qwik.core.store import get_store
16
+ from qwik.ui.prompts import print_error, print_success
17
+
18
+ __all__ = ["edit_command"]
19
+
20
+
21
+ def edit_command(
22
+ name: str = typer.Argument(..., help="Alias name to edit."),
23
+ ) -> None:
24
+ """Open the alias entry in ``$EDITOR`` as an editable TOML snippet."""
25
+ store = get_store()
26
+ data = store.load()
27
+ console = Console()
28
+
29
+ alias = data.get(name)
30
+ if alias is None:
31
+ print_error(f'Alias "{name}" does not exist.', console=console)
32
+ raise typer.Exit(1)
33
+
34
+ editor = os.environ.get("EDITOR", "vi")
35
+
36
+ snippet = (
37
+ f'# Edit the fields below and save/quit to apply changes to "{name}"\n'
38
+ f"command = {alias.command!r}\n"
39
+ f"tag = {alias.tag!r}\n"
40
+ f"description = {alias.description!r}\n"
41
+ f"enabled = {alias.enabled!r}\n"
42
+ )
43
+
44
+ with tempfile.NamedTemporaryFile(
45
+ mode="w+", suffix=".toml", delete=False, encoding="utf-8"
46
+ ) as tmp:
47
+ tmp.write(snippet)
48
+ tmp_path = Path(tmp.name)
49
+
50
+ try:
51
+ subprocess.run([editor, str(tmp_path)], check=True)
52
+ edited_text = tmp_path.read_text(encoding="utf-8")
53
+ # Very simple parser: extract key = value lines
54
+ new_fields: dict[str, object] = {}
55
+ for line in edited_text.splitlines():
56
+ line = line.strip()
57
+ if not line or line.startswith("#"):
58
+ continue
59
+ # Strip inline comments (first unquoted #)
60
+ comment_idx = -1
61
+ in_quote: str | None = None
62
+ for i, ch in enumerate(line):
63
+ if ch in ('"', "'") and (i == 0 or line[i - 1] != "\\"):
64
+ if in_quote == ch:
65
+ in_quote = None
66
+ elif in_quote is None:
67
+ in_quote = ch
68
+ elif ch == "#" and in_quote is None:
69
+ comment_idx = i
70
+ break
71
+ if comment_idx >= 0:
72
+ line = line[:comment_idx].rstrip()
73
+ if " = " not in line:
74
+ continue
75
+ key, raw_val = line.split(" = ", 1)
76
+ key = key.strip()
77
+ raw_val = raw_val.strip()
78
+ low = raw_val.lower()
79
+ if low == "true":
80
+ new_fields[key] = True
81
+ elif low == "false":
82
+ new_fields[key] = False
83
+ elif raw_val.startswith("[") and raw_val.endswith("]"):
84
+ # list of strings
85
+ inner = raw_val[1:-1]
86
+ new_fields[key] = [
87
+ v.strip().strip("'\"").strip()
88
+ for v in inner.split(",")
89
+ if v.strip()
90
+ ]
91
+ else:
92
+ new_fields[key] = raw_val.strip("'\"").strip()
93
+
94
+ data.aliases[name] = Alias(
95
+ command=str(new_fields.get("command", alias.command)),
96
+ tag=new_fields.get("tag", alias.tag) or [],
97
+ description=str(new_fields.get("description", alias.description)),
98
+ enabled=new_fields.get("enabled", alias.enabled),
99
+ created_at=alias.created_at,
100
+ updated_at=datetime.now(timezone.utc),
101
+ last_used=alias.last_used,
102
+ run_count=alias.run_count,
103
+ )
104
+ store.save_with_backup(data)
105
+ print_success(f'Updated "{name}".', console=console)
106
+ except subprocess.CalledProcessError as exc:
107
+ print_error(f"Editor exited with code {exc.returncode}.", console=console)
108
+ raise typer.Exit(1)
109
+ finally:
110
+ tmp_path.unlink(missing_ok=True)
@@ -0,0 +1,50 @@
1
+ """``qwik enable`` / ``qwik disable`` — toggle alias state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from qwik.core.store import get_store
11
+ from qwik.ui.prompts import print_error, print_success
12
+
13
+ __all__ = ["enable_command", "disable_command"]
14
+
15
+
16
+ def _toggle(name: str, enabled: bool) -> None:
17
+ """Set the enabled flag for *name* and persist.
18
+
19
+ Args:
20
+ name: Alias identifier.
21
+ enabled: Desired state.
22
+ """
23
+ store = get_store()
24
+ data = store.load()
25
+ console = Console()
26
+
27
+ alias = data.get(name)
28
+ if alias is None:
29
+ print_error(f'Alias "{name}" does not exist.', console=console)
30
+ raise typer.Exit(1)
31
+
32
+ alias.enabled = enabled
33
+ alias.updated_at = datetime.now(timezone.utc)
34
+ store.save_with_backup(data)
35
+ state = "enabled" if enabled else "disabled"
36
+ print_success(f'"{name}" is now {state}.', console=console)
37
+
38
+
39
+ def enable_command(
40
+ name: str = typer.Argument(..., help="Alias name to enable."),
41
+ ) -> None:
42
+ """Re-enable a previously disabled alias."""
43
+ _toggle(name, True)
44
+
45
+
46
+ def disable_command(
47
+ name: str = typer.Argument(..., help="Alias name to disable."),
48
+ ) -> None:
49
+ """Temporarily disable an alias without deleting it."""
50
+ _toggle(name, False)
@@ -0,0 +1,53 @@
1
+ """``qwik export`` — write aliases to TOML/JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from qwik.core.store import get_store
12
+ from qwik.ui.prompts import print_error, print_success
13
+
14
+ __all__ = ["export_command"]
15
+
16
+
17
+ def export_command(
18
+ path: Path = typer.Argument(..., help="Destination file path."),
19
+ format: Optional[str] = typer.Option(
20
+ None,
21
+ "--format",
22
+ "-f",
23
+ help="toml or json (inferred from extension if omitted).",
24
+ ),
25
+ ) -> None:
26
+ """Export aliases to a file."""
27
+ store = get_store()
28
+ data = store.load()
29
+ console = Console()
30
+
31
+ if not data.aliases:
32
+ print_error("No aliases to export.", console=console)
33
+ raise typer.Exit(1)
34
+
35
+ fmt = (format or path.suffix.lstrip(".")).lower()
36
+ if fmt not in ("toml", "json"):
37
+ print_error(f"Unknown format '{fmt}'. Use toml or json.", console=console)
38
+ raise typer.Exit(1)
39
+
40
+ if fmt == "toml":
41
+ import tomlkit
42
+
43
+ doc = store._store_to_document(data)
44
+ path.write_text(tomlkit.dumps(doc), encoding="utf-8")
45
+ else:
46
+ import json
47
+
48
+ path.write_text(
49
+ json.dumps(data.model_dump(mode="json"), indent=2),
50
+ encoding="utf-8",
51
+ )
52
+
53
+ print_success(f"Exported {len(data.aliases)} aliases to {path}", console=console)