@owdproject/cli 0.0.3 → 0.2.0
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 +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +129 -0
- package/bin/cli.js +491 -0
- package/bin/cp.js +3542 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +504 -0
- package/bin/lib/config.js +173 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/create.js +246 -0
- package/bin/lib/desktopConfig.js +48 -0
- package/bin/lib/generateTemplate.js +427 -0
- package/bin/lib/install.js +521 -0
- package/bin/lib/installWizardFlow.js +792 -0
- package/bin/lib/migrateModulePlayground.config.mjs +187 -0
- package/bin/lib/migrateModulePlayground.mjs +318 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/packageSources.js +375 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +319 -0
- package/bin/lib/validateModule.js +978 -0
- package/bin/lib/workspace.js +289 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +220 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +33 -0
- package/go/go.sum +52 -0
- package/go/main.go +34 -0
- package/go/owd-cli +0 -0
- package/go/tui/engine.go +472 -0
- package/go/tui/engine_test.go +40 -0
- package/go/tui/handlers.go +604 -0
- package/go/tui/helpers.go +916 -0
- package/go/tui/helpers_test.go +97 -0
- package/go/tui/modals.go +423 -0
- package/go/tui/resolver.go +377 -0
- package/go/tui/resolver_test.go +86 -0
- package/go/tui/styles.go +153 -0
- package/go/tui/tasks.go +850 -0
- package/go/tui/tui.go +869 -0
- package/go/tui/tui_test.go +69 -0
- package/go/tui/types.go +230 -0
- package/go/tui/view.go +812 -0
- package/go/tui/workqueue.go +179 -0
- package/go/tui/workqueue_test.go +90 -0
- package/package.json +33 -22
- package/src/commands/create.js +0 -11
- package/src/commands/generate.js +0 -12
- package/src/commands/installApp.js +0 -9
- package/src/commands/installTheme.js +0 -9
- package/src/commands/start.js +0 -13
- package/src/utils/cloneRepo.js +0 -25
- package/src/utils/installPackage.js +0 -15
- package/src/utils/logging.js +0 -15
package/go/go.sum
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
|
2
|
+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
|
3
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
4
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
5
|
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
|
6
|
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
|
7
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
8
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
9
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
10
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
11
|
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
12
|
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
13
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
14
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
15
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
16
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
17
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
18
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
19
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
20
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
21
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
22
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
23
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
24
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
25
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
26
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
27
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
28
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
29
|
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
30
|
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
31
|
+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
32
|
+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
33
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
34
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
35
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
36
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
37
|
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
38
|
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
39
|
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
40
|
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
41
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
42
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
43
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
44
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
45
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
46
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
47
|
+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
48
|
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
49
|
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
50
|
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
51
|
+
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
|
52
|
+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
package/go/main.go
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
"owd-cli/bridge"
|
|
8
|
+
"owd-cli/tui"
|
|
9
|
+
|
|
10
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var p *tea.Program
|
|
14
|
+
|
|
15
|
+
func main() {
|
|
16
|
+
root, err := bridge.FindWorkspaceRoot()
|
|
17
|
+
if err != nil {
|
|
18
|
+
fmt.Printf("Error: %v\n", err)
|
|
19
|
+
os.Exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
model := tui.NewModel(root)
|
|
23
|
+
p = tea.NewProgram(&model, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
|
24
|
+
|
|
25
|
+
finalModel, err := p.Run()
|
|
26
|
+
if err != nil {
|
|
27
|
+
fmt.Printf("Alas, there's been an error: %v", err)
|
|
28
|
+
os.Exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if m, ok := finalModel.(*tui.TuiModel); ok && m.ExitCode != 0 {
|
|
32
|
+
os.Exit(m.ExitCode)
|
|
33
|
+
}
|
|
34
|
+
}
|
package/go/owd-cli
ADDED
|
Binary file
|
package/go/tui/engine.go
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
package tui
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
|
|
8
|
+
"owd-cli/bridge"
|
|
9
|
+
|
|
10
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type WizardMode int
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
WizardSave WizardMode = iota
|
|
17
|
+
WizardSaveAndServe
|
|
18
|
+
WizardStartupCheck
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type EnginePhase string
|
|
22
|
+
|
|
23
|
+
const (
|
|
24
|
+
PhaseIdle EnginePhase = "idle"
|
|
25
|
+
PhaseWriteConfig EnginePhase = "write_config"
|
|
26
|
+
PhaseResolve EnginePhase = "resolve"
|
|
27
|
+
PhasePrompt EnginePhase = "prompt"
|
|
28
|
+
PhaseProcess EnginePhase = "process"
|
|
29
|
+
PhaseInstall EnginePhase = "install"
|
|
30
|
+
PhasePrepare EnginePhase = "prepare"
|
|
31
|
+
PhaseDone EnginePhase = "done"
|
|
32
|
+
PhaseFailed EnginePhase = "failed"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// buildConfigFromPending applies catalog toggles to a copy of the current config.
|
|
36
|
+
func buildConfigFromPending(
|
|
37
|
+
ctx *bridge.WorkspaceContext,
|
|
38
|
+
catalog *bridge.CatalogResponse,
|
|
39
|
+
pending map[string]bool,
|
|
40
|
+
pendingTheme *string,
|
|
41
|
+
) (*bridge.Config, []string) {
|
|
42
|
+
if ctx == nil {
|
|
43
|
+
return nil, nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
config := bridge.Config{
|
|
47
|
+
Apps: append([]string{}, ctx.Config.Apps...),
|
|
48
|
+
Modules: append([]string{}, ctx.Config.Modules...),
|
|
49
|
+
}
|
|
50
|
+
if ctx.Config.Theme != nil {
|
|
51
|
+
t := *ctx.Config.Theme
|
|
52
|
+
config.Theme = &t
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var depsToRemove []string
|
|
56
|
+
|
|
57
|
+
for name, install := range pending {
|
|
58
|
+
entry := catalogEntryForName(catalog, name)
|
|
59
|
+
if install {
|
|
60
|
+
switch entry.Kind {
|
|
61
|
+
case "app":
|
|
62
|
+
if !containsStr(config.Apps, name) {
|
|
63
|
+
config.Apps = append(config.Apps, name)
|
|
64
|
+
}
|
|
65
|
+
case "module":
|
|
66
|
+
if !containsStr(config.Modules, name) {
|
|
67
|
+
config.Modules = append(config.Modules, name)
|
|
68
|
+
}
|
|
69
|
+
case "theme":
|
|
70
|
+
config.Theme = &name
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
depsToRemove = append(depsToRemove, name)
|
|
74
|
+
switch entry.Kind {
|
|
75
|
+
case "app":
|
|
76
|
+
config.Apps = removeStr(config.Apps, name)
|
|
77
|
+
case "module":
|
|
78
|
+
config.Modules = removeStr(config.Modules, name)
|
|
79
|
+
case "theme":
|
|
80
|
+
if config.Theme != nil && *config.Theme == name {
|
|
81
|
+
empty := ""
|
|
82
|
+
config.Theme = &empty
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if pendingTheme != nil {
|
|
89
|
+
config.Theme = pendingTheme
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return &config, depsToRemove
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func containsStr(list []string, v string) bool {
|
|
96
|
+
for _, s := range list {
|
|
97
|
+
if s == v {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func removeStr(list []string, v string) []string {
|
|
105
|
+
var out []string
|
|
106
|
+
for _, s := range list {
|
|
107
|
+
if s != v {
|
|
108
|
+
out = append(out, s)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func (m *TuiModel) startWizard(mode WizardMode) tea.Cmd {
|
|
115
|
+
if mode == WizardStartupCheck {
|
|
116
|
+
q := BuildStartupCheckQueue(m.workspaceRoot, m.ctx, m.catalog, func(string) {})
|
|
117
|
+
hasWork := false
|
|
118
|
+
for _, item := range q.Items {
|
|
119
|
+
if item.Status == StatusPending {
|
|
120
|
+
hasWork = true
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if !hasWork {
|
|
125
|
+
m.addLog("ℹ Startup check: all packages are available")
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
m.activeTask = TaskSetup
|
|
131
|
+
m.enginePhase = PhaseWriteConfig
|
|
132
|
+
m.workQueue = NewWorkQueue()
|
|
133
|
+
m.statusMsg = "Starting wizard…"
|
|
134
|
+
m.addLog("ℹ Starting wizard workflow…")
|
|
135
|
+
|
|
136
|
+
if mode == WizardSaveAndServe {
|
|
137
|
+
m.startServerAfterSetup = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var config *bridge.Config
|
|
141
|
+
var depsToRemove []string
|
|
142
|
+
|
|
143
|
+
if mode == WizardStartupCheck {
|
|
144
|
+
m.addLog("ℹ Running startup check wizard…")
|
|
145
|
+
} else {
|
|
146
|
+
config, depsToRemove = buildConfigFromPending(m.ctx, m.catalog, m.pendingPackages, m.pendingTheme)
|
|
147
|
+
m.pendingPackages = make(map[string]bool)
|
|
148
|
+
m.pendingTheme = nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
catalogCopy := m.catalog
|
|
152
|
+
workspaceRoot := m.workspaceRoot
|
|
153
|
+
runtime := m.runtime
|
|
154
|
+
lastMethod := m.lastInstallMethod
|
|
155
|
+
|
|
156
|
+
return tea.Batch(func() tea.Msg {
|
|
157
|
+
m.RunWizardEngine(mode, config, depsToRemove, catalogCopy, workspaceRoot, runtime, lastMethod)
|
|
158
|
+
return nil
|
|
159
|
+
}, m.listenToChannel())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func (m *TuiModel) RunWizardEngine(
|
|
163
|
+
mode WizardMode,
|
|
164
|
+
config *bridge.Config,
|
|
165
|
+
depsToRemove []string,
|
|
166
|
+
catalog *bridge.CatalogResponse,
|
|
167
|
+
workspaceRoot string,
|
|
168
|
+
runtime *RuntimeState,
|
|
169
|
+
lastInstallMethod string,
|
|
170
|
+
) {
|
|
171
|
+
go func() {
|
|
172
|
+
msgChan := runtime.msgChan
|
|
173
|
+
log := func(line string) { msgChan <- logLineMsg(line) }
|
|
174
|
+
emitQueue := func(q *WorkQueue) {
|
|
175
|
+
msgChan <- engineQueueUpdatedMsg{Queue: q.Clone()}
|
|
176
|
+
}
|
|
177
|
+
emitPhase := func(p EnginePhase) { msgChan <- enginePhaseMsg{Phase: p} }
|
|
178
|
+
|
|
179
|
+
var ctx *bridge.WorkspaceContext
|
|
180
|
+
var err error
|
|
181
|
+
|
|
182
|
+
if mode != WizardStartupCheck {
|
|
183
|
+
emitPhase(PhaseWriteConfig)
|
|
184
|
+
log("ℹ Updating desktop.config.ts")
|
|
185
|
+
if err = bridge.WriteConfigOnly(workspaceRoot, config); err != nil {
|
|
186
|
+
log("✖ Failed to update desktop.config.ts")
|
|
187
|
+
emitPhase(PhaseFailed)
|
|
188
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
log("✔ desktop.config.ts updated")
|
|
192
|
+
|
|
193
|
+
if len(depsToRemove) > 0 {
|
|
194
|
+
log(fmt.Sprintf("ℹ Removing %d package(s) from package.json", len(depsToRemove)))
|
|
195
|
+
if err = bridge.WritePackageJsonDeps(workspaceRoot, nil, depsToRemove); err != nil {
|
|
196
|
+
log("✖ Failed to update package.json removals")
|
|
197
|
+
emitPhase(PhaseFailed)
|
|
198
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
log("✔ package.json updated")
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
emitPhase(PhaseResolve)
|
|
206
|
+
ctx, err = bridge.ReadWorkspaceContext(workspaceRoot)
|
|
207
|
+
if err != nil {
|
|
208
|
+
log("✖ Failed to read workspace context")
|
|
209
|
+
emitPhase(PhaseFailed)
|
|
210
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var q *WorkQueue
|
|
215
|
+
if mode == WizardStartupCheck {
|
|
216
|
+
q = BuildStartupCheckQueue(workspaceRoot, ctx, catalog, log)
|
|
217
|
+
} else {
|
|
218
|
+
q = BuildQueueFromConfig(workspaceRoot, ctx, catalog, log)
|
|
219
|
+
}
|
|
220
|
+
emitQueue(q)
|
|
221
|
+
|
|
222
|
+
settings := ctx.Settings
|
|
223
|
+
|
|
224
|
+
for {
|
|
225
|
+
if promptItem := q.NextPendingPrompt(); promptItem != nil {
|
|
226
|
+
emitPhase(PhasePrompt)
|
|
227
|
+
msgChan <- engineNeedsPromptMsg{Item: *promptItem}
|
|
228
|
+
method := <-runtime.engineResume
|
|
229
|
+
|
|
230
|
+
if method == "abort" {
|
|
231
|
+
log("✖ Wizard aborted by user")
|
|
232
|
+
emitPhase(PhaseFailed)
|
|
233
|
+
msgChan <- taskFinishedMsg{Success: false, Err: fmt.Errorf("wizard aborted")}
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
log("ℹ Download method selected: " + resolveMethodLabel(method))
|
|
238
|
+
if err := applyResolution(q, promptItem, method, workspaceRoot, catalog, &settings, log); err != nil {
|
|
239
|
+
emitPhase(PhaseFailed)
|
|
240
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
emitQueue(q)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
item := q.NextPending()
|
|
248
|
+
if item == nil {
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if item.Kind == KindNpm || item.Kind == KindLocal {
|
|
253
|
+
prev := item.Status
|
|
254
|
+
q.MarkSkipped(item.Name)
|
|
255
|
+
log(item.LogTransition(prev, StatusSkipped))
|
|
256
|
+
emitQueue(q)
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
emitPhase(PhaseProcess)
|
|
261
|
+
if err := processCloneItem(q, item, workspaceRoot, catalog, runtime, log); err != nil {
|
|
262
|
+
emitPhase(PhaseFailed)
|
|
263
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
emitQueue(q)
|
|
267
|
+
|
|
268
|
+
updated := q.FindByName(item.Name)
|
|
269
|
+
if updated != nil {
|
|
270
|
+
DiscoverNestedDeps(workspaceRoot, *updated, catalog, q, log)
|
|
271
|
+
emitQueue(q)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
var catalogEntries []bridge.CatalogEntry
|
|
275
|
+
if catalog != nil {
|
|
276
|
+
catalogEntries = catalog.Entries
|
|
277
|
+
}
|
|
278
|
+
if err := ResolveAndCloneMissingDependencies(workspaceRoot, catalogEntries, runtime, log, nil, nil, nil); err != nil {
|
|
279
|
+
emitPhase(PhaseFailed)
|
|
280
|
+
msgChan <- taskFinishedMsg{Success: false, Err: err}
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
emitPhase(PhaseInstall)
|
|
285
|
+
log("ℹ Running pnpm install")
|
|
286
|
+
result := runtime.runProcessStream(workspaceRoot, "pnpm", []string{"install"})
|
|
287
|
+
if result.Err != nil {
|
|
288
|
+
log("✖ pnpm install failed")
|
|
289
|
+
emitPhase(PhaseFailed)
|
|
290
|
+
msgChan <- taskFinishedMsg{Success: false, Err: result.Err}
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
log("✔ pnpm install completed")
|
|
294
|
+
|
|
295
|
+
emitPhase(PhasePrepare)
|
|
296
|
+
log("ℹ Running pnpm run prepare:modules")
|
|
297
|
+
result = runtime.runProcessStream(workspaceRoot, "pnpm", []string{"run", "prepare:modules"})
|
|
298
|
+
if result.Err != nil {
|
|
299
|
+
log("✖ prepare:modules failed")
|
|
300
|
+
emitPhase(PhaseFailed)
|
|
301
|
+
msgChan <- taskFinishedMsg{Success: false, Err: result.Err}
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
log("✔ prepare:modules completed")
|
|
305
|
+
|
|
306
|
+
if settings.LastInstallChoices != nil {
|
|
307
|
+
_ = bridge.WriteChanges(workspaceRoot, &bridge.WritePayload{Settings: &settings})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
emitPhase(PhaseDone)
|
|
311
|
+
log("✔ Wizard completed successfully")
|
|
312
|
+
msgChan <- taskFinishedMsg{Success: true}
|
|
313
|
+
}()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func resolveMethodLabel(method string) string {
|
|
317
|
+
switch method {
|
|
318
|
+
case "git-ssh":
|
|
319
|
+
return "Git SSH"
|
|
320
|
+
case "git-https":
|
|
321
|
+
return "Git HTTPS"
|
|
322
|
+
case "npm":
|
|
323
|
+
return "NPM Package"
|
|
324
|
+
default:
|
|
325
|
+
return method
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func applyResolution(
|
|
330
|
+
q *WorkQueue,
|
|
331
|
+
item *WorkItem,
|
|
332
|
+
method string,
|
|
333
|
+
workspaceRoot string,
|
|
334
|
+
catalog *bridge.CatalogResponse,
|
|
335
|
+
settings *bridge.Settings,
|
|
336
|
+
log func(string),
|
|
337
|
+
) error {
|
|
338
|
+
name := item.Name
|
|
339
|
+
short := item.ShortName
|
|
340
|
+
|
|
341
|
+
switch method {
|
|
342
|
+
case "local":
|
|
343
|
+
if err := bridge.WritePackageJsonDeps(workspaceRoot, map[string]string{name: "workspace:*"}, nil); err != nil {
|
|
344
|
+
log("✖ Failed to link local package " + short)
|
|
345
|
+
return err
|
|
346
|
+
}
|
|
347
|
+
q.SetSource(name, "local", KindLocal)
|
|
348
|
+
q.MarkSkipped(name)
|
|
349
|
+
log("✔ " + short + " linked as local package")
|
|
350
|
+
return nil
|
|
351
|
+
|
|
352
|
+
case "git-ssh", "git-https":
|
|
353
|
+
owner := resolveOwner(name, catalogEntriesSlice(catalog))
|
|
354
|
+
var gitURL string
|
|
355
|
+
if method == "git-ssh" {
|
|
356
|
+
gitURL = fmt.Sprintf("git@github.com:%s/%s.git", owner, short)
|
|
357
|
+
} else {
|
|
358
|
+
gitURL = fmt.Sprintf("https://github.com/%s/%s.git", owner, short)
|
|
359
|
+
}
|
|
360
|
+
if err := bridge.WritePackageJsonDeps(workspaceRoot, map[string]string{name: "workspace:*"}, nil); err != nil {
|
|
361
|
+
log("✖ Failed to update package.json for " + short)
|
|
362
|
+
return err
|
|
363
|
+
}
|
|
364
|
+
if settings.LastInstallChoices == nil {
|
|
365
|
+
settings.LastInstallChoices = make(map[string]interface{})
|
|
366
|
+
}
|
|
367
|
+
settings.LastInstallChoices[name] = map[string]string{"type": "git", "gitUrl": gitURL}
|
|
368
|
+
q.SetSource(name, method, KindClone)
|
|
369
|
+
if item.TargetDir == "" {
|
|
370
|
+
item.TargetDir = filepath.Join(workspaceRoot, kindDirForKind(item.Type), short)
|
|
371
|
+
}
|
|
372
|
+
log("✔ " + short + " queued for clone (" + resolveMethodLabel(method) + ")")
|
|
373
|
+
return nil
|
|
374
|
+
|
|
375
|
+
case "npm":
|
|
376
|
+
version := "latest"
|
|
377
|
+
if item.Entry.Version != nil {
|
|
378
|
+
version = *item.Entry.Version
|
|
379
|
+
}
|
|
380
|
+
if err := bridge.WritePackageJsonDeps(workspaceRoot, map[string]string{name: version}, nil); err != nil {
|
|
381
|
+
log("✖ Failed to add " + short + " to package.json")
|
|
382
|
+
return err
|
|
383
|
+
}
|
|
384
|
+
q.SetSource(name, "npm:"+version, KindNpm)
|
|
385
|
+
q.MarkSkipped(name)
|
|
386
|
+
log("✔ " + short + " added to package.json")
|
|
387
|
+
log("ℹ " + short + " will be installed through pnpm install")
|
|
388
|
+
return nil
|
|
389
|
+
|
|
390
|
+
default:
|
|
391
|
+
return fmt.Errorf("unknown resolution method: %s", method)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func catalogEntriesSlice(catalog *bridge.CatalogResponse) []bridge.CatalogEntry {
|
|
396
|
+
if catalog == nil {
|
|
397
|
+
return nil
|
|
398
|
+
}
|
|
399
|
+
return catalog.Entries
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func processCloneItem(
|
|
403
|
+
q *WorkQueue,
|
|
404
|
+
item *WorkItem,
|
|
405
|
+
workspaceRoot string,
|
|
406
|
+
catalog *bridge.CatalogResponse,
|
|
407
|
+
runtime *RuntimeState,
|
|
408
|
+
log func(string),
|
|
409
|
+
) error {
|
|
410
|
+
prev := item.Status
|
|
411
|
+
q.MarkRunning(item.Name)
|
|
412
|
+
log(item.LogTransition(prev, StatusRunning))
|
|
413
|
+
|
|
414
|
+
short := item.ShortName
|
|
415
|
+
targetDir := item.TargetDir
|
|
416
|
+
if targetDir == "" {
|
|
417
|
+
targetDir = filepath.Join(workspaceRoot, kindDirForKind(item.Type), short)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if _, err := os.Stat(filepath.Join(targetDir, "package.json")); err == nil {
|
|
421
|
+
log("ℹ " + short + " already cloned — skipping")
|
|
422
|
+
q.MarkCompleted(item.Name)
|
|
423
|
+
log("✔ Directory verified: " + relWorkspacePath(workspaceRoot, targetDir))
|
|
424
|
+
return nil
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
method := item.Source
|
|
428
|
+
if method == SourcePending {
|
|
429
|
+
method = "git-https"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
owner := resolveOwner(item.Name, catalogEntriesSlice(catalog))
|
|
433
|
+
var gitURL string
|
|
434
|
+
if method == "git-ssh" {
|
|
435
|
+
gitURL = fmt.Sprintf("git@github.com:%s/%s.git", owner, short)
|
|
436
|
+
} else {
|
|
437
|
+
gitURL = fmt.Sprintf("https://github.com/%s/%s.git", owner, short)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
log("ℹ Cloning " + short + "…")
|
|
441
|
+
result := runtime.runProcessStream(workspaceRoot, "git", []string{"clone", gitURL, targetDir})
|
|
442
|
+
if result.Err != nil {
|
|
443
|
+
q.MarkFailed(item.Name, result.Err.Error())
|
|
444
|
+
log("✖ Clone failed for " + short)
|
|
445
|
+
return result.Err
|
|
446
|
+
}
|
|
447
|
+
log("✔ Clone completed")
|
|
448
|
+
|
|
449
|
+
if _, err := os.Stat(filepath.Join(targetDir, "package.json")); err != nil {
|
|
450
|
+
errMsg := "directory not found after clone"
|
|
451
|
+
q.MarkFailed(item.Name, errMsg)
|
|
452
|
+
log("✖ Clone reported success but directory not found")
|
|
453
|
+
return fmt.Errorf("%s: %s", short, errMsg)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
q.MarkCompleted(item.Name)
|
|
457
|
+
log("✔ Directory verified: " + relWorkspacePath(workspaceRoot, targetDir))
|
|
458
|
+
return nil
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
func relWorkspacePath(workspaceRoot, absPath string) string {
|
|
462
|
+
if rel, err := filepath.Rel(workspaceRoot, absPath); err == nil {
|
|
463
|
+
return rel
|
|
464
|
+
}
|
|
465
|
+
return absPath
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func (m *TuiModel) resolveEnginePrompt(method string) {
|
|
469
|
+
if m.runtime.engineResume != nil {
|
|
470
|
+
m.runtime.engineResume <- method
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package tui
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"owd-cli/bridge"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestResolveMethodLabel(t *testing.T) {
|
|
10
|
+
if resolveMethodLabel("git-ssh") != "Git SSH" {
|
|
11
|
+
t.Fatal("unexpected label for git-ssh")
|
|
12
|
+
}
|
|
13
|
+
if resolveMethodLabel("npm") != "NPM Package" {
|
|
14
|
+
t.Fatal("unexpected label for npm")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func TestBuildConfigFromPendingRemovals(t *testing.T) {
|
|
19
|
+
ctx := &bridge.WorkspaceContext{
|
|
20
|
+
Config: bridge.Config{
|
|
21
|
+
Apps: []string{"@owdproject/app-about", "@owdproject/app-todo"},
|
|
22
|
+
Modules: []string{},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
catalog := &bridge.CatalogResponse{
|
|
26
|
+
Entries: []bridge.CatalogEntry{
|
|
27
|
+
{Name: "@owdproject/app-todo", ShortName: "app-todo", Kind: "app"},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
pending := map[string]bool{
|
|
31
|
+
"@owdproject/app-todo": false,
|
|
32
|
+
}
|
|
33
|
+
config, removes := buildConfigFromPending(ctx, catalog, pending, nil)
|
|
34
|
+
if len(removes) != 1 || removes[0] != "@owdproject/app-todo" {
|
|
35
|
+
t.Fatalf("expected removal, got %v", removes)
|
|
36
|
+
}
|
|
37
|
+
if containsStr(config.Apps, "@owdproject/app-todo") {
|
|
38
|
+
t.Fatalf("app-todo should be removed from config, got %v", config.Apps)
|
|
39
|
+
}
|
|
40
|
+
}
|