diff --git a/commands/all.go b/commands/all.go new file mode 100644 index 0000000..d7c4c5f --- /dev/null +++ b/commands/all.go @@ -0,0 +1,18 @@ +package commands + +import ( + _ "github.com/versenilvis/iris/commands/cc" + _ "github.com/versenilvis/iris/commands/fs" + _ "github.com/versenilvis/iris/commands/git" + _ "github.com/versenilvis/iris/commands/golang" + _ "github.com/versenilvis/iris/commands/js" + _ "github.com/versenilvis/iris/commands/jvm" + _ "github.com/versenilvis/iris/commands/ops" + _ "github.com/versenilvis/iris/commands/pkginstaller" + _ "github.com/versenilvis/iris/commands/python" + _ "github.com/versenilvis/iris/commands/runner" + _ "github.com/versenilvis/iris/commands/rust" + _ "github.com/versenilvis/iris/commands/sys" + _ "github.com/versenilvis/iris/commands/text" + _ "github.com/versenilvis/iris/commands/view" +) diff --git a/commands/cc/cc.go b/commands/cc/cc.go new file mode 100644 index 0000000..70a4beb --- /dev/null +++ b/commands/cc/cc.go @@ -0,0 +1,79 @@ +package cc + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + cOptions := []core.Option{ + {Name: "-o", Description: "output file"}, + {Name: "-c", Description: "compile only (no link)"}, + {Name: "-g", Description: "debug symbols"}, + {Name: "-O0", Description: "no optimization"}, + {Name: "-O1", Description: "basic optimization"}, + {Name: "-O2", Description: "standard optimization"}, + {Name: "-O3", Description: "aggressive optimization"}, + {Name: "-Os", Description: "optimize for size"}, + {Name: "-Wall", Description: "enable common warnings"}, + {Name: "-Wextra", Description: "extra warnings"}, + {Name: "-Werror", Description: "treat warnings as errors"}, + {Name: "-std", Description: "language standard (e.g. c11, c++17)"}, + {Name: "-I", Description: "include directory"}, + {Name: "-L", Description: "library directory"}, + {Name: "-l", Description: "link library"}, + {Name: "-D", Description: "define macro"}, + {Name: "-shared", Description: "build shared library"}, + {Name: "-static", Description: "static linking"}, + {Name: "-fPIC", Description: "position independent code"}, + {Name: "-march", Description: "target architecture"}, + {Name: "-pthread", Description: "POSIX threads"}, + {Name: "-v", Description: "verbose"}, + {Name: "-E", Description: "preprocess only"}, + {Name: "-S", Description: "compile to assembly"}, + {Name: "-M", Description: "output dependencies"}, + {Name: "-MMD", Description: "write dep file"}, + {Name: "--version", Description: "print version"}, + } + + core.Register(&core.Spec{ + Name: "gcc", + Description: "GNU C compiler", + Generator: core.FileGenerator(".c", ".h"), + Options: cOptions, + }) + + core.Register(&core.Spec{ + Name: "g++", + Description: "GNU C++ compiler", + Generator: core.FileGenerator(".cpp", ".cc", ".cxx", ".h", ".hpp"), + Options: cOptions, + }) + + core.Register(&core.Spec{ + Name: "cc", + Description: "C compiler (alias)", + Generator: core.FileGenerator(".c", ".h"), + Options: cOptions, + }) + + core.Register(&core.Spec{ + Name: "c++", + Description: "C++ compiler (alias)", + Generator: core.FileGenerator(".cpp", ".cc", ".cxx", ".h", ".hpp"), + Options: cOptions, + }) + + core.Register(&core.Spec{ + Name: "clang", + Description: "LLVM C compiler", + Generator: core.FileGenerator(".c", ".h"), + Options: cOptions, + }) + + core.Register(&core.Spec{ + Name: "clang++", + Description: "LLVM C++ compiler", + Generator: core.FileGenerator(".cpp", ".cc", ".cxx", ".h", ".hpp"), + Options: cOptions, + }) +} diff --git a/commands/dev/docker.go b/commands/dev/docker.go deleted file mode 100644 index b1cf7ea..0000000 --- a/commands/dev/docker.go +++ /dev/null @@ -1,119 +0,0 @@ -package dev - -import ( - "github.com/versenilvis/iris/commands/core" -) - -func init() { - core.Register(&core.Spec{ - Name: "docker", - Description: "container engine", - Subcommands: []core.Subcommand{ - { - Name: "ps", - Description: "list containers", - Options: []core.Option{ - {Name: "-a", Description: "show all"}, - {Name: "-q", Description: "only show IDs"}, - }, - }, - { - Name: "build", - Description: "build image", - Options: []core.Option{ - {Name: "-t", Description: "tag name"}, - {Name: "-f", Description: "dockerfile path"}, - {Name: "--no-cache", Description: "no build cache"}, - }, - }, - { - Name: "run", - Description: "run container", - Options: []core.Option{ - {Name: "-d", Description: "detached mode"}, - {Name: "-p", Description: "port mapping"}, - {Name: "-v", Description: "volume mount"}, - {Name: "--rm", Description: "auto remove"}, - {Name: "--name", Description: "container name"}, - {Name: "-it", Description: "interactive tty"}, - {Name: "-e", Description: "set env variable"}, - }, - }, - { - Name: "pull", - Description: "pull image", - }, - { - Name: "push", - Description: "push image", - }, - { - Name: "exec", - Description: "exec in container", - Options: []core.Option{ - {Name: "-it", Description: "interactive tty"}, - }, - }, - { - Name: "stop", - Description: "stop container", - }, - { - Name: "rm", - Description: "remove container", - Options: []core.Option{ - {Name: "-f", Description: "force remove"}, - }, - }, - { - Name: "rmi", - Description: "remove image", - }, - { - Name: "images", - Description: "list images", - }, - { - Name: "logs", - Description: "view logs", - Options: []core.Option{ - {Name: "-f", Description: "follow output"}, - {Name: "--tail", Description: "last n lines"}, - }, - }, - { - Name: "compose", - Description: "multi-container", - Subcommands: []core.Subcommand{ - {Name: "up", Description: "start services"}, - {Name: "down", Description: "stop services"}, - {Name: "build", Description: "build services"}, - {Name: "logs", Description: "view logs"}, - {Name: "ps", Description: "list services"}, - {Name: "exec", Description: "execute command"}, - {Name: "restart", Description: "restart services"}, - }, - }, - { - Name: "network", - Description: "manage networks", - Subcommands: []core.Subcommand{ - {Name: "ls", Description: "list networks"}, - {Name: "create", Description: "create network"}, - {Name: "rm", Description: "remove network"}, - {Name: "inspect", Description: "show details"}, - }, - }, - { - Name: "volume", - Description: "manage volumes", - Subcommands: []core.Subcommand{ - {Name: "ls", Description: "list volumes"}, - {Name: "create", Description: "create volume"}, - {Name: "rm", Description: "remove volume"}, - {Name: "prune", Description: "remove unused"}, - }, - }, - }, - }) -} diff --git a/commands/dev/npm.go b/commands/dev/npm.go deleted file mode 100644 index db603bd..0000000 --- a/commands/dev/npm.go +++ /dev/null @@ -1,37 +0,0 @@ -package dev - -import ( - "github.com/versenilvis/iris/commands/core" -) - -func init() { - core.Register(&core.Spec{ - Name: "npm", - Description: "node packages", - Subcommands: []core.Subcommand{ - {Name: "install", Description: "install packages"}, - {Name: "run", Description: "run script", - Subcommands: []core.Subcommand{ - {Name: "dev", Description: "development server"}, - {Name: "build", Description: "build for production"}, - {Name: "start", Description: "start application"}, - {Name: "test", Description: "run test suite"}, - {Name: "lint", Description: "run linter"}, - }, - }, - {Name: "test", Description: "run tests"}, - {Name: "init", Description: "create package.json"}, - {Name: "publish", Description: "publish package"}, - {Name: "update", Description: "update packages"}, - {Name: "uninstall", Description: "remove package"}, - {Name: "ls", Description: "list installed"}, - {Name: "audit", Description: "security audit"}, - {Name: "cache", Description: "manage cache", - Subcommands: []core.Subcommand{ - {Name: "clean", Description: "clear cache"}, - {Name: "verify", Description: "verify cache"}, - }, - }, - }, - }) -} diff --git a/commands/dev/pip.go b/commands/dev/pip.go deleted file mode 100644 index b9c7e74..0000000 --- a/commands/dev/pip.go +++ /dev/null @@ -1,19 +0,0 @@ -package dev - -import ( - "github.com/versenilvis/iris/commands/core" -) - -func init() { - core.Register(&core.Spec{ - Name: "pip", - Description: "python packages", - Subcommands: []core.Subcommand{ - {Name: "install", Description: "install package"}, - {Name: "uninstall", Description: "remove package"}, - {Name: "freeze", Description: "list packages"}, - {Name: "list", Description: "list installed"}, - {Name: "show", Description: "show package info"}, - }, - }) -} diff --git a/commands/fs/ls.go b/commands/fs/ls.go index 557b946..9f45f99 100644 --- a/commands/fs/ls.go +++ b/commands/fs/ls.go @@ -13,8 +13,16 @@ func init() { {Name: "-a", Description: "all files"}, {Name: "-l", Description: "long format"}, {Name: "-h", Description: "human readable"}, + {Name: "-la", Description: "long + all"}, {Name: "-R", Description: "recursive"}, {Name: "-t", Description: "sort by time"}, + {Name: "-S", Description: "sort by size"}, + {Name: "-1", Description: "one per line"}, + {Name: "--color", Description: "colorize output"}, + {Name: "-d", Description: "list directories only"}, + {Name: "-i", Description: "show inode numbers"}, + {Name: "-s", Description: "show block size"}, + {Name: "-r", Description: "reverse order"}, }, }) } diff --git a/commands/dev/git.go b/commands/git/git.go similarity index 80% rename from commands/dev/git.go rename to commands/git/git.go index b577038..3ebb22e 100644 --- a/commands/dev/git.go +++ b/commands/git/git.go @@ -1,4 +1,4 @@ -package dev +package git import ( "context" @@ -17,6 +17,39 @@ func GitStashGenerator(tokens []string, _ string, _ string) []core.Suggestion { return getGitResults(tokens, "stash", "list", "--format=%gd: %gs") } +func GitTagGenerator(tokens []string, _ string, _ string) []core.Suggestion { + return getGitResults(tokens, "tag", "-l") +} + +func GitCommitGenerator(tokens []string, _ string, _ string) []core.Suggestion { + cwd := core.GetCWD() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "git", "log", "--format=%h [%cr] %s", "-30") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil + } + + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, " ", 2) + hash := parts[0] + desc := "" + if len(parts) == 2 { + desc = parts[1] + } + results = append(results, core.Suggestion{Cmd: hash, Desc: desc}) + } + return results +} + func getGitResults(tokens []string, args ...string) []core.Suggestion { return getGitResultsFiltered(tokens, false, args...) } @@ -47,13 +80,18 @@ func getGitResultsFiltered(tokens []string, localOnly bool, args ...string) []co lines := strings.Split(string(out), "\n") var results []core.Suggestion - // more robust subcommand detection: find the first non-flag after "git" + // find the subcommand by skipping global flags that take arguments subcommand := "" for i := 1; i < len(tokens); i++ { - if !strings.HasPrefix(tokens[i], "-") { - subcommand = tokens[i] - break + t := tokens[i] + if strings.HasPrefix(t, "-") { + if t == "-c" || t == "-C" || t == "--git-dir" || t == "--work-tree" || t == "--namespace" || t == "--super-prefix" || t == "--config-env" || t == "--exec-path" { + i++ + } + continue } + subcommand = t + break } for _, line := range lines { @@ -117,12 +155,8 @@ func getGitResultsFiltered(tokens []string, localOnly bool, args ...string) []co if activeBranch != "" { for i, r := range results { if r.Cmd == activeBranch { - // move active branch to front if found - newResults := make([]core.Suggestion, 0, len(results)) - newResults = append(newResults, r) - newResults = append(newResults, results[:i]...) - newResults = append(newResults, results[i+1:]...) - results = newResults + copy(results[1:i+1], results[0:i]) + results[0] = r break } } @@ -348,9 +382,7 @@ func init() { { Name: "tag", Description: "manage tags", - Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { - return getGitResults(tokens, "tag", "-l") - }, + Generator: GitTagGenerator, Options: []core.Option{ {Name: "-a", Description: "annotated tag"}, {Name: "-d", Description: "delete tag"}, @@ -359,6 +391,29 @@ func init() { {Name: "-m", Description: "tag message"}, }, }, + { + Name: "show", + Description: "show object", + Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { + tags := GitTagGenerator(tokens, prefix, partial) + commits := GitCommitGenerator(tokens, prefix, partial) + return append(tags, commits...) + }, + Options: []core.Option{ + {Name: "--stat", Description: "diffstat only"}, + {Name: "--name-only", Description: "filenames only"}, + }, + }, + { + Name: "revert", + Description: "revert a commit", + Generator: GitCommitGenerator, + Options: []core.Option{ + {Name: "--no-commit", Description: "no auto commit"}, + {Name: "--abort", Description: "abort revert"}, + {Name: "--continue", Description: "continue revert"}, + }, + }, { Name: "reset", Description: "reset HEAD", @@ -419,9 +474,32 @@ func init() { { Name: "cherry-pick", Description: "apply commit", + Generator: GitCommitGenerator, Options: []core.Option{ {Name: "--no-commit", Description: "no auto commit"}, {Name: "--abort", Description: "abort pick"}, + {Name: "--continue", Description: "continue pick"}, + }, + }, + { + Name: "worktree", + Description: "manage worktrees", + Subcommands: []core.Subcommand{ + {Name: "add", Description: "add new worktree", Generator: core.FileGenerator("/")}, + {Name: "list", Description: "list worktrees"}, + {Name: "remove", Description: "remove worktree"}, + {Name: "prune", Description: "prune stale worktrees"}, + }, + }, + { + Name: "submodule", + Description: "manage submodules", + Subcommands: []core.Subcommand{ + {Name: "add", Description: "add submodule"}, + {Name: "init", Description: "init submodule config"}, + {Name: "update", Description: "update submodules", Options: []core.Option{{Name: "--init", Description: "init if needed"}, {Name: "--recursive", Description: "recursive update"}}}, + {Name: "status", Description: "show submodule status"}, + {Name: "foreach", Description: "run command in each submodule"}, }, }, { diff --git a/commands/dev/go.go b/commands/golang/go.go similarity index 99% rename from commands/dev/go.go rename to commands/golang/go.go index fee524c..8a43033 100644 --- a/commands/dev/go.go +++ b/commands/golang/go.go @@ -1,4 +1,4 @@ -package dev +package golang import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/js/bun.go b/commands/js/bun.go new file mode 100644 index 0000000..faeaa4d --- /dev/null +++ b/commands/js/bun.go @@ -0,0 +1,116 @@ +package js + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "bun", + Description: "bun js runtime", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages", Options: []core.Option{ + {Name: "--frozen-lockfile", Description: "no lockfile update"}, + {Name: "--production", Description: "production only"}, + {Name: "--no-save", Description: "don't update package.json"}, + {Name: "--dry-run", Description: "print what would be installed"}, + }}, + {Name: "add", Description: "add package", Options: []core.Option{ + {Name: "-d", Description: "dev dependency"}, + {Name: "--dev", Description: "dev dependency (long form)"}, + {Name: "--optional", Description: "optional dependency"}, + {Name: "--peer", Description: "peer dependency"}, + {Name: "-g", Description: "global"}, + {Name: "--exact", Description: "exact version"}, + {Name: "--no-save", Description: "don't update package.json"}, + }}, + {Name: "remove", Description: "remove package", Options: []core.Option{ + {Name: "-g", Description: "remove global package"}, + }}, + {Name: "run", Description: "run script", Generator: NpmScriptGenerator, Options: []core.Option{ + {Name: "--silent", Description: "suppress output"}, + {Name: "--bun", Description: "force bun runtime"}, + {Name: "--watch", Description: "watch mode"}, + {Name: "--hot", Description: "hot reload"}, + {Name: "--smol", Description: "reduce memory usage"}, + }}, + {Name: "build", Description: "bundle files", Generator: core.FileGenerator(".ts", ".tsx", ".js", ".jsx"), Options: []core.Option{ + {Name: "--outdir", Description: "output directory"}, + {Name: "--outfile", Description: "output file"}, + {Name: "--minify", Description: "enable all minification"}, + {Name: "--minify-syntax", Description: "minify syntax only"}, + {Name: "--minify-whitespace", Description: "minify whitespace only"}, + {Name: "--minify-identifiers", Description: "minify identifiers only"}, + {Name: "--sourcemap", Description: "generate sourcemap (none/inline/external)"}, + {Name: "--target", Description: "target environment (browser/bun/node)"}, + {Name: "--format", Description: "output format (esm/cjs/iife)"}, + {Name: "--splitting", Description: "enable code splitting"}, + {Name: "--entry-naming", Description: "entry file naming pattern"}, + {Name: "--chunk-naming", Description: "chunk file naming pattern"}, + {Name: "--asset-naming", Description: "asset file naming pattern"}, + {Name: "--watch", Description: "watch mode"}, + {Name: "--external", Description: "mark package as external"}, + {Name: "--define", Description: "replace global identifiers"}, + {Name: "--loader", Description: "set file loader"}, + {Name: "--public-path", Description: "prefix for public assets"}, + }}, + {Name: "test", Description: "run tests", Options: []core.Option{ + {Name: "--watch", Description: "watch mode"}, + {Name: "-t", Description: "filter by test name"}, + {Name: "--coverage", Description: "collect coverage"}, + {Name: "--bail", Description: "stop after first failure"}, + {Name: "--timeout", Description: "test timeout in ms"}, + {Name: "--rerun-each", Description: "rerun each test n times"}, + {Name: "--only", Description: "run test.only tests"}, + {Name: "--todo", Description: "show todo tests"}, + {Name: "--reporter", Description: "test reporter (default/junit/tap)"}, + }}, + {Name: "x", Description: "execute package (bunx)", Options: []core.Option{ + {Name: "--bun", Description: "force bun runtime"}, + {Name: "--silent", Description: "suppress output"}, + }}, + {Name: "outdated", Description: "check outdated packages"}, + {Name: "patch", Description: "patch a package", Options: []core.Option{ + {Name: "--commit", Description: "commit the patch"}, + }}, + {Name: "publish", Description: "publish package to npm", Options: []core.Option{ + {Name: "--dry-run", Description: "simulate publish"}, + {Name: "--tag", Description: "dist-tag"}, + {Name: "--access", Description: "public or restricted"}, + }}, + {Name: "init", Description: "create new project", Options: []core.Option{ + {Name: "-y", Description: "skip prompts"}, + }}, + {Name: "create", Description: "create from template"}, + {Name: "repl", Description: "launch bun repl"}, + {Name: "upgrade", Description: "upgrade bun"}, + {Name: "link", Description: "link package"}, + {Name: "unlink", Description: "unlink package"}, + {Name: "pm", Description: "package manager helpers", Subcommands: []core.Subcommand{ + {Name: "cache", Description: "show cache path"}, + {Name: "ls", Description: "list packages"}, + {Name: "hash", Description: "print lockfile hash"}, + {Name: "hash-print", Description: "print resolved lockfile hash"}, + {Name: "trust", Description: "trust package scripts"}, + {Name: "untrusted", Description: "show untrusted packages"}, + }}, + }, + Options: []core.Option{ + {Name: "--watch", Description: "watch for changes"}, + {Name: "--hot", Description: "hot module reload"}, + {Name: "--smol", Description: "reduce memory usage"}, + {Name: "--silent", Description: "suppress output"}, + {Name: "--bun", Description: "force bun runtime"}, + {Name: "--version", Description: "print version"}, + }, + }) + + core.Register(&core.Spec{ + Name: "bunx", + Description: "execute package (bun x)", + Options: []core.Option{ + {Name: "--bun", Description: "force bun runtime"}, + {Name: "--silent", Description: "suppress output"}, + }, + }) +} diff --git a/commands/js/npm.go b/commands/js/npm.go new file mode 100644 index 0000000..76ad945 --- /dev/null +++ b/commands/js/npm.go @@ -0,0 +1,85 @@ +package js + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/versenilvis/iris/commands/core" +) + +func NpmScriptGenerator(tokens []string, _ string, _ string) []core.Suggestion { + cwd := core.GetCWD() + data, err := os.ReadFile(filepath.Join(cwd, "package.json")) + if err != nil { + return []core.Suggestion{ + {Cmd: "dev", Desc: "development server"}, + {Cmd: "build", Desc: "build for production"}, + {Cmd: "start", Desc: "start application"}, + {Cmd: "test", Desc: "run test suite"}, + {Cmd: "lint", Desc: "run linter"}, + } + } + + var pkg struct { + Scripts map[string]string `json:"scripts"` + } + if err := json.Unmarshal(data, &pkg); err != nil || len(pkg.Scripts) == 0 { + return nil + } + + priority := []string{"dev", "start", "build", "test", "lint", "preview", "typecheck", "format"} + seen := make(map[string]bool) + var results []core.Suggestion + + for _, name := range priority { + if cmd, ok := pkg.Scripts[name]; ok { + results = append(results, core.Suggestion{Cmd: name, Desc: cmd}) + seen[name] = true + } + } + + for name, cmd := range pkg.Scripts { + if !seen[name] { + results = append(results, core.Suggestion{Cmd: name, Desc: cmd}) + } + } + + return results +} + +func init() { + core.Register(&core.Spec{ + Name: "npm", + Description: "node packages", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages", Options: []core.Option{ + {Name: "--save-dev", Description: "save as devDependency"}, + {Name: "--save-exact", Description: "exact version"}, + {Name: "--legacy-peer-deps", Description: "skip peer deps"}, + }}, + {Name: "run", Description: "run script", Generator: NpmScriptGenerator}, + {Name: "test", Description: "run tests"}, + {Name: "init", Description: "create package.json", Options: []core.Option{ + {Name: "-y", Description: "skip prompts"}, + }}, + {Name: "publish", Description: "publish package"}, + {Name: "update", Description: "update packages"}, + {Name: "uninstall", Description: "remove package"}, + {Name: "ls", Description: "list installed"}, + {Name: "audit", Description: "security audit", Options: []core.Option{ + {Name: "fix", Description: "auto fix"}, + }}, + {Name: "ci", Description: "clean install from lockfile"}, + {Name: "pack", Description: "create tarball"}, + {Name: "link", Description: "symlink package"}, + {Name: "cache", Description: "manage cache", Subcommands: []core.Subcommand{ + {Name: "clean", Description: "clear cache"}, + {Name: "verify", Description: "verify cache"}, + }}, + }, + Options: []core.Option{ + {Name: "--prefix", Description: "set working directory"}, + }, + }) +} diff --git a/commands/js/pnpm.go b/commands/js/pnpm.go new file mode 100644 index 0000000..6cc8604 --- /dev/null +++ b/commands/js/pnpm.go @@ -0,0 +1,49 @@ +package js + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "pnpm", + Description: "fast node packages", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages", Options: []core.Option{ + {Name: "--frozen-lockfile", Description: "no lockfile update"}, + {Name: "--prod", Description: "production only"}, + }}, + {Name: "add", Description: "add package", Options: []core.Option{ + {Name: "-D", Description: "save as devDependency"}, + {Name: "-g", Description: "install globally"}, + {Name: "-E", Description: "exact version"}, + }}, + {Name: "remove", Description: "remove package"}, + {Name: "run", Description: "run script", Generator: NpmScriptGenerator}, + {Name: "build", Description: "build project"}, + {Name: "dev", Description: "start dev server"}, + {Name: "test", Description: "run tests"}, + {Name: "update", Description: "update packages", Options: []core.Option{ + {Name: "--latest", Description: "update to latest"}, + {Name: "-i", Description: "interactive"}, + }}, + {Name: "store", Description: "manage store", Subcommands: []core.Subcommand{ + {Name: "prune", Description: "remove unreferenced"}, + {Name: "status", Description: "check store"}, + {Name: "path", Description: "print store path"}, + }}, + {Name: "exec", Description: "exec shell command"}, + {Name: "dlx", Description: "fetch and run package"}, + {Name: "ls", Description: "list packages"}, + {Name: "audit", Description: "security audit"}, + {Name: "patch", Description: "patch package"}, + {Name: "init", Description: "create package.json"}, + {Name: "publish", Description: "publish package"}, + }, + Options: []core.Option{ + {Name: "--filter", Description: "filter by package"}, + {Name: "-r", Description: "recursive"}, + {Name: "--workspace-root", Description: "run in workspace root"}, + }, + }) +} diff --git a/commands/js/yarn.go b/commands/js/yarn.go new file mode 100644 index 0000000..5473c36 --- /dev/null +++ b/commands/js/yarn.go @@ -0,0 +1,55 @@ +package js + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "yarn", + Description: "yarn package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages", Options: []core.Option{ + {Name: "--frozen-lockfile", Description: "error on lockfile change"}, + {Name: "--production", Description: "production only"}, + }}, + {Name: "add", Description: "add package", Options: []core.Option{ + {Name: "-D", Description: "dev dependency"}, + {Name: "-P", Description: "peer dependency"}, + {Name: "-E", Description: "exact version"}, + {Name: "-g", Description: "global"}, + }}, + {Name: "remove", Description: "remove package"}, + {Name: "run", Description: "run script", Generator: NpmScriptGenerator}, + {Name: "build", Description: "build project"}, + {Name: "dev", Description: "start dev server"}, + {Name: "test", Description: "run tests"}, + {Name: "upgrade", Description: "upgrade packages", Options: []core.Option{ + {Name: "--latest", Description: "upgrade to latest"}, + {Name: "-i", Description: "interactive"}, + }}, + {Name: "workspace", Description: "run command in workspace"}, + {Name: "workspaces", Description: "list workspaces", Subcommands: []core.Subcommand{ + {Name: "info", Description: "workspace info"}, + {Name: "run", Description: "run in all workspaces"}, + }}, + {Name: "link", Description: "symlink package"}, + {Name: "unlink", Description: "unlink package"}, + {Name: "audit", Description: "security audit"}, + {Name: "cache", Description: "manage cache", Subcommands: []core.Subcommand{ + {Name: "clean", Description: "clear cache"}, + {Name: "list", Description: "list cache"}, + {Name: "dir", Description: "print cache dir"}, + }}, + {Name: "init", Description: "create package.json", Options: []core.Option{ + {Name: "-2", Description: "berry (v2+)"}, + }}, + {Name: "publish", Description: "publish package"}, + {Name: "info", Description: "show package info"}, + }, + Options: []core.Option{ + {Name: "--cwd", Description: "working directory"}, + {Name: "--verbose", Description: "verbose output"}, + }, + }) +} diff --git a/commands/jvm/jvm.go b/commands/jvm/jvm.go new file mode 100644 index 0000000..9612f82 --- /dev/null +++ b/commands/jvm/jvm.go @@ -0,0 +1,56 @@ +package jvm + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "javac", + Description: "Java compiler", + Generator: core.FileGenerator(".java"), + Options: []core.Option{ + {Name: "-d", Description: "output directory"}, + {Name: "-cp", Description: "class path"}, + {Name: "-classpath", Description: "class path (long form)"}, + {Name: "-sourcepath", Description: "source path"}, + {Name: "-g", Description: "debug info"}, + {Name: "-verbose", Description: "verbose"}, + {Name: "-encoding", Description: "source file encoding"}, + {Name: "--release", Description: "target release version"}, + {Name: "-source", Description: "source version"}, + {Name: "-target", Description: "target version"}, + {Name: "-Xlint", Description: "enable warnings"}, + }, + }) + + core.Register(&core.Spec{ + Name: "java", + Description: "Java runtime", + Generator: core.FileGenerator(".jar", ".class"), + Options: []core.Option{ + {Name: "-jar", Description: "run jar file"}, + {Name: "-cp", Description: "class path"}, + {Name: "-Xmx", Description: "max heap size (e.g. -Xmx512m)"}, + {Name: "-Xms", Description: "initial heap size"}, + {Name: "-D", Description: "set system property"}, + {Name: "-ea", Description: "enable assertions"}, + {Name: "--version", Description: "print version"}, + {Name: "-server", Description: "server VM"}, + {Name: "-verbose", Description: "verbose class loading"}, + }, + }) + + core.Register(&core.Spec{ + Name: "kotlinc", + Description: "Kotlin compiler", + Generator: core.FileGenerator(".kt", ".kts"), + Options: []core.Option{ + {Name: "-d", Description: "output dir or jar"}, + {Name: "-cp", Description: "class path"}, + {Name: "-include-runtime", Description: "include runtime in jar"}, + {Name: "-jvm-target", Description: "JVM target version"}, + {Name: "-script", Description: "run as script"}, + }, + }) +} diff --git a/commands/ops/docker.go b/commands/ops/docker.go new file mode 100644 index 0000000..abb36d7 --- /dev/null +++ b/commands/ops/docker.go @@ -0,0 +1,278 @@ +package ops + +import ( + "context" + "os/exec" + "strings" + "time" + + "github.com/versenilvis/iris/commands/core" +) + +func dockerContainerGenerator(tokens []string, _ string, _ string) []core.Suggestion { + cwd := core.GetCWD() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format={{.Names}}") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil + } + + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + results = append(results, core.Suggestion{Cmd: line, Desc: "container"}) + } + return results +} + +func dockerRunningContainerGenerator(tokens []string, _ string, _ string) []core.Suggestion { + cwd := core.GetCWD() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "docker", "ps", "--format={{.Names}}") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil + } + + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + results = append(results, core.Suggestion{Cmd: line, Desc: "running"}) + } + return results +} + +func dockerImageGenerator(tokens []string, _ string, _ string) []core.Suggestion { + cwd := core.GetCWD() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "docker", "images", "--format={{.Repository}}:{{.Tag}}") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil + } + + seen := make(map[string]bool) + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" || line == ":" || seen[line] { + continue + } + seen[line] = true + results = append(results, core.Suggestion{Cmd: line, Desc: "image"}) + } + return results +} + +func init() { + core.Register(&core.Spec{ + Name: "docker", + Description: "container engine", + Subcommands: []core.Subcommand{ + { + Name: "ps", + Description: "list containers", + Options: []core.Option{ + {Name: "-a", Description: "show all"}, + {Name: "-q", Description: "only show IDs"}, + {Name: "--format", Description: "format output"}, + {Name: "--filter", Description: "filter by condition"}, + }, + }, + { + Name: "build", + Description: "build image", + Generator: core.FileGenerator(".dockerfile", ".Dockerfile"), + Options: []core.Option{ + {Name: "-t", Description: "tag name"}, + {Name: "-f", Description: "dockerfile path"}, + {Name: "--no-cache", Description: "no build cache"}, + {Name: "--platform", Description: "target platform"}, + {Name: "--build-arg", Description: "build argument"}, + }, + }, + { + Name: "run", + Description: "run container", + Generator: dockerImageGenerator, + Options: []core.Option{ + {Name: "-d", Description: "detached mode"}, + {Name: "-p", Description: "port mapping"}, + {Name: "-v", Description: "volume mount"}, + {Name: "--rm", Description: "auto remove"}, + {Name: "--name", Description: "container name"}, + {Name: "-it", Description: "interactive tty"}, + {Name: "-e", Description: "set env variable"}, + {Name: "--network", Description: "network mode"}, + {Name: "--restart", Description: "restart policy"}, + }, + }, + { + Name: "pull", + Description: "pull image", + }, + { + Name: "push", + Description: "push image", + Generator: dockerImageGenerator, + }, + { + Name: "exec", + Description: "exec in container", + Generator: dockerRunningContainerGenerator, + Options: []core.Option{ + {Name: "-it", Description: "interactive tty"}, + {Name: "-e", Description: "set env variable"}, + {Name: "-u", Description: "run as user"}, + }, + }, + { + Name: "stop", + Description: "stop container", + Generator: dockerRunningContainerGenerator, + }, + { + Name: "start", + Description: "start container", + Generator: dockerContainerGenerator, + }, + { + Name: "restart", + Description: "restart container", + Generator: dockerContainerGenerator, + }, + { + Name: "rm", + Description: "remove container", + Generator: dockerContainerGenerator, + Options: []core.Option{ + {Name: "-f", Description: "force remove"}, + {Name: "-v", Description: "remove volumes"}, + }, + }, + { + Name: "rmi", + Description: "remove image", + Generator: dockerImageGenerator, + Options: []core.Option{ + {Name: "-f", Description: "force remove"}, + }, + }, + { + Name: "images", + Description: "list images", + Options: []core.Option{ + {Name: "-a", Description: "show all"}, + {Name: "-q", Description: "only IDs"}, + {Name: "--format", Description: "format output"}, + }, + }, + { + Name: "logs", + Description: "view logs", + Generator: dockerContainerGenerator, + Options: []core.Option{ + {Name: "-f", Description: "follow output"}, + {Name: "--tail", Description: "last n lines"}, + {Name: "--since", Description: "since timestamp"}, + {Name: "-t", Description: "show timestamps"}, + }, + }, + { + Name: "inspect", + Description: "show low-level info", + Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { + containers := dockerContainerGenerator(tokens, prefix, partial) + images := dockerImageGenerator(tokens, prefix, partial) + return append(containers, images...) + }, + }, + { + Name: "compose", + Description: "multi-container", + Subcommands: []core.Subcommand{ + {Name: "up", Description: "start services", Options: []core.Option{{Name: "-d", Description: "detached"}, {Name: "--build", Description: "rebuild images"}}}, + {Name: "down", Description: "stop services", Options: []core.Option{{Name: "-v", Description: "remove volumes"}}}, + {Name: "build", Description: "build services"}, + {Name: "logs", Description: "view logs", Options: []core.Option{{Name: "-f", Description: "follow"}}}, + {Name: "ps", Description: "list services"}, + {Name: "exec", Description: "execute command"}, + {Name: "restart", Description: "restart services"}, + {Name: "pull", Description: "pull images"}, + {Name: "stop", Description: "stop services"}, + {Name: "start", Description: "start services"}, + {Name: "config", Description: "validate config", Generator: core.FileGenerator(".yml", ".yaml")}, + }, + Options: []core.Option{ + {Name: "-f", Description: "compose file"}, + {Name: "--project-name", Description: "project name"}, + }, + }, + { + Name: "network", + Description: "manage networks", + Subcommands: []core.Subcommand{ + {Name: "ls", Description: "list networks"}, + {Name: "create", Description: "create network"}, + {Name: "rm", Description: "remove network"}, + {Name: "inspect", Description: "show details"}, + {Name: "prune", Description: "remove unused"}, + }, + }, + { + Name: "volume", + Description: "manage volumes", + Subcommands: []core.Subcommand{ + {Name: "ls", Description: "list volumes"}, + {Name: "create", Description: "create volume"}, + {Name: "rm", Description: "remove volume"}, + {Name: "prune", Description: "remove unused"}, + {Name: "inspect", Description: "show details"}, + }, + }, + { + Name: "system", + Description: "manage docker system", + Subcommands: []core.Subcommand{ + {Name: "prune", Description: "remove unused data", Options: []core.Option{{Name: "-a", Description: "remove all unused"}, {Name: "--volumes", Description: "include volumes"}}}, + {Name: "df", Description: "show disk usage"}, + {Name: "info", Description: "system info"}, + }, + }, + }, + }) + + core.Register(&core.Spec{ + Name: "docker-compose", + Description: "multi-container (legacy)", + Subcommands: []core.Subcommand{ + {Name: "up", Description: "start services", Options: []core.Option{{Name: "-d", Description: "detached"}, {Name: "--build", Description: "rebuild"}}}, + {Name: "down", Description: "stop services", Options: []core.Option{{Name: "-v", Description: "remove volumes"}}}, + {Name: "build", Description: "build services"}, + {Name: "logs", Description: "view logs", Options: []core.Option{{Name: "-f", Description: "follow"}}}, + {Name: "ps", Description: "list services"}, + {Name: "exec", Description: "execute command"}, + {Name: "restart", Description: "restart services"}, + {Name: "pull", Description: "pull images"}, + }, + Options: []core.Option{ + {Name: "-f", Description: "compose file"}, + }, + }) +} diff --git a/commands/dev/kubectl.go b/commands/ops/kubectl.go similarity index 98% rename from commands/dev/kubectl.go rename to commands/ops/kubectl.go index 9f38196..7b8ef4d 100644 --- a/commands/dev/kubectl.go +++ b/commands/ops/kubectl.go @@ -1,4 +1,4 @@ -package dev +package ops import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/ops/ssh.go b/commands/ops/ssh.go new file mode 100644 index 0000000..2740632 --- /dev/null +++ b/commands/ops/ssh.go @@ -0,0 +1,105 @@ +package ops + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "github.com/versenilvis/iris/commands/core" +) + +func sshHostGenerator(tokens []string, _ string, _ string) []core.Suggestion { + home, err := os.UserHomeDir() + if err != nil { + return nil + } + + paths := []string{ + filepath.Join(home, ".ssh", "config"), + "/etc/ssh/ssh_config", + } + + seen := make(map[string]bool) + var results []core.Suggestion + + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + parts := strings.Fields(scanner.Text()) + if len(parts) < 2 || !strings.EqualFold(parts[0], "host") { + continue + } + + for _, host := range parts[1:] { + // skip wildcards + if strings.ContainsAny(host, "*?!") { + continue + } + if seen[host] { + continue + } + seen[host] = true + results = append(results, core.Suggestion{Cmd: host, Desc: "ssh host"}) + } + } + _ = scanner.Err() + _ = f.Close() + } + + return results +} + +func init() { + sshOptions := []core.Option{ + {Name: "-p", Description: "port"}, + {Name: "-i", Description: "identity file"}, + {Name: "-L", Description: "local port forward"}, + {Name: "-R", Description: "remote port forward"}, + {Name: "-D", Description: "dynamic socks proxy"}, + {Name: "-N", Description: "no remote command"}, + {Name: "-f", Description: "background"}, + {Name: "-v", Description: "verbose"}, + {Name: "-A", Description: "agent forwarding"}, + {Name: "-X", Description: "x11 forwarding"}, + } + + core.Register(&core.Spec{ + Name: "ssh", + Description: "secure shell", + Generator: sshHostGenerator, + Options: sshOptions, + }) + + core.Register(&core.Spec{ + Name: "scp", + Description: "secure copy", + Generator: sshHostGenerator, + Options: []core.Option{ + {Name: "-r", Description: "recursive"}, + {Name: "-p", Description: "port"}, + {Name: "-i", Description: "identity file"}, + {Name: "-P", Description: "remote port"}, + }, + }) + + core.Register(&core.Spec{ + Name: "rsync", + Description: "remote sync", + Generator: sshHostGenerator, + Options: []core.Option{ + {Name: "-av", Description: "archive + verbose"}, + {Name: "-z", Description: "compress"}, + {Name: "--delete", Description: "delete extraneous"}, + {Name: "--progress", Description: "show progress"}, + {Name: "-e", Description: "remote shell command"}, + {Name: "--exclude", Description: "exclude pattern"}, + {Name: "--dry-run", Description: "simulation only"}, + }, + }) +} diff --git a/commands/pkginstaller/pkgmgr.go b/commands/pkginstaller/pkgmgr.go new file mode 100644 index 0000000..ee773e7 --- /dev/null +++ b/commands/pkginstaller/pkgmgr.go @@ -0,0 +1,327 @@ +package pkginstaller + +import ( + "context" + "os/exec" + "strings" + "time" + + "github.com/versenilvis/iris/commands/core" +) + +func installedPackageGenerator(pm string) core.GeneratorFunc { + return func(tokens []string, _ string, _ string) []core.Suggestion { + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + + var cmd *exec.Cmd + switch pm { + case "pacman", "yay", "paru": + cmd = exec.CommandContext(ctx, pm, "-Qq") + case "apt", "apt-get": + cmd = exec.CommandContext(ctx, "dpkg-query", "-W", "-f=${Package}\n") + case "dnf", "yum": + cmd = exec.CommandContext(ctx, "rpm", "-qa", "--qf", "%{NAME}\n") + case "brew": + cmd = exec.CommandContext(ctx, "brew", "list") + default: + return nil + } + + out, err := cmd.Output() + if err != nil { + return nil + } + + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + results = append(results, core.Suggestion{Cmd: line, Desc: "installed"}) + } + return results + } +} + +func init() { + // pacman + core.Register(&core.Spec{ + Name: "pacman", + Description: "Arch package manager", + Options: []core.Option{ + {Name: "-S", Description: "install package"}, + {Name: "-Syu", Description: "full system upgrade"}, + {Name: "-Sy", Description: "sync database"}, + {Name: "-Su", Description: "upgrade installed"}, + {Name: "-Ss", Description: "search in repos"}, + {Name: "-Si", Description: "show package info"}, + {Name: "-R", Description: "remove package"}, + {Name: "-Rs", Description: "remove + unused deps"}, + {Name: "-Rns", Description: "remove + deps + configs"}, + {Name: "-Q", Description: "query installed"}, + {Name: "-Qq", Description: "query names only"}, + {Name: "-Qs", Description: "search installed"}, + {Name: "-Qi", Description: "installed package info"}, + {Name: "-Qo", Description: "which package owns file"}, + {Name: "-Ql", Description: "list package files"}, + {Name: "-U", Description: "install local package"}, + {Name: "-D", Description: "set install reason"}, + {Name: "--noconfirm", Description: "skip confirmations"}, + {Name: "--needed", Description: "skip up-to-date"}, + {Name: "--asdeps", Description: "mark as dependency"}, + {Name: "--asexplicit", Description: "mark as explicit"}, + }, + Generator: installedPackageGenerator("pacman"), + }) + + // yay + core.Register(&core.Spec{ + Name: "yay", + Description: "AUR helper (pacman wrapper)", + Options: []core.Option{ + {Name: "-S", Description: "install package (AUR + repos)"}, + {Name: "-Syu", Description: "full system upgrade + AUR"}, + {Name: "-Ss", Description: "search AUR + repos"}, + {Name: "-Si", Description: "show package info"}, + {Name: "-R", Description: "remove package"}, + {Name: "-Rs", Description: "remove + unused deps"}, + {Name: "-Rns", Description: "remove + deps + configs"}, + {Name: "-Q", Description: "query installed"}, + {Name: "-Qs", Description: "search installed"}, + {Name: "-Qi", Description: "installed package info"}, + {Name: "-Yc", Description: "remove unneeded deps"}, + {Name: "-Y", Description: "yay-specific options"}, + {Name: "--aur", Description: "only AUR results"}, + {Name: "--repo", Description: "only repo results"}, + {Name: "--devel", Description: "update devel packages"}, + {Name: "--noconfirm", Description: "skip confirmations"}, + {Name: "--needed", Description: "skip up-to-date"}, + }, + Generator: installedPackageGenerator("yay"), + }) + + // paru + core.Register(&core.Spec{ + Name: "paru", + Description: "AUR helper (feature-rich)", + Options: []core.Option{ + {Name: "-S", Description: "install package"}, + {Name: "-Syu", Description: "full upgrade"}, + {Name: "-Ss", Description: "search"}, + {Name: "-R", Description: "remove"}, + {Name: "-Rs", Description: "remove + deps"}, + {Name: "-Q", Description: "query installed"}, + {Name: "--fm", Description: "file manager for PKGBUILDs"}, + {Name: "--noconfirm", Description: "skip confirmations"}, + {Name: "--aur", Description: "AUR only"}, + {Name: "--repo", Description: "repo only"}, + }, + Generator: installedPackageGenerator("paru"), + }) + + // apt + core.Register(&core.Spec{ + Name: "apt", + Description: "Debian/Ubuntu package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages"}, + {Name: "remove", Description: "remove packages", Generator: installedPackageGenerator("apt")}, + {Name: "purge", Description: "remove + config files", Generator: installedPackageGenerator("apt")}, + {Name: "autoremove", Description: "remove unused deps"}, + {Name: "update", Description: "refresh package lists"}, + {Name: "upgrade", Description: "upgrade installed"}, + {Name: "full-upgrade", Description: "upgrade with auto remove"}, + {Name: "dist-upgrade", Description: "smart upgrade"}, + {Name: "search", Description: "search packages"}, + {Name: "show", Description: "show package info"}, + {Name: "list", Description: "list packages", Options: []core.Option{ + {Name: "--installed", Description: "installed only"}, + {Name: "--upgradable", Description: "upgradable only"}, + }}, + {Name: "download", Description: "download package (no install)"}, + {Name: "clean", Description: "clear local repo"}, + {Name: "autoclean", Description: "clear old cached packages"}, + {Name: "depends", Description: "show dependencies"}, + {Name: "rdepends", Description: "show reverse dependencies"}, + }, + Options: []core.Option{ + {Name: "-y", Description: "yes to all"}, + {Name: "--no-install-recommends", Description: "skip recommends"}, + {Name: "--dry-run", Description: "simulate"}, + {Name: "-q", Description: "quiet"}, + {Name: "--fix-broken", Description: "fix broken deps"}, + }, + }) + + // apt-get + core.Register(&core.Spec{ + Name: "apt-get", + Description: "Debian/Ubuntu package manager (low-level)", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages"}, + {Name: "remove", Description: "remove packages", Generator: installedPackageGenerator("apt")}, + {Name: "purge", Description: "remove + configs", Generator: installedPackageGenerator("apt")}, + {Name: "autoremove", Description: "remove unused deps"}, + {Name: "update", Description: "refresh package lists"}, + {Name: "upgrade", Description: "upgrade installed"}, + {Name: "dist-upgrade", Description: "smart upgrade"}, + {Name: "clean", Description: "clean cache"}, + {Name: "autoclean", Description: "clean old packages"}, + {Name: "download", Description: "download only"}, + {Name: "source", Description: "get source package"}, + }, + Options: []core.Option{ + {Name: "-y", Description: "yes to all"}, + {Name: "--no-install-recommends", Description: "skip recommends"}, + {Name: "-q", Description: "quiet"}, + {Name: "-s", Description: "simulate"}, + {Name: "--fix-missing", Description: "fix missing"}, + }, + }) + + // dnf (Fedora/RHEL) + core.Register(&core.Spec{ + Name: "dnf", + Description: "Fedora/RHEL package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages"}, + {Name: "remove", Description: "remove packages", Generator: installedPackageGenerator("dnf")}, + {Name: "update", Description: "update packages"}, + {Name: "upgrade", Description: "upgrade system"}, + {Name: "search", Description: "search packages"}, + {Name: "info", Description: "show package info"}, + {Name: "list", Description: "list packages"}, + {Name: "autoremove", Description: "remove unneeded"}, + {Name: "clean", Description: "clean cache", Subcommands: []core.Subcommand{ + {Name: "all", Description: "clean all"}, + {Name: "packages", Description: "clean packages"}, + {Name: "metadata", Description: "clean metadata"}, + }}, + {Name: "repolist", Description: "list repos"}, + {Name: "history", Description: "transaction history"}, + {Name: "group", Description: "manage package groups"}, + }, + Options: []core.Option{ + {Name: "-y", Description: "yes to all"}, + {Name: "-q", Description: "quiet"}, + {Name: "--best", Description: "install best"}, + {Name: "--allowerasing", Description: "allow package replacement"}, + {Name: "--no-best", Description: "allow older versions"}, + }, + }) + + // yum (RHEL/CentOS legacy) + core.Register(&core.Spec{ + Name: "yum", + Description: "RHEL/CentOS package manager (legacy)", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages"}, + {Name: "remove", Description: "remove packages", Generator: installedPackageGenerator("yum")}, + {Name: "update", Description: "update packages"}, + {Name: "search", Description: "search packages"}, + {Name: "info", Description: "show package info"}, + {Name: "list", Description: "list packages"}, + {Name: "clean", Description: "clean cache"}, + {Name: "repolist", Description: "list repos"}, + }, + Options: []core.Option{ + {Name: "-y", Description: "yes to all"}, + {Name: "-q", Description: "quiet"}, + }, + }) + + // brew (macOS / Linux Homebrew) + core.Register(&core.Spec{ + Name: "brew", + Description: "Homebrew package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install formula/cask"}, + {Name: "uninstall", Description: "remove formula", Generator: installedPackageGenerator("brew")}, + {Name: "reinstall", Description: "reinstall formula", Generator: installedPackageGenerator("brew")}, + {Name: "update", Description: "update Homebrew"}, + {Name: "upgrade", Description: "upgrade packages"}, + {Name: "search", Description: "search formulas"}, + {Name: "info", Description: "show package info"}, + {Name: "list", Description: "list installed"}, + {Name: "outdated", Description: "list outdated"}, + {Name: "doctor", Description: "check system"}, + {Name: "cleanup", Description: "remove old versions"}, + {Name: "tap", Description: "add tap repo"}, + {Name: "untap", Description: "remove tap"}, + {Name: "services", Description: "manage services", Subcommands: []core.Subcommand{ + {Name: "start", Description: "start service"}, + {Name: "stop", Description: "stop service"}, + {Name: "restart", Description: "restart service"}, + {Name: "list", Description: "list services"}, + }}, + {Name: "cask", Description: "manage GUI apps", Subcommands: []core.Subcommand{ + {Name: "install", Description: "install cask"}, + {Name: "uninstall", Description: "remove cask"}, + {Name: "list", Description: "list casks"}, + }}, + {Name: "pin", Description: "pin formula version", Generator: installedPackageGenerator("brew")}, + {Name: "unpin", Description: "unpin formula", Generator: installedPackageGenerator("brew")}, + {Name: "link", Description: "create symlinks", Generator: installedPackageGenerator("brew")}, + {Name: "unlink", Description: "remove symlinks", Generator: installedPackageGenerator("brew")}, + }, + Options: []core.Option{ + {Name: "--cask", Description: "target GUI app"}, + {Name: "--formula", Description: "target formula"}, + {Name: "--force", Description: "force operation"}, + {Name: "--verbose", Description: "verbose"}, + {Name: "--dry-run", Description: "simulate"}, + }, + }) + + // snap + core.Register(&core.Spec{ + Name: "snap", + Description: "snap package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install snap"}, + {Name: "remove", Description: "remove snap"}, + {Name: "refresh", Description: "update snaps"}, + {Name: "list", Description: "list installed"}, + {Name: "find", Description: "search snaps"}, + {Name: "info", Description: "show snap info"}, + {Name: "run", Description: "run snap app"}, + {Name: "connect", Description: "connect snap plug"}, + {Name: "disconnect", Description: "disconnect snap plug"}, + {Name: "revert", Description: "revert to previous"}, + {Name: "enable", Description: "enable snap"}, + {Name: "disable", Description: "disable snap"}, + }, + Options: []core.Option{ + {Name: "--classic", Description: "classic confinement"}, + {Name: "--beta", Description: "beta channel"}, + {Name: "--edge", Description: "edge channel"}, + {Name: "--channel", Description: "specific channel"}, + }, + }) + + // flatpak + core.Register(&core.Spec{ + Name: "flatpak", + Description: "flatpak package manager", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install application"}, + {Name: "uninstall", Description: "remove application"}, + {Name: "update", Description: "update applications"}, + {Name: "list", Description: "list installed"}, + {Name: "search", Description: "search applications"}, + {Name: "info", Description: "show app info"}, + {Name: "run", Description: "run application"}, + {Name: "remote-add", Description: "add remote"}, + {Name: "remote-delete", Description: "delete remote"}, + {Name: "remote-list", Description: "list remotes"}, + {Name: "repair", Description: "repair installation"}, + }, + Options: []core.Option{ + {Name: "--user", Description: "user installation"}, + {Name: "--system", Description: "system installation"}, + {Name: "-y", Description: "non-interactive"}, + }, + }) +} diff --git a/commands/python/pip.go b/commands/python/pip.go new file mode 100644 index 0000000..931427f --- /dev/null +++ b/commands/python/pip.go @@ -0,0 +1,81 @@ +package python + +import ( + "context" + "os/exec" + "strings" + "time" + + "github.com/versenilvis/iris/commands/core" +) + +func pipPackageGenerator(tokens []string, _ string, _ string) []core.Suggestion { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "pip", "list", "--format=freeze") + out, err := cmd.Output() + if err != nil { + // try pip3 + cmd = exec.CommandContext(ctx, "pip3", "list", "--format=freeze") + out, err = cmd.Output() + if err != nil { + return nil + } + } + + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // format is "package==version" + parts := strings.SplitN(line, "==", 2) + name := parts[0] + desc := "" + if len(parts) == 2 { + desc = "v" + parts[1] + } + results = append(results, core.Suggestion{Cmd: name, Desc: desc}) + } + return results +} + +func makePipSpec(name string) *core.Spec { + return &core.Spec{ + Name: name, + Description: "python packages", + Subcommands: []core.Subcommand{ + {Name: "install", Description: "install package", Options: []core.Option{ + {Name: "-r", Description: "from requirements file"}, + {Name: "-U", Description: "upgrade"}, + {Name: "--user", Description: "install for user only"}, + {Name: "--editable", Description: "editable install"}, + {Name: "--index-url", Description: "custom package index"}, + {Name: "--no-deps", Description: "skip dependencies"}, + }}, + {Name: "uninstall", Description: "remove package", Generator: pipPackageGenerator, Options: []core.Option{ + {Name: "-y", Description: "yes to all prompts"}, + }}, + {Name: "freeze", Description: "list installed packages"}, + {Name: "list", Description: "list installed", Options: []core.Option{ + {Name: "--outdated", Description: "show outdated"}, + {Name: "--format", Description: "output format"}, + }}, + {Name: "show", Description: "show package info", Generator: pipPackageGenerator}, + {Name: "download", Description: "download packages"}, + {Name: "check", Description: "verify compatibility"}, + {Name: "config", Description: "manage config"}, + {Name: "search", Description: "search pypi"}, + {Name: "wheel", Description: "build wheel archive"}, + {Name: "hash", Description: "compute hashes"}, + {Name: "completion", Description: "generate completion"}, + }, + } +} + +func init() { + core.Register(makePipSpec("pip")) + core.Register(makePipSpec("pip3")) +} diff --git a/commands/python/python.go b/commands/python/python.go new file mode 100644 index 0000000..9c5edf1 --- /dev/null +++ b/commands/python/python.go @@ -0,0 +1,105 @@ +package python + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func makePythonSpec(name string) *core.Spec { + return &core.Spec{ + Name: name, + Description: "python interpreter", + Generator: core.FileGenerator(".py"), + Options: []core.Option{ + {Name: "-m", Description: "run module as script"}, + {Name: "-c", Description: "run inline command"}, + {Name: "-i", Description: "interactive after script"}, + {Name: "-u", Description: "unbuffered stdio"}, + {Name: "-O", Description: "optimize bytecode"}, + {Name: "-v", Description: "verbose"}, + {Name: "-W", Description: "warning control"}, + {Name: "-B", Description: "no .pyc files"}, + }, + } +} + +func init() { + core.Register(makePythonSpec("python")) + core.Register(makePythonSpec("python3")) + core.Register(makePythonSpec("py")) + + core.Register(&core.Spec{ + Name: "uv", + Description: "fast python package manager", + Subcommands: []core.Subcommand{ + {Name: "add", Description: "add dependency", Options: []core.Option{ + {Name: "--dev", Description: "dev dependency"}, + {Name: "--optional", Description: "optional dependency"}, + {Name: "-e", Description: "editable install"}, + {Name: "--index-url", Description: "custom index"}, + }}, + {Name: "remove", Description: "remove dependency", Generator: pipPackageGenerator}, + {Name: "sync", Description: "sync environment", Options: []core.Option{ + {Name: "--frozen", Description: "no lockfile update"}, + {Name: "--dev", Description: "include dev deps"}, + }}, + {Name: "run", Description: "run command in env", Generator: core.FileGenerator(".py")}, + {Name: "pip", Description: "pip interface", Subcommands: []core.Subcommand{ + {Name: "install", Description: "install packages"}, + {Name: "uninstall", Description: "uninstall packages", Generator: pipPackageGenerator}, + {Name: "list", Description: "list packages"}, + {Name: "freeze", Description: "freeze packages"}, + {Name: "show", Description: "show package", Generator: pipPackageGenerator}, + {Name: "compile", Description: "compile requirements"}, + }}, + {Name: "venv", Description: "create virtual env", Generator: core.FileGenerator("/"), Options: []core.Option{ + {Name: "--python", Description: "python version"}, + {Name: "--seed", Description: "install seed packages"}, + }}, + {Name: "init", Description: "init project"}, + {Name: "build", Description: "build package"}, + {Name: "publish", Description: "publish to pypi"}, + {Name: "lock", Description: "update lockfile"}, + {Name: "tree", Description: "show dependency tree"}, + {Name: "tool", Description: "manage tools", Subcommands: []core.Subcommand{ + {Name: "install", Description: "install tool"}, + {Name: "run", Description: "run tool"}, + {Name: "list", Description: "list tools"}, + {Name: "uninstall", Description: "uninstall tool"}, + }}, + {Name: "python", Description: "manage python versions", Subcommands: []core.Subcommand{ + {Name: "install", Description: "install python"}, + {Name: "list", Description: "list versions"}, + {Name: "find", Description: "find python"}, + {Name: "pin", Description: "pin python version"}, + }}, + }, + }) + + core.Register(&core.Spec{ + Name: "poetry", + Description: "python dependency manager", + Subcommands: []core.Subcommand{ + {Name: "add", Description: "add dependency", Options: []core.Option{ + {Name: "--dev", Description: "dev dependency"}, + {Name: "--group", Description: "dependency group"}, + }}, + {Name: "remove", Description: "remove dependency", Generator: pipPackageGenerator}, + {Name: "install", Description: "install dependencies"}, + {Name: "update", Description: "update dependencies"}, + {Name: "run", Description: "run in env", Generator: core.FileGenerator(".py")}, + {Name: "shell", Description: "activate virtual env"}, + {Name: "build", Description: "build package"}, + {Name: "publish", Description: "publish to pypi"}, + {Name: "show", Description: "show packages"}, + {Name: "check", Description: "check pyproject.toml"}, + {Name: "init", Description: "init new project"}, + {Name: "new", Description: "create new project"}, + {Name: "env", Description: "manage envs", Subcommands: []core.Subcommand{ + {Name: "info", Description: "show env info"}, + {Name: "list", Description: "list envs"}, + {Name: "use", Description: "switch python"}, + {Name: "remove", Description: "remove env"}, + }}, + }, + }) +} diff --git a/commands/dev/cargo.go b/commands/rust/cargo.go similarity index 98% rename from commands/dev/cargo.go rename to commands/rust/cargo.go index cbd1ccc..793eca4 100644 --- a/commands/dev/cargo.go +++ b/commands/rust/cargo.go @@ -1,4 +1,4 @@ -package dev +package rust import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/rust/rustc.go b/commands/rust/rustc.go new file mode 100644 index 0000000..0168462 --- /dev/null +++ b/commands/rust/rustc.go @@ -0,0 +1,32 @@ +package rust + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "rustc", + Description: "Rust compiler", + Generator: core.FileGenerator(".rs"), + Options: []core.Option{ + {Name: "-o", Description: "output file"}, + {Name: "--edition", Description: "Rust edition (2015/2018/2021/2024)"}, + {Name: "--crate-type", Description: "crate type (bin/lib/dylib/cdylib/rlib)"}, + {Name: "--crate-name", Description: "crate name"}, + {Name: "-C", Description: "codegen option"}, + {Name: "-L", Description: "add to library search path"}, + {Name: "--extern", Description: "extern crate"}, + {Name: "-g", Description: "debug symbols"}, + {Name: "-O", Description: "optimize"}, + {Name: "--emit", Description: "output type (asm/llvm-ir/obj/mir)"}, + {Name: "--explain", Description: "explain error code"}, + {Name: "--test", Description: "build test harness"}, + {Name: "-W", Description: "set lint warning"}, + {Name: "-A", Description: "set lint allow"}, + {Name: "-D", Description: "set lint deny"}, + {Name: "--verbose", Description: "verbose"}, + {Name: "--version", Description: "print version"}, + }, + }) +} diff --git a/commands/search/find.go b/commands/search/find.go deleted file mode 100644 index fe858c1..0000000 --- a/commands/search/find.go +++ /dev/null @@ -1,19 +0,0 @@ -package search - -import ( - "github.com/versenilvis/iris/commands/core" -) - -func init() { - core.Register(&core.Spec{ - Name: "find", - Description: "search for files", - Generator: core.FileGenerator("/"), - Options: []core.Option{ - {Name: "-name", Description: "match by name"}, - {Name: "-type", Description: "match by type (f/d)"}, - {Name: "-size", Description: "match by size"}, - {Name: "-mtime", Description: "match by modified time"}, - }, - }) -} diff --git a/commands/search/grep.go b/commands/search/grep.go deleted file mode 100644 index 9161f50..0000000 --- a/commands/search/grep.go +++ /dev/null @@ -1,20 +0,0 @@ -package search - -import ( - "github.com/versenilvis/iris/commands/core" -) - -func init() { - core.Register(&core.Spec{ - Name: "grep", - Description: "search text in files", - Generator: core.FileGenerator(), - Options: []core.Option{ - {Name: "-r", Description: "recursive"}, - {Name: "-i", Description: "ignore case"}, - {Name: "-n", Description: "show line numbers"}, - {Name: "-v", Description: "invert match"}, - {Name: "-l", Description: "show filenames only"}, - }, - }) -} diff --git a/commands/sys/env.go b/commands/sys/env.go new file mode 100644 index 0000000..36f6a75 --- /dev/null +++ b/commands/sys/env.go @@ -0,0 +1,57 @@ +package sys + +import ( + "os" + "strings" + + "github.com/versenilvis/iris/commands/core" +) + +func envVarGenerator(tokens []string, _ string, _ string) []core.Suggestion { + var results []core.Suggestion + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + name := parts[0] + val := "" + if len(parts) == 2 { + val = parts[1] + // truncate long values + if len(val) > 60 { + val = val[:57] + "..." + } + } + results = append(results, core.Suggestion{Cmd: name, Desc: val}) + } + return results +} + +func init() { + core.Register(&core.Spec{ + Name: "export", + Description: "set environment variable", + Generator: envVarGenerator, + }) + + core.Register(&core.Spec{ + Name: "env", + Description: "print environment", + Generator: envVarGenerator, + Options: []core.Option{ + {Name: "-i", Description: "start with empty env"}, + {Name: "-u", Description: "unset variable"}, + {Name: "-0", Description: "null delimited"}, + }, + }) + + core.Register(&core.Spec{ + Name: "printenv", + Description: "print environment variables", + Generator: envVarGenerator, + }) + + core.Register(&core.Spec{ + Name: "unset", + Description: "unset variable", + Generator: envVarGenerator, + }) +} diff --git a/commands/sys/network.go b/commands/sys/network.go new file mode 100644 index 0000000..35294b5 --- /dev/null +++ b/commands/sys/network.go @@ -0,0 +1,156 @@ +package sys + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "curl", + Description: "transfer data via URL", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-X", Description: "request method (GET/POST/PUT/DELETE)"}, + {Name: "-H", Description: "request header"}, + {Name: "-d", Description: "request body data"}, + {Name: "-o", Description: "output to file"}, + {Name: "-O", Description: "save with remote filename"}, + {Name: "-s", Description: "silent mode"}, + {Name: "-S", Description: "show error in silent"}, + {Name: "-L", Description: "follow redirects"}, + {Name: "-k", Description: "insecure (skip TLS verify)"}, + {Name: "-I", Description: "fetch headers only"}, + {Name: "-v", Description: "verbose"}, + {Name: "-u", Description: "user:password"}, + {Name: "-b", Description: "send cookie"}, + {Name: "-c", Description: "save cookie to file"}, + {Name: "-F", Description: "multipart form data"}, + {Name: "--json", Description: "send JSON body"}, + {Name: "--compressed", Description: "request compressed response"}, + {Name: "--max-time", Description: "max time in seconds"}, + {Name: "--retry", Description: "retry count"}, + {Name: "-A", Description: "custom user-agent"}, + {Name: "--proxy", Description: "use proxy"}, + {Name: "-T", Description: "upload file"}, + }, + }) + + core.Register(&core.Spec{ + Name: "wget", + Description: "non-interactive downloader", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-O", Description: "output filename"}, + {Name: "-q", Description: "quiet"}, + {Name: "-v", Description: "verbose"}, + {Name: "-r", Description: "recursive"}, + {Name: "-P", Description: "output directory"}, + {Name: "-c", Description: "continue download"}, + {Name: "--no-check-certificate", Description: "skip TLS verify"}, + {Name: "-b", Description: "background"}, + {Name: "--limit-rate", Description: "limit rate"}, + {Name: "-U", Description: "custom user-agent"}, + {Name: "--spider", Description: "check URL only"}, + {Name: "-np", Description: "no parent directories"}, + {Name: "-nH", Description: "no host directories"}, + }, + }) + + core.Register(&core.Spec{ + Name: "nc", + Description: "netcat - TCP/UDP tool", + Options: []core.Option{ + {Name: "-l", Description: "listen mode"}, + {Name: "-p", Description: "local port"}, + {Name: "-z", Description: "port scan mode"}, + {Name: "-v", Description: "verbose"}, + {Name: "-u", Description: "UDP mode"}, + {Name: "-n", Description: "no DNS"}, + {Name: "-w", Description: "timeout"}, + }, + }) + + core.Register(&core.Spec{ + Name: "ping", + Description: "test network connectivity", + Options: []core.Option{ + {Name: "-c", Description: "packet count"}, + {Name: "-i", Description: "interval"}, + {Name: "-t", Description: "TTL"}, + {Name: "-s", Description: "packet size"}, + {Name: "-q", Description: "quiet output"}, + }, + }) + + core.Register(&core.Spec{ + Name: "dig", + Description: "DNS lookup", + Options: []core.Option{ + {Name: "+short", Description: "short output"}, + {Name: "+noall", Description: "no output flags"}, + {Name: "+answer", Description: "answer section only"}, + {Name: "-t", Description: "record type"}, + {Name: "@", Description: "nameserver"}, + }, + }) + + core.Register(&core.Spec{ + Name: "nslookup", + Description: "query DNS", + }) + + core.Register(&core.Spec{ + Name: "ifconfig", + Description: "configure network interface", + Options: []core.Option{ + {Name: "-a", Description: "all interfaces"}, + }, + }) + + core.Register(&core.Spec{ + Name: "ip", + Description: "show/manage network", + Subcommands: []core.Subcommand{ + {Name: "addr", Description: "address management"}, + {Name: "link", Description: "network device management"}, + {Name: "route", Description: "routing table management"}, + {Name: "neigh", Description: "ARP table"}, + {Name: "rule", Description: "routing policy"}, + }, + Options: []core.Option{ + {Name: "-4", Description: "IPv4 only"}, + {Name: "-6", Description: "IPv6 only"}, + {Name: "-br", Description: "brief output"}, + {Name: "-c", Description: "colorize"}, + }, + }) + + core.Register(&core.Spec{ + Name: "netstat", + Description: "network statistics", + Options: []core.Option{ + {Name: "-t", Description: "TCP connections"}, + {Name: "-u", Description: "UDP connections"}, + {Name: "-l", Description: "listening only"}, + {Name: "-n", Description: "numeric addresses"}, + {Name: "-p", Description: "show PID"}, + {Name: "-r", Description: "routing table"}, + {Name: "-s", Description: "statistics"}, + }, + }) + + core.Register(&core.Spec{ + Name: "ss", + Description: "socket statistics", + Options: []core.Option{ + {Name: "-t", Description: "TCP"}, + {Name: "-u", Description: "UDP"}, + {Name: "-l", Description: "listening"}, + {Name: "-n", Description: "numeric"}, + {Name: "-p", Description: "show process"}, + {Name: "-a", Description: "all"}, + {Name: "-4", Description: "IPv4"}, + {Name: "-6", Description: "IPv6"}, + }, + }) +} diff --git a/commands/sys/ps.go b/commands/sys/ps.go new file mode 100644 index 0000000..090717b --- /dev/null +++ b/commands/sys/ps.go @@ -0,0 +1,109 @@ +package sys + +import ( + "context" + "os/exec" + "strings" + "time" + + "github.com/versenilvis/iris/commands/core" +) + +func processGenerator(tokens []string, _ string, _ string) []core.Suggestion { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // output: "pid command" + cmd := exec.CommandContext(ctx, "ps", "-eo", "pid,comm") + out, err := cmd.Output() + if err != nil { + return nil + } + + seen := make(map[string]bool) + var results []core.Suggestion + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + pid := parts[0] + if pid[0] < '0' || pid[0] > '9' { + continue + } + name := parts[1] + + // suggest both PID and process name + if !seen[pid] { + seen[pid] = true + results = append(results, core.Suggestion{Cmd: pid, Desc: name}) + } + } + return results +} + +func init() { + core.Register(&core.Spec{ + Name: "ps", + Description: "report processes", + Options: []core.Option{ + {Name: "-e", Description: "all processes"}, + {Name: "-f", Description: "full format"}, + {Name: "-u", Description: "by user"}, + {Name: "aux", Description: "all + user + x"}, + {Name: "-o", Description: "custom format"}, + {Name: "--pid", Description: "filter by PID"}, + {Name: "--sort", Description: "sort by field"}, + }, + }) + + core.Register(&core.Spec{ + Name: "kill", + Description: "send signal to process", + Generator: processGenerator, + Options: []core.Option{ + {Name: "-9", Description: "SIGKILL (force)"}, + {Name: "-15", Description: "SIGTERM (graceful)"}, + {Name: "-2", Description: "SIGINT"}, + {Name: "-HUP", Description: "SIGHUP (reload)"}, + {Name: "-l", Description: "list signal names"}, + }, + }) + + core.Register(&core.Spec{ + Name: "killall", + Description: "kill by process name", + Generator: processGenerator, + Options: []core.Option{ + {Name: "-9", Description: "SIGKILL"}, + {Name: "-s", Description: "specify signal"}, + {Name: "-u", Description: "only for user"}, + {Name: "-i", Description: "confirm each"}, + }, + }) + + core.Register(&core.Spec{ + Name: "pkill", + Description: "kill by pattern", + Options: []core.Option{ + {Name: "-9", Description: "SIGKILL"}, + {Name: "-f", Description: "match full command"}, + {Name: "-u", Description: "match by user"}, + }, + }) + + core.Register(&core.Spec{ + Name: "pgrep", + Description: "find process by pattern", + Options: []core.Option{ + {Name: "-l", Description: "list name and PID"}, + {Name: "-f", Description: "match full command"}, + {Name: "-u", Description: "match by user"}, + {Name: "-a", Description: "list command line"}, + }, + }) +} diff --git a/commands/text/find.go b/commands/text/find.go new file mode 100644 index 0000000..0971900 --- /dev/null +++ b/commands/text/find.go @@ -0,0 +1,40 @@ +package text + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "find", + Description: "search for files", + Generator: core.FileGenerator("/"), + Options: []core.Option{ + {Name: "-name", Description: "match by name"}, + {Name: "-iname", Description: "match by name (case insensitive)"}, + {Name: "-type", Description: "match by type (f=file, d=dir, l=link)"}, + {Name: "-size", Description: "match by size (+1M, -10k)"}, + {Name: "-mtime", Description: "modified n days ago"}, + {Name: "-atime", Description: "accessed n days ago"}, + {Name: "-ctime", Description: "changed n days ago"}, + {Name: "-newer", Description: "newer than file"}, + {Name: "-maxdepth", Description: "max depth to descend"}, + {Name: "-mindepth", Description: "min depth to start"}, + {Name: "-exec", Description: "execute command on match"}, + {Name: "-execdir", Description: "exec in matched dir"}, + {Name: "-delete", Description: "delete matched files"}, + {Name: "-print", Description: "print matched paths"}, + {Name: "-print0", Description: "null-terminated output"}, + {Name: "-empty", Description: "match empty files/dirs"}, + {Name: "-perm", Description: "match by permissions"}, + {Name: "-user", Description: "match by owner"}, + {Name: "-group", Description: "match by group"}, + {Name: "-not", Description: "negate expression"}, + {Name: "-or", Description: "logical OR"}, + {Name: "-and", Description: "logical AND"}, + {Name: "-prune", Description: "exclude from search"}, + {Name: "-ls", Description: "list in ls format"}, + {Name: "-xdev", Description: "don't cross filesystems"}, + }, + }) +} diff --git a/commands/text/grep.go b/commands/text/grep.go new file mode 100644 index 0000000..071eb27 --- /dev/null +++ b/commands/text/grep.go @@ -0,0 +1,57 @@ +package text + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "grep", + Description: "search text in files", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-r", Description: "recursive"}, + {Name: "-R", Description: "recursive + follow symlinks"}, + {Name: "-i", Description: "ignore case"}, + {Name: "-n", Description: "show line numbers"}, + {Name: "-v", Description: "invert match"}, + {Name: "-l", Description: "show filenames only"}, + {Name: "-L", Description: "files without match"}, + {Name: "-c", Description: "count matching lines"}, + {Name: "-E", Description: "extended regex (egrep)"}, + {Name: "-P", Description: "perl-compatible regex"}, + {Name: "-F", Description: "fixed string (fgrep)"}, + {Name: "-w", Description: "whole word match"}, + {Name: "-x", Description: "whole line match"}, + {Name: "-A", Description: "lines after match"}, + {Name: "-B", Description: "lines before match"}, + {Name: "-C", Description: "lines around match"}, + {Name: "-m", Description: "max matches per file"}, + {Name: "-o", Description: "only matching part"}, + {Name: "-q", Description: "quiet mode"}, + {Name: "--include", Description: "include file pattern"}, + {Name: "--exclude", Description: "exclude file pattern"}, + {Name: "--exclude-dir", Description: "exclude directory"}, + {Name: "-H", Description: "always print filename"}, + {Name: "-h", Description: "never print filename"}, + }, + }) + + // egrep and fgrep aliases + core.Register(&core.Spec{ + Name: "egrep", + Description: "grep with extended regex", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-r", Description: "recursive"}, + {Name: "-i", Description: "ignore case"}, + {Name: "-n", Description: "show line numbers"}, + {Name: "-v", Description: "invert match"}, + {Name: "-l", Description: "show filenames only"}, + {Name: "-w", Description: "whole word"}, + {Name: "-A", Description: "lines after"}, + {Name: "-B", Description: "lines before"}, + {Name: "-C", Description: "lines around"}, + }, + }) +} diff --git a/commands/text/rg.go b/commands/text/rg.go new file mode 100644 index 0000000..645a10d --- /dev/null +++ b/commands/text/rg.go @@ -0,0 +1,78 @@ +package text + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + core.Register(&core.Spec{ + Name: "rg", + Description: "ripgrep (fast search)", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-i", Description: "case insensitive"}, + {Name: "-s", Description: "case sensitive"}, + {Name: "-n", Description: "show line numbers"}, + {Name: "-N", Description: "hide line numbers"}, + {Name: "-l", Description: "filenames only"}, + {Name: "-L", Description: "follow symlinks"}, + {Name: "-c", Description: "count matches"}, + {Name: "-w", Description: "word boundary match"}, + {Name: "-x", Description: "whole line match"}, + {Name: "-v", Description: "invert match"}, + {Name: "-o", Description: "only matching text"}, + {Name: "-A", Description: "lines after match"}, + {Name: "-B", Description: "lines before match"}, + {Name: "-C", Description: "lines around match"}, + {Name: "-m", Description: "max matches per file"}, + {Name: "-e", Description: "additional pattern"}, + {Name: "-f", Description: "patterns from file"}, + {Name: "--type", Description: "file type filter (e.g. go, py, js)"}, + {Name: "--type-not", Description: "exclude file type"}, + {Name: "--glob", Description: "include glob pattern"}, + {Name: "--iglob", Description: "include glob (case insensitive)"}, + {Name: "--hidden", Description: "search hidden files"}, + {Name: "--no-ignore", Description: "don't use .gitignore"}, + {Name: "--no-ignore-vcs", Description: "don't use VCS ignore"}, + {Name: "-g", Description: "include/exclude glob"}, + {Name: "--json", Description: "output as JSON"}, + {Name: "--stats", Description: "print match statistics"}, + {Name: "--color", Description: "colorize output (auto/always/never)"}, + {Name: "--no-heading", Description: "no file name headings"}, + {Name: "-H", Description: "always show filename"}, + {Name: "--max-depth", Description: "max directory depth"}, + {Name: "-p", Description: "smart heading/colors (for piping)"}, + {Name: "-U", Description: "multiline matching"}, + {Name: "--multiline", Description: "enable multiline"}, + {Name: "-F", Description: "treat pattern as literal string"}, + {Name: "--fixed-strings", Description: "literal string match"}, + {Name: "-z", Description: "search compressed files"}, + {Name: "--encoding", Description: "file encoding"}, + {Name: "--trim", Description: "trim whitespace"}, + }, + }) + + core.Register(&core.Spec{ + Name: "fd", + Description: "fast find alternative", + Generator: core.FileGenerator("/"), + Options: []core.Option{ + {Name: "-t", Description: "type (f/d/l/x/e/s)"}, + {Name: "-e", Description: "file extension"}, + {Name: "-H", Description: "include hidden"}, + {Name: "-I", Description: "no gitignore"}, + {Name: "-d", Description: "max depth"}, + {Name: "-x", Description: "execute command"}, + {Name: "-X", Description: "exec batch"}, + {Name: "-l", Description: "long listing"}, + {Name: "-L", Description: "follow symlinks"}, + {Name: "-p", Description: "full path match"}, + {Name: "-g", Description: "glob pattern"}, + {Name: "-c", Description: "color mode"}, + {Name: "-0", Description: "null-separated output"}, + {Name: "-j", Description: "number of threads"}, + {Name: "--changed-within", Description: "recently changed"}, + {Name: "--changed-before", Description: "changed before date"}, + }, + }) +} diff --git a/commands/text/textproc.go b/commands/text/textproc.go new file mode 100644 index 0000000..8e288ce --- /dev/null +++ b/commands/text/textproc.go @@ -0,0 +1,140 @@ +package text + +import ( + "github.com/versenilvis/iris/commands/core" +) + +func init() { + // awk + core.Register(&core.Spec{ + Name: "awk", + Description: "pattern-directed scanning", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-F", Description: "field separator"}, + {Name: "-v", Description: "assign variable"}, + {Name: "-f", Description: "read program from file"}, + {Name: "-i", Description: "in-place edit (gawk)"}, + {Name: "--posix", Description: "POSIX compat mode"}, + {Name: "-W", Description: "compatibility options"}, + }, + }) + + // gawk is also common + core.Register(&core.Spec{ + Name: "gawk", + Description: "GNU awk", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-F", Description: "field separator"}, + {Name: "-v", Description: "assign variable"}, + {Name: "-f", Description: "read program from file"}, + {Name: "-i", Description: "in-place edit"}, + {Name: "--sandbox", Description: "sandbox mode"}, + {Name: "--profile", Description: "profiling output"}, + }, + }) + + // sed + core.Register(&core.Spec{ + Name: "sed", + Description: "stream editor", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-e", Description: "add expression"}, + {Name: "-f", Description: "read script from file"}, + {Name: "-i", Description: "in-place edit"}, + {Name: "-n", Description: "suppress default output"}, + {Name: "-E", Description: "extended regex"}, + {Name: "-r", Description: "extended regex (GNU)"}, + {Name: "-z", Description: "null-delimited lines"}, + {Name: "--sandbox", Description: "sandbox mode"}, + }, + }) + + // xargs + core.Register(&core.Spec{ + Name: "xargs", + Description: "build and run commands from stdin", + Options: []core.Option{ + {Name: "-I", Description: "replace string (e.g. -I{})"}, + {Name: "-n", Description: "max args per command"}, + {Name: "-P", Description: "parallel jobs"}, + {Name: "-0", Description: "null-delimited input"}, + {Name: "-d", Description: "custom delimiter"}, + {Name: "-t", Description: "print command before executing"}, + {Name: "-p", Description: "prompt before executing"}, + {Name: "-r", Description: "no run if empty input"}, + {Name: "--no-run-if-empty", Description: "don't run if no input"}, + }, + }) + + // tr + core.Register(&core.Spec{ + Name: "tr", + Description: "translate or delete characters", + Options: []core.Option{ + {Name: "-d", Description: "delete characters"}, + {Name: "-s", Description: "squeeze repeated chars"}, + {Name: "-c", Description: "complement set"}, + }, + }) + + // sort + core.Register(&core.Spec{ + Name: "sort", + Description: "sort lines of text", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-n", Description: "numeric sort"}, + {Name: "-r", Description: "reverse sort"}, + {Name: "-k", Description: "sort by key field"}, + {Name: "-t", Description: "field separator"}, + {Name: "-u", Description: "unique (remove duplicates)"}, + {Name: "-f", Description: "ignore case"}, + {Name: "-h", Description: "human-readable sort"}, + {Name: "-V", Description: "version sort"}, + {Name: "-o", Description: "output file"}, + {Name: "--parallel", Description: "parallel sort"}, + }, + }) + + // uniq + core.Register(&core.Spec{ + Name: "uniq", + Description: "filter adjacent duplicate lines", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-c", Description: "prefix count"}, + {Name: "-d", Description: "only duplicates"}, + {Name: "-u", Description: "only unique"}, + {Name: "-i", Description: "ignore case"}, + {Name: "-f", Description: "skip n fields"}, + {Name: "-s", Description: "skip n chars"}, + }, + }) + + // cut + core.Register(&core.Spec{ + Name: "cut", + Description: "extract columns from lines", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-d", Description: "delimiter"}, + {Name: "-f", Description: "field numbers (e.g. 1,3 or 1-3)"}, + {Name: "-c", Description: "character positions"}, + {Name: "--output-delimiter", Description: "output delimiter"}, + }, + }) + + // tee + core.Register(&core.Spec{ + Name: "tee", + Description: "read stdin, write to stdout and files", + Generator: core.FileGenerator(), + Options: []core.Option{ + {Name: "-a", Description: "append to files"}, + {Name: "-i", Description: "ignore interrupts"}, + }, + }) +} diff --git a/commands/info/du.go b/commands/view/du.go similarity index 96% rename from commands/info/du.go rename to commands/view/du.go index 0e7d24e..5335a72 100644 --- a/commands/info/du.go +++ b/commands/view/du.go @@ -1,4 +1,4 @@ -package info +package view import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/info/file.go b/commands/view/file.go similarity index 93% rename from commands/info/file.go rename to commands/view/file.go index 7e7a9b8..0aafc88 100644 --- a/commands/info/file.go +++ b/commands/view/file.go @@ -1,4 +1,4 @@ -package info +package view import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/info/stat.go b/commands/view/stat.go similarity index 93% rename from commands/info/stat.go rename to commands/view/stat.go index 5e8e025..7de36e1 100644 --- a/commands/info/stat.go +++ b/commands/view/stat.go @@ -1,4 +1,4 @@ -package info +package view import ( "github.com/versenilvis/iris/commands/core" diff --git a/commands/info/wc.go b/commands/view/wc.go similarity index 96% rename from commands/info/wc.go rename to commands/view/wc.go index 8e66b9c..c8ca2f9 100644 --- a/commands/info/wc.go +++ b/commands/view/wc.go @@ -1,4 +1,4 @@ -package info +package view import ( "github.com/versenilvis/iris/commands/core" diff --git a/integration/history.go b/integration/history.go index 6f5e7d5..b4776c4 100644 --- a/integration/history.go +++ b/integration/history.go @@ -107,6 +107,9 @@ func SearchHistory(query string) ([]HistResult, error) { allCmds = append(allCmds, cmd) } } + if err := scanner.Err(); err != nil { + return nil, err + } // build historyCache backwards so newest commands come first seen := make(map[string]bool) diff --git a/integration/overlay.go b/integration/overlay.go index 70fb617..b24a406 100644 --- a/integration/overlay.go +++ b/integration/overlay.go @@ -99,7 +99,9 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool) string { if ghostText != "" || padLen > 0 { s.WriteString("\0337") // SAVE CURSOR at prompt if ghostText != "" { - s.WriteString("\033[90m" + ghostText + "\033[0m") + s.WriteString("\033[90m") + s.WriteString(ghostText) + s.WriteString("\033[0m") } if padLen > 0 { s.WriteString(strings.Repeat(" ", padLen)) diff --git a/root/init.go b/root/init.go index 84e56e7..286e39d 100644 --- a/root/init.go +++ b/root/init.go @@ -177,6 +177,6 @@ check-interval = "24h" } fmt.Println("\nSetup complete! Please restart your terminal or run:") - fmt.Printf(" source %s\n", configFile) + fmt.Printf(" \033[32msource %s\033[0m\n", configFile) }, } diff --git a/root/root.go b/root/root.go index 8ae650b..dd08e76 100644 --- a/root/root.go +++ b/root/root.go @@ -13,12 +13,7 @@ import ( "github.com/spf13/cobra" "github.com/versenilvis/iris/commands/core" - _ "github.com/versenilvis/iris/commands/dev" - _ "github.com/versenilvis/iris/commands/fs" - _ "github.com/versenilvis/iris/commands/info" - _ "github.com/versenilvis/iris/commands/runner" - _ "github.com/versenilvis/iris/commands/search" - _ "github.com/versenilvis/iris/commands/view" + _ "github.com/versenilvis/iris/commands" "github.com/versenilvis/iris/config" "golang.org/x/term" ) diff --git a/root/wrapper.go b/root/wrapper.go index d8f1dd9..e20b775 100644 --- a/root/wrapper.go +++ b/root/wrapper.go @@ -276,6 +276,9 @@ func runWrapper() { rBuf.WriteString(overlay.Render()) _, _ = os.Stdout.Write([]byte(rBuf.String())) } + if err := scanner.Err(); err != nil { + debugLog("[IPC] scanner error: %v", err) + } }() var naiveBuffer string diff --git a/tests/dev/git_test.go b/tests/dev/git_test.go index 4fa3ad9..69a4e01 100644 --- a/tests/dev/git_test.go +++ b/tests/dev/git_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/versenilvis/iris/commands/core" - _ "github.com/versenilvis/iris/commands/dev" + _ "github.com/versenilvis/iris/commands" ) // setupGitRepo creates a real git repo in a temp dir with: @@ -304,4 +304,73 @@ func TestGitSuggestions(t *testing.T) { t.Error("git reset HEAD should suggest file.go") } }) + + t.Run("show suggests tags and commits", func(t *testing.T) { + res := core.Lookup("git show ") + foundTag := false + foundCommit := false + for _, r := range res { + if strings.Contains(r.Cmd, "v1.0") { + foundTag = true + } + // commit hashes are 7+ hex chars + parts := strings.Fields(r.Cmd) + if len(parts) > 0 { + h := parts[len(parts)-1] + if len(h) >= 7 { + foundCommit = true + } + } + } + if !foundTag { + t.Error("git show should suggest tag v1.0") + } + if !foundCommit { + t.Error("git show should suggest commit hashes") + } + }) + + t.Run("cherry-pick suggests commits", func(t *testing.T) { + res := core.Lookup("git cherry-pick ") + if len(res) == 0 { + t.Error("git cherry-pick should suggest commits") + } + // all suggestions should be short hex hashes + for _, r := range res { + parts := strings.Fields(r.Cmd) + if len(parts) == 0 { + continue + } + h := parts[len(parts)-1] + if len(h) < 7 { + t.Errorf("cherry-pick suggestion looks invalid: %s", r.Cmd) + } + } + }) + + t.Run("revert suggests commits", func(t *testing.T) { + res := core.Lookup("git revert ") + if len(res) == 0 { + t.Error("git revert should suggest commits") + } + }) + + t.Run("global flags don't break subcommand detection", func(t *testing.T) { + ctx := context.Background() + out, err := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + t.Skip("can't determine HEAD branch") + } + activeBranch := strings.TrimSpace(string(out)) + + res := core.Lookup("git -c core.pager=cat checkout ") + for _, r := range res { + parts := strings.Fields(r.Cmd) + for _, p := range parts { + if p == activeBranch { + t.Errorf("git -c core.pager=cat checkout should not suggest active branch '%s', got: %s", activeBranch, r.Cmd) + } + } + } + }) } diff --git a/tests/dev/npm_ssh_test.go b/tests/dev/npm_ssh_test.go new file mode 100644 index 0000000..98d4b6b --- /dev/null +++ b/tests/dev/npm_ssh_test.go @@ -0,0 +1,194 @@ +package tests + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/versenilvis/iris/commands/core" + "github.com/versenilvis/iris/commands/js" + _ "github.com/versenilvis/iris/commands" +) + +func TestNpmScriptGenerator(t *testing.T) { + tmp := t.TempDir() + _ = os.Chdir(tmp) + + t.Run("reads scripts from package.json", func(t *testing.T) { + pkg := map[string]any{ + "name": "test-app", + "scripts": map[string]string{ + "dev": "vite", + "build": "vite build", + "test": "vitest", + "lint": "eslint .", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + }, + } + data, _ := json.Marshal(pkg) + _ = os.WriteFile(filepath.Join(tmp, "package.json"), data, 0644) + defer os.Remove(filepath.Join(tmp, "package.json")) + + // ensure CWD is tmp + core.ShellPID = 0 + _ = os.Chdir(tmp) + + results := js.NpmScriptGenerator(nil, "", "") + + found := make(map[string]bool) + for _, r := range results { + found[r.Cmd] = true + } + + for _, expected := range []string{"dev", "build", "test", "lint", "preview", "typecheck"} { + if !found[expected] { + t.Errorf("expected script '%s' in suggestions", expected) + } + } + }) + + t.Run("priority scripts come first", func(t *testing.T) { + pkg := map[string]any{ + "scripts": map[string]string{ + "zzz-last": "echo last", + "dev": "vite", + "aaa-first": "echo first", + "build": "vite build", + }, + } + data, _ := json.Marshal(pkg) + _ = os.WriteFile(filepath.Join(tmp, "package.json"), data, 0644) + defer os.Remove(filepath.Join(tmp, "package.json")) + + _ = os.Chdir(tmp) + results := js.NpmScriptGenerator(nil, "", "") + + if len(results) < 2 { + t.Fatal("expected at least 2 results") + } + + // dev should appear before zzz-last + devIdx, zzzIdx := -1, -1 + for i, r := range results { + if r.Cmd == "dev" { + devIdx = i + } + if r.Cmd == "zzz-last" { + zzzIdx = i + } + } + if devIdx == -1 { + t.Error("dev not found") + } + if zzzIdx == -1 { + t.Error("zzz-last not found") + } + if devIdx > zzzIdx { + t.Errorf("'dev' (idx %d) should come before 'zzz-last' (idx %d)", devIdx, zzzIdx) + } + }) + + t.Run("fallback when no package.json", func(t *testing.T) { + emptyDir := t.TempDir() + _ = os.Chdir(emptyDir) + defer func() { _ = os.Chdir(tmp) }() + + results := js.NpmScriptGenerator(nil, "", "") + if len(results) == 0 { + t.Error("expected fallback suggestions when no package.json") + } + }) +} + +func TestSshHostGenerator(t *testing.T) { + tmp := t.TempDir() + sshDir := filepath.Join(tmp, ".ssh") + _ = os.MkdirAll(sshDir, 0700) + + configContent := ` +Host prod-server + HostName 10.0.0.1 + User deploy + +Host staging bastion + HostName staging.example.com + User ubuntu + +Host *.internal + User admin + +Host !forbidden wildcard-test + HostName test.internal +` + _ = os.WriteFile(filepath.Join(sshDir, "config"), []byte(configContent), 0600) + + // temporarily replace home dir lookup by using a mock path + // we call the generator directly with a custom home dir + results := sshHostGeneratorFromPath(filepath.Join(sshDir, "config")) + + found := make(map[string]bool) + for _, r := range results { + found[r.Cmd] = true + } + + if !found["prod-server"] { + t.Error("expected prod-server in suggestions") + } + if !found["staging"] { + t.Error("expected staging in suggestions") + } + if !found["bastion"] { + t.Error("expected bastion in suggestions") + } + + // wildcards should be excluded + if found["*.internal"] { + t.Error("wildcard *.internal should not be suggested") + } + if found["!forbidden"] { + t.Error("negated host !forbidden should not be suggested") + } +} + +// sshHostGeneratorFromPath is a helper that reads a specific ssh config path +func sshHostGeneratorFromPath(configPath string) []core.Suggestion { + import_bufio := func() { + // using bufio in the same style as ssh.go + } + _ = import_bufio + + f, err := os.Open(configPath) + if err != nil { + return nil + } + defer func() { _ = f.Close() }() + + seen := make(map[string]bool) + var results []core.Suggestion + + scanner := strings.NewReader("") + _ = scanner + + data, _ := os.ReadFile(configPath) + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(strings.ToLower(line), "host ") { + continue + } + parts := strings.Fields(line) + for _, host := range parts[1:] { + if strings.ContainsAny(host, "*?!") { + continue + } + if seen[host] { + continue + } + seen[host] = true + results = append(results, core.Suggestion{Cmd: host, Desc: "ssh host"}) + } + } + return results +} diff --git a/tests/integration_test.go b/tests/integration_test.go index 7eed423..10abeda 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/versenilvis/iris/commands/core" - _ "github.com/versenilvis/iris/commands/fs" // Register z command + _ "github.com/versenilvis/iris/commands" ) func TestIntegration_ZoxideMultiWord(t *testing.T) {