@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -2
  3. package/THIRD_PARTY_NOTICES.txt +673 -0
  4. package/bin/rian.cjs +12 -0
  5. package/cmd/rian/main.go +17 -0
  6. package/dist/binaries/darwin-arm64/rian +0 -0
  7. package/dist/binaries/darwin-x64/rian +0 -0
  8. package/dist/binaries/linux-arm64/rian +0 -0
  9. package/dist/binaries/linux-x64/rian +0 -0
  10. package/dist/binaries/manifest.json +29 -0
  11. package/dist/binaries/win32-arm64/rian.exe +0 -0
  12. package/dist/binaries/win32-x64/rian.exe +0 -0
  13. package/go.mod +28 -0
  14. package/go.sum +44 -0
  15. package/internal/app/app.go +298 -0
  16. package/internal/app/app_test.go +85 -0
  17. package/internal/config/config.go +189 -0
  18. package/internal/config/config_test.go +101 -0
  19. package/internal/detect/detect.go +252 -0
  20. package/internal/detect/detect_test.go +86 -0
  21. package/internal/git/git.go +166 -0
  22. package/internal/git/git_test.go +73 -0
  23. package/internal/git/window_unix.go +7 -0
  24. package/internal/git/window_windows.go +8 -0
  25. package/internal/process/platform_unix.go +19 -0
  26. package/internal/process/platform_windows.go +25 -0
  27. package/internal/process/process.go +172 -0
  28. package/internal/process/process_test.go +137 -0
  29. package/internal/project/project.go +180 -0
  30. package/internal/project/project_test.go +71 -0
  31. package/internal/tools/lookup.go +41 -0
  32. package/internal/tools/platform_unix.go +34 -0
  33. package/internal/tools/platform_windows.go +52 -0
  34. package/internal/tools/platform_windows_test.go +29 -0
  35. package/internal/tools/tools.go +152 -0
  36. package/internal/tools/tools_test.go +56 -0
  37. package/internal/ui/actions.go +259 -0
  38. package/internal/ui/forms.go +156 -0
  39. package/internal/ui/model.go +220 -0
  40. package/internal/ui/styles.go +85 -0
  41. package/internal/ui/ui_test.go +170 -0
  42. package/internal/ui/update.go +470 -0
  43. package/internal/ui/view.go +459 -0
  44. package/lib/launcher.cjs +46 -0
  45. package/lib/platforms.cjs +20 -0
  46. package/package.json +39 -4
  47. package/scripts/build-binaries.cjs +60 -0
  48. package/scripts/check-package.cjs +49 -0
  49. package/scripts/ensure-binary.cjs +11 -0
  50. package/test/npm/launcher.test.cjs +45 -0
  51. package/test/npm/package.test.cjs +26 -0
@@ -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
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
+ }