@fatalmistake02/rian 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/THIRD_PARTY_NOTICES.txt +673 -0
- package/bin/rian.cjs +12 -0
- package/cmd/rian/main.go +17 -0
- package/dist/binaries/darwin-arm64/rian +0 -0
- package/dist/binaries/darwin-x64/rian +0 -0
- package/dist/binaries/linux-arm64/rian +0 -0
- package/dist/binaries/linux-x64/rian +0 -0
- package/dist/binaries/manifest.json +29 -0
- package/dist/binaries/win32-arm64/rian.exe +0 -0
- package/dist/binaries/win32-x64/rian.exe +0 -0
- package/go.mod +28 -0
- package/go.sum +44 -0
- package/internal/app/app.go +298 -0
- package/internal/app/app_test.go +85 -0
- package/internal/config/config.go +189 -0
- package/internal/config/config_test.go +101 -0
- package/internal/detect/detect.go +252 -0
- package/internal/detect/detect_test.go +86 -0
- package/internal/git/git.go +166 -0
- package/internal/git/git_test.go +73 -0
- package/internal/git/window_unix.go +7 -0
- package/internal/git/window_windows.go +8 -0
- package/internal/process/platform_unix.go +19 -0
- package/internal/process/platform_windows.go +25 -0
- package/internal/process/process.go +172 -0
- package/internal/process/process_test.go +137 -0
- package/internal/project/project.go +180 -0
- package/internal/project/project_test.go +71 -0
- package/internal/tools/lookup.go +41 -0
- package/internal/tools/platform_unix.go +34 -0
- package/internal/tools/platform_windows.go +52 -0
- package/internal/tools/platform_windows_test.go +29 -0
- package/internal/tools/tools.go +152 -0
- package/internal/tools/tools_test.go +56 -0
- package/internal/ui/actions.go +259 -0
- package/internal/ui/forms.go +156 -0
- package/internal/ui/model.go +220 -0
- package/internal/ui/styles.go +85 -0
- package/internal/ui/ui_test.go +170 -0
- package/internal/ui/update.go +470 -0
- package/internal/ui/view.go +459 -0
- package/lib/launcher.cjs +46 -0
- package/lib/platforms.cjs +20 -0
- package/package.json +39 -4
- package/scripts/build-binaries.cjs +60 -0
- package/scripts/check-package.cjs +49 -0
- package/scripts/ensure-binary.cjs +11 -0
- package/test/npm/launcher.test.cjs +45 -0
- package/test/npm/package.test.cjs +26 -0
package/cmd/rian/main.go
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
"rian/internal/app"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var version = "0.2.1"
|
|
11
|
+
|
|
12
|
+
func main() {
|
|
13
|
+
if err := app.Run(os.Args[1:], os.Stdin, os.Stdout, version); err != nil {
|
|
14
|
+
fmt.Fprintln(os.Stderr, "rian:", err)
|
|
15
|
+
os.Exit(1)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.1",
|
|
3
|
+
"targets": {
|
|
4
|
+
"darwin-arm64": {
|
|
5
|
+
"file": "darwin-arm64/rian",
|
|
6
|
+
"sha256": "a087c777ed82a10a6e3bb206c9db149baf917e9d8a288922c8902e5ceb83a624"
|
|
7
|
+
},
|
|
8
|
+
"darwin-x64": {
|
|
9
|
+
"file": "darwin-x64/rian",
|
|
10
|
+
"sha256": "feff77cfcb3c2525960813da4ef02cbc6652ebc8efe720d8857fd9627484b2de"
|
|
11
|
+
},
|
|
12
|
+
"linux-arm64": {
|
|
13
|
+
"file": "linux-arm64/rian",
|
|
14
|
+
"sha256": "ae1eac7b9aef670bce87407abdd99b3ca40522e6dba679491eb3cc1959b557aa"
|
|
15
|
+
},
|
|
16
|
+
"linux-x64": {
|
|
17
|
+
"file": "linux-x64/rian",
|
|
18
|
+
"sha256": "8804e1c1b55ddfc5d7e5ae3d6713660960d988480f896b4f95184277c9f1dc5b"
|
|
19
|
+
},
|
|
20
|
+
"win32-arm64": {
|
|
21
|
+
"file": "win32-arm64/rian.exe",
|
|
22
|
+
"sha256": "fdfc1e55be0d758beaf53eaffe4905f09a2dc9f703e693af165b5638ad024476"
|
|
23
|
+
},
|
|
24
|
+
"win32-x64": {
|
|
25
|
+
"file": "win32-x64/rian.exe",
|
|
26
|
+
"sha256": "d54373082f157a55aec87c127b1f769b62cccd1e55aa375ccfa9ddb72ce36687"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
Binary file
|
|
Binary file
|
package/go.mod
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module rian
|
|
2
|
+
|
|
3
|
+
go 1.26.0
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
charm.land/bubbles/v2 v2.2.1
|
|
7
|
+
charm.land/bubbletea/v2 v2.0.10
|
|
8
|
+
charm.land/lipgloss/v2 v2.0.6
|
|
9
|
+
github.com/charmbracelet/x/ansi v0.11.8
|
|
10
|
+
github.com/charmbracelet/x/term v0.2.2
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
require (
|
|
14
|
+
github.com/atotto/clipboard v0.1.4 // indirect
|
|
15
|
+
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
|
16
|
+
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
|
|
17
|
+
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
|
18
|
+
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
|
19
|
+
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
|
20
|
+
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
|
21
|
+
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
|
22
|
+
github.com/mattn/go-runewidth v0.0.27 // indirect
|
|
23
|
+
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
24
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
25
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
26
|
+
golang.org/x/sync v0.22.0 // indirect
|
|
27
|
+
golang.org/x/sys v0.47.0 // indirect
|
|
28
|
+
)
|
package/go.sum
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU=
|
|
2
|
+
charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc=
|
|
3
|
+
charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o=
|
|
4
|
+
charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc=
|
|
5
|
+
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
|
6
|
+
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
|
7
|
+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
|
8
|
+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
|
9
|
+
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
|
10
|
+
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
|
11
|
+
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
|
12
|
+
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
|
13
|
+
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
|
|
14
|
+
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
|
|
15
|
+
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
|
16
|
+
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
|
17
|
+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
|
18
|
+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
|
19
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
20
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
21
|
+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
22
|
+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
23
|
+
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
|
24
|
+
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
|
25
|
+
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
|
26
|
+
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
|
27
|
+
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
|
28
|
+
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
|
29
|
+
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
|
30
|
+
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
31
|
+
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
|
|
32
|
+
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
|
33
|
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
34
|
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
35
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
36
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
37
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
38
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
39
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
40
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
41
|
+
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
|
42
|
+
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
43
|
+
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
|
44
|
+
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Package app dispatches the small CLI and creates the terminal application.
|
|
2
|
+
package app
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"bufio"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"log"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strings"
|
|
14
|
+
"text/tabwriter"
|
|
15
|
+
|
|
16
|
+
tea "charm.land/bubbletea/v2"
|
|
17
|
+
"github.com/charmbracelet/x/term"
|
|
18
|
+
|
|
19
|
+
"rian/internal/config"
|
|
20
|
+
"rian/internal/detect"
|
|
21
|
+
"rian/internal/git"
|
|
22
|
+
"rian/internal/process"
|
|
23
|
+
"rian/internal/project"
|
|
24
|
+
"rian/internal/tools"
|
|
25
|
+
"rian/internal/ui"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
func Run(args []string, in *os.File, out io.Writer, version string) error {
|
|
29
|
+
if len(args) > 0 && (args[0] == "--version" || args[0] == "version") {
|
|
30
|
+
fmt.Fprintln(out, "rian "+version)
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
33
|
+
if len(args) > 0 && (args[0] == "--help" || args[0] == "-h" || args[0] == "help") {
|
|
34
|
+
fmt.Fprint(out, usage)
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
path, err := config.DefaultPath()
|
|
38
|
+
if err != nil {
|
|
39
|
+
return err
|
|
40
|
+
}
|
|
41
|
+
store := config.New(path)
|
|
42
|
+
if len(args) == 0 {
|
|
43
|
+
if !term.IsTerminal(in.Fd()) {
|
|
44
|
+
return fmt.Errorf("the interface needs an interactive terminal; use 'rian list' or 'rian --help'")
|
|
45
|
+
}
|
|
46
|
+
logger, closeLog := openLog(filepath.Dir(path))
|
|
47
|
+
defer closeLog()
|
|
48
|
+
manager := process.New()
|
|
49
|
+
defer manager.Close()
|
|
50
|
+
_, err := tea.NewProgram(ui.New(store, manager, logger), tea.WithInput(in), tea.WithOutput(out)).Run()
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
if args[0] == "config" {
|
|
54
|
+
fmt.Fprintln(out, path)
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
cfg, err := store.Load()
|
|
58
|
+
if err != nil {
|
|
59
|
+
return err
|
|
60
|
+
}
|
|
61
|
+
switch args[0] {
|
|
62
|
+
case "add":
|
|
63
|
+
return addProject(store, args[1:], in, out)
|
|
64
|
+
case "list":
|
|
65
|
+
if len(args) == 2 && args[1] == "--json" {
|
|
66
|
+
return json.NewEncoder(out).Encode(cfg.Projects)
|
|
67
|
+
}
|
|
68
|
+
if len(args) != 1 {
|
|
69
|
+
return fmt.Errorf("usage: rian list [--json]")
|
|
70
|
+
}
|
|
71
|
+
if len(cfg.Projects) == 0 {
|
|
72
|
+
fmt.Fprintln(out, "No projects yet. Add one with: rian add .")
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
w := tabwriter.NewWriter(out, 0, 4, 3, ' ', 0)
|
|
76
|
+
fmt.Fprintln(w, "NAME\tID\tDIRECTORY")
|
|
77
|
+
for _, p := range cfg.Projects {
|
|
78
|
+
name := safe(p.Name)
|
|
79
|
+
if p.Favorite {
|
|
80
|
+
name = "★ " + name
|
|
81
|
+
}
|
|
82
|
+
fmt.Fprintf(w, "%s\t%s\t%s\n", name, p.ID, safe(p.Path))
|
|
83
|
+
}
|
|
84
|
+
return w.Flush()
|
|
85
|
+
case "remove":
|
|
86
|
+
args, yes := confirmationFlag(args[1:])
|
|
87
|
+
if len(args) != 1 {
|
|
88
|
+
return fmt.Errorf("usage: rian remove <project> [--yes]")
|
|
89
|
+
}
|
|
90
|
+
p, err := project.Lookup(cfg.Projects, args[0])
|
|
91
|
+
if err != nil {
|
|
92
|
+
return err
|
|
93
|
+
}
|
|
94
|
+
fmt.Fprintf(out, "Remove %s from Rian? Its directory and files stay in place.\n", safe(p.Name))
|
|
95
|
+
if err := confirm(yes, in, out); err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
if _, err := store.Update(func(cfg *config.Config) error { return config.Remove(cfg, p.ID) }); err != nil {
|
|
99
|
+
return err
|
|
100
|
+
}
|
|
101
|
+
fmt.Fprintln(out, "Project entry removed.")
|
|
102
|
+
return nil
|
|
103
|
+
case "open", "vscode", "terminal", "github", "opencode", "kilocode", "chatgpt", "run":
|
|
104
|
+
if len(args) < 2 {
|
|
105
|
+
return fmt.Errorf("usage: rian %s <project>", args[0])
|
|
106
|
+
}
|
|
107
|
+
if args[0] != "run" && len(args) != 2 || args[0] == "run" && len(args) != 3 {
|
|
108
|
+
return fmt.Errorf("unexpected arguments; see rian --help")
|
|
109
|
+
}
|
|
110
|
+
p, err := project.Lookup(cfg.Projects, args[1])
|
|
111
|
+
if err != nil {
|
|
112
|
+
return err
|
|
113
|
+
}
|
|
114
|
+
return launch(store, p, args, in, out)
|
|
115
|
+
default:
|
|
116
|
+
return fmt.Errorf("unknown command %q; see rian --help", args[0])
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func addProject(store *config.Store, args []string, in *os.File, out io.Writer) error {
|
|
121
|
+
args, yes := confirmationFlag(args)
|
|
122
|
+
if len(args) != 1 {
|
|
123
|
+
return fmt.Errorf("usage: rian add <directory> [--yes]")
|
|
124
|
+
}
|
|
125
|
+
result, err := detect.Inspect(args[0])
|
|
126
|
+
if err != nil {
|
|
127
|
+
return err
|
|
128
|
+
}
|
|
129
|
+
cfg, err := store.Load()
|
|
130
|
+
if err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
for _, p := range cfg.Projects {
|
|
134
|
+
if project.SamePath(p.Path, result.Project.Path) {
|
|
135
|
+
return fmt.Errorf("directory already saved as %s", p.Name)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
p := result.Project
|
|
139
|
+
fmt.Fprintf(out, "\n%s\n %s\n", safe(p.Name), safe(p.Path))
|
|
140
|
+
if len(p.Stack) > 0 {
|
|
141
|
+
fmt.Fprintln(out, " Stack: "+strings.Join(p.Stack, " · "))
|
|
142
|
+
}
|
|
143
|
+
if result.Git.Repository {
|
|
144
|
+
fmt.Fprintln(out, " Branch: "+safe(result.Git.Branch))
|
|
145
|
+
}
|
|
146
|
+
if result.Git.GitHub != "" {
|
|
147
|
+
fmt.Fprintln(out, " GitHub: "+safe(result.Git.GitHub))
|
|
148
|
+
}
|
|
149
|
+
for _, name := range detect.CommandNames(p.AutoCommands) {
|
|
150
|
+
fmt.Fprintf(out, " %-12s %s\n", safe(name), safe(p.AutoCommands[name]))
|
|
151
|
+
}
|
|
152
|
+
for _, tool := range []string{"code", "opencode", "kilocode", "gh"} {
|
|
153
|
+
status := "not in PATH"
|
|
154
|
+
if result.Available[tool] {
|
|
155
|
+
status = "available"
|
|
156
|
+
}
|
|
157
|
+
fmt.Fprintf(out, " %-12s %s\n", tool, status)
|
|
158
|
+
}
|
|
159
|
+
for _, warning := range result.Warnings {
|
|
160
|
+
fmt.Fprintln(out, " "+safe(warning))
|
|
161
|
+
}
|
|
162
|
+
fmt.Fprintln(out, "\nSave this project?")
|
|
163
|
+
if err := confirm(yes, in, out); err != nil {
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
if _, err := store.Update(func(cfg *config.Config) error { return config.Add(cfg, p) }); err != nil {
|
|
167
|
+
return err
|
|
168
|
+
}
|
|
169
|
+
fmt.Fprintf(out, "Saved %s [%s].\n", safe(p.Name), p.ID)
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func confirmationFlag(args []string) ([]string, bool) {
|
|
174
|
+
result, yes := []string{}, false
|
|
175
|
+
for _, arg := range args {
|
|
176
|
+
if arg == "--yes" || arg == "-y" {
|
|
177
|
+
yes = true
|
|
178
|
+
} else {
|
|
179
|
+
result = append(result, arg)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result, yes
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func confirm(yes bool, in *os.File, out io.Writer) error {
|
|
186
|
+
if yes {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
if !term.IsTerminal(in.Fd()) {
|
|
190
|
+
return fmt.Errorf("confirmation requires a terminal; pass --yes after reviewing the detected project")
|
|
191
|
+
}
|
|
192
|
+
fmt.Fprint(out, "[y/N] ")
|
|
193
|
+
answer, err := bufio.NewReader(in).ReadString('\n')
|
|
194
|
+
if err != nil {
|
|
195
|
+
return err
|
|
196
|
+
}
|
|
197
|
+
if strings.EqualFold(strings.TrimSpace(answer), "y") || strings.EqualFold(strings.TrimSpace(answer), "yes") {
|
|
198
|
+
return nil
|
|
199
|
+
}
|
|
200
|
+
return fmt.Errorf("cancelled")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func launch(store *config.Store, p project.Project, args []string, in *os.File, out io.Writer) error {
|
|
204
|
+
name := args[0]
|
|
205
|
+
if name == "open" {
|
|
206
|
+
name = "vscode"
|
|
207
|
+
}
|
|
208
|
+
if !p.ToolEnabled(name) {
|
|
209
|
+
return fmt.Errorf("%s is disabled for this project", name)
|
|
210
|
+
}
|
|
211
|
+
launcher := tools.System{}
|
|
212
|
+
var err error
|
|
213
|
+
var cmd *exec.Cmd
|
|
214
|
+
switch name {
|
|
215
|
+
case "vscode":
|
|
216
|
+
err = launcher.OpenEditor(p)
|
|
217
|
+
case "chatgpt":
|
|
218
|
+
err = launcher.OpenChatGPT()
|
|
219
|
+
case "github":
|
|
220
|
+
var value string
|
|
221
|
+
value, err = tools.GitHubURL(p, git.Inspect(p.Path))
|
|
222
|
+
if err == nil {
|
|
223
|
+
err = launcher.OpenURL(value)
|
|
224
|
+
}
|
|
225
|
+
case "run":
|
|
226
|
+
result, detectErr := detect.Inspect(p.Path)
|
|
227
|
+
if detectErr != nil {
|
|
228
|
+
return detectErr
|
|
229
|
+
}
|
|
230
|
+
p.AutoCommands = result.Project.AutoCommands
|
|
231
|
+
commands := p.AllCommands()
|
|
232
|
+
command, exists := commands[args[2]]
|
|
233
|
+
if !exists {
|
|
234
|
+
return fmt.Errorf("command %q not found; available: %s", args[2], strings.Join(detect.CommandNames(commands), ", "))
|
|
235
|
+
}
|
|
236
|
+
fmt.Fprintln(out, "$ "+safe(command))
|
|
237
|
+
cmd = tools.Shell(p.Path, command)
|
|
238
|
+
default:
|
|
239
|
+
cmd, err = launcher.Interactive(name, p)
|
|
240
|
+
}
|
|
241
|
+
if err != nil {
|
|
242
|
+
return err
|
|
243
|
+
}
|
|
244
|
+
if _, err := store.Update(func(cfg *config.Config) error { return config.Touch(cfg, p.ID) }); err != nil {
|
|
245
|
+
return err
|
|
246
|
+
}
|
|
247
|
+
if cmd != nil {
|
|
248
|
+
cmd.Stdin, cmd.Stdout, cmd.Stderr = in, out, out
|
|
249
|
+
return cmd.Run()
|
|
250
|
+
}
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func openLog(dir string) (*log.Logger, func()) {
|
|
255
|
+
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
256
|
+
return log.New(io.Discard, "", 0), func() {}
|
|
257
|
+
}
|
|
258
|
+
path := filepath.Join(dir, "rian.log")
|
|
259
|
+
if info, err := os.Stat(path); err == nil && info.Size() > 1024*1024 {
|
|
260
|
+
_ = os.Rename(path, path+".1")
|
|
261
|
+
}
|
|
262
|
+
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
|
263
|
+
if err != nil {
|
|
264
|
+
return log.New(io.Discard, "", 0), func() {}
|
|
265
|
+
}
|
|
266
|
+
return log.New(f, "rian ", log.LstdFlags), func() { _ = f.Close() }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func safe(value string) string {
|
|
270
|
+
return strings.Map(func(r rune) rune {
|
|
271
|
+
if r < 32 || r == 127 {
|
|
272
|
+
return ' '
|
|
273
|
+
}
|
|
274
|
+
return r
|
|
275
|
+
}, value)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const usage = `Rian · your local project desk
|
|
279
|
+
|
|
280
|
+
Usage:
|
|
281
|
+
rian Open the full-screen interface
|
|
282
|
+
rian add <directory> [--yes] Inspect and save a project
|
|
283
|
+
rian list [--json] List saved projects
|
|
284
|
+
rian open <project> Open in VS Code
|
|
285
|
+
rian vscode <project> Open in VS Code
|
|
286
|
+
rian terminal <project> Open an interactive shell here
|
|
287
|
+
rian opencode <project> Launch OpenCode interactively
|
|
288
|
+
rian kilocode <project> Launch KiloCode (kilo or kilocode)
|
|
289
|
+
rian chatgpt <project> Open ChatGPT
|
|
290
|
+
rian github <project> Open the GitHub repository
|
|
291
|
+
rian run <project> <command> Run a detected or custom command
|
|
292
|
+
rian remove <project> [--yes] Remove only the Rian entry
|
|
293
|
+
rian config Print the configuration path
|
|
294
|
+
rian --version Show version
|
|
295
|
+
|
|
296
|
+
Projects accept a name, ID, or unambiguous fuzzy match. Quote names with spaces.
|
|
297
|
+
In the interface: a add · / search · Ctrl+K actions · ? help · q quit
|
|
298
|
+
`
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"rian/internal/config"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestCLIProjectLifecycle(t *testing.T) {
|
|
15
|
+
root := t.TempDir()
|
|
16
|
+
t.Setenv("RIAN_CONFIG_HOME", filepath.Join(root, "settings"))
|
|
17
|
+
dir := filepath.Join(root, "Sample Project")
|
|
18
|
+
if err := os.Mkdir(dir, 0700); err != nil {
|
|
19
|
+
t.Fatal(err)
|
|
20
|
+
}
|
|
21
|
+
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"scripts":{"test":"echo project-ok"}}`), 0600); err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
in, err := os.Open(os.DevNull)
|
|
25
|
+
if err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
defer in.Close()
|
|
29
|
+
call := func(args ...string) (string, error) {
|
|
30
|
+
t.Helper()
|
|
31
|
+
var out bytes.Buffer
|
|
32
|
+
err := Run(args, in, &out, "test")
|
|
33
|
+
return out.String(), err
|
|
34
|
+
}
|
|
35
|
+
if _, err := call("add", dir); err == nil {
|
|
36
|
+
t.Fatal("noninteractive add must require --yes")
|
|
37
|
+
}
|
|
38
|
+
if _, err := call("add", dir, "--yes"); err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
if _, err := call("add", dir, "--yes"); err == nil {
|
|
42
|
+
t.Fatal("duplicate add should fail")
|
|
43
|
+
}
|
|
44
|
+
output, err := call("list")
|
|
45
|
+
if err != nil || !strings.Contains(output, "Sample Project") {
|
|
46
|
+
t.Fatalf("list: %s %v", output, err)
|
|
47
|
+
}
|
|
48
|
+
output, err = call("list", "--json")
|
|
49
|
+
if err != nil || !json.Valid([]byte(output)) {
|
|
50
|
+
t.Fatalf("JSON: %s %v", output, err)
|
|
51
|
+
}
|
|
52
|
+
if _, err := call("run", "sample", "unknown"); err == nil || !strings.Contains(err.Error(), "available: test") {
|
|
53
|
+
t.Fatalf("missing command: %v", err)
|
|
54
|
+
}
|
|
55
|
+
path, _ := config.DefaultPath()
|
|
56
|
+
store := config.New(path)
|
|
57
|
+
if _, err := store.Update(func(cfg *config.Config) error {
|
|
58
|
+
cfg.Projects[0].Commands = map[string]string{"smoke": "echo project-ok"}
|
|
59
|
+
return nil
|
|
60
|
+
}); err != nil {
|
|
61
|
+
t.Fatal(err)
|
|
62
|
+
}
|
|
63
|
+
output, err = call("run", "Sample Project", "smoke")
|
|
64
|
+
if err != nil || !strings.Contains(output, "project-ok") {
|
|
65
|
+
t.Fatalf("run: %s %v", output, err)
|
|
66
|
+
}
|
|
67
|
+
cfg, _ := store.Load()
|
|
68
|
+
if cfg.Projects[0].LastUsed.IsZero() {
|
|
69
|
+
t.Fatal("CLI did not record use")
|
|
70
|
+
}
|
|
71
|
+
if _, err := call("remove", "sample", "--yes"); err != nil {
|
|
72
|
+
t.Fatal(err)
|
|
73
|
+
}
|
|
74
|
+
if _, err := os.Stat(filepath.Join(dir, "package.json")); err != nil {
|
|
75
|
+
t.Fatal("removal touched project files")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestHelpDoesNotNeedConfig(t *testing.T) {
|
|
80
|
+
t.Setenv("RIAN_CONFIG_HOME", filepath.Join(t.TempDir(), "missing"))
|
|
81
|
+
var out bytes.Buffer
|
|
82
|
+
if err := Run([]string{"--help"}, os.Stdin, &out, "test"); err != nil || !strings.Contains(out.String(), "rian add") {
|
|
83
|
+
t.Fatalf("%s %v", out.String(), err)
|
|
84
|
+
}
|
|
85
|
+
}
|