@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.
Files changed (56) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +50 -16
  3. package/bin/bridge.js +129 -0
  4. package/bin/cli.js +491 -0
  5. package/bin/cp.js +3542 -0
  6. package/bin/desktop.js +5 -0
  7. package/bin/lib/catalog.js +504 -0
  8. package/bin/lib/config.js +173 -0
  9. package/bin/lib/cpAscii.js +39 -0
  10. package/bin/lib/cpFormat.js +343 -0
  11. package/bin/lib/create.js +246 -0
  12. package/bin/lib/desktopConfig.js +48 -0
  13. package/bin/lib/generateTemplate.js +427 -0
  14. package/bin/lib/install.js +521 -0
  15. package/bin/lib/installWizardFlow.js +792 -0
  16. package/bin/lib/migrateModulePlayground.config.mjs +187 -0
  17. package/bin/lib/migrateModulePlayground.mjs +318 -0
  18. package/bin/lib/npmVersions.js +124 -0
  19. package/bin/lib/packageSources.js +375 -0
  20. package/bin/lib/playgroundContext.js +188 -0
  21. package/bin/lib/scaffold.js +205 -0
  22. package/bin/lib/status.js +319 -0
  23. package/bin/lib/validateModule.js +978 -0
  24. package/bin/lib/workspace.js +289 -0
  25. package/bin/owd.js +2 -118
  26. package/go/bridge/bridge.go +220 -0
  27. package/go/desktop-go +0 -0
  28. package/go/go.mod +33 -0
  29. package/go/go.sum +52 -0
  30. package/go/main.go +34 -0
  31. package/go/owd-cli +0 -0
  32. package/go/tui/engine.go +472 -0
  33. package/go/tui/engine_test.go +40 -0
  34. package/go/tui/handlers.go +604 -0
  35. package/go/tui/helpers.go +916 -0
  36. package/go/tui/helpers_test.go +97 -0
  37. package/go/tui/modals.go +423 -0
  38. package/go/tui/resolver.go +377 -0
  39. package/go/tui/resolver_test.go +86 -0
  40. package/go/tui/styles.go +153 -0
  41. package/go/tui/tasks.go +850 -0
  42. package/go/tui/tui.go +869 -0
  43. package/go/tui/tui_test.go +69 -0
  44. package/go/tui/types.go +230 -0
  45. package/go/tui/view.go +812 -0
  46. package/go/tui/workqueue.go +179 -0
  47. package/go/tui/workqueue_test.go +90 -0
  48. package/package.json +33 -22
  49. package/src/commands/create.js +0 -11
  50. package/src/commands/generate.js +0 -12
  51. package/src/commands/installApp.js +0 -9
  52. package/src/commands/installTheme.js +0 -9
  53. package/src/commands/start.js +0 -13
  54. package/src/utils/cloneRepo.js +0 -25
  55. package/src/utils/installPackage.js +0 -15
  56. 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
@@ -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
+ }