@@ -3,6 +3,7 @@ package tui
33import (
44 "bufio"
55 "context"
6+ "encoding/json"
67 "fmt"
78 "io"
89 "os"
@@ -105,12 +106,18 @@ func (m *TuiModel) RunSetupTask(adds map[string]string) {
105106 }
106107
107108 go func () {
108- totalSteps := len (adds ) + 2
109+ totalSteps := len (adds ) + 2 // N clones + pnpm install + prepare:modules
109110 step := 0
110111 msgChan <- setupProgressMsg {Step : step , Total : totalSteps , Label : "Initializing…" }
111112
112- // 1. Clone each package via git directly (bridge.WriteChanges already updated
113- // package.json and desktop.config.ts, so we just need the git clone)
113+ // clonedDirs tracks successfully cloned target directories for dep scanning
114+ type clonedInfo struct {
115+ shortName string
116+ targetDir string
117+ }
118+ var clonedDirs []clonedInfo
119+
120+ // ── Phase 1: Clone each package ──────────────────────────────────────
114121 for pkgName , method := range adds {
115122 step ++
116123 shortName := pkgName
@@ -121,65 +128,27 @@ func (m *TuiModel) RunSetupTask(adds map[string]string) {
121128 msgChan <- setupProgressMsg {Step : step , Total : totalSteps , Label : fmt .Sprintf ("Cloning %s…" , shortName )}
122129
123130 if method == "npm" {
124- // npm install: will be handled by pnpm install below; nothing to clone
125131 msgChan <- logLineMsg (fmt .Sprintf (">>> %s will be installed via npm (pnpm install)" , shortName ))
126132 continue
127133 }
128-
129134 if method == "local" {
130- // local workspace folder already present; nothing to clone
131135 msgChan <- logLineMsg (fmt .Sprintf (">>> %s already available as local workspace package" , shortName ))
132136 continue
133137 }
134138
135- // Determine target directory based on package kind
136- kindDir := ""
137- for _ , e := range catalogEntries {
138- if e .Name == pkgName {
139- switch e .Kind {
140- case "app" :
141- kindDir = "apps"
142- case "module" :
143- kindDir = "packages"
144- case "theme" :
145- kindDir = "themes"
146- }
147- break
148- }
149- }
150- if kindDir == "" {
151- // Infer from shortname prefix
152- switch {
153- case strings .HasPrefix (shortName , "app-" ):
154- kindDir = "apps"
155- case strings .HasPrefix (shortName , "theme-" ):
156- kindDir = "themes"
157- default :
158- kindDir = "packages"
159- }
160- }
161-
139+ // Determine target directory
140+ kindDir := kindDirForEntry (pkgName , shortName , catalogEntries )
162141 targetDir := filepath .Join (workspaceRoot , kindDir , shortName )
163142
164143 // Skip if already cloned
165144 if _ , err := os .Stat (filepath .Join (targetDir , "package.json" )); err == nil {
166145 msgChan <- logLineMsg (fmt .Sprintf (">>> %s already cloned — skipping" , shortName ))
146+ clonedDirs = append (clonedDirs , clonedInfo {shortName , targetDir })
167147 continue
168148 }
169149
170- // Resolve git URL
171- owner := "owdproject"
172- if githubUser != "" {
173- owner = githubUser
174- } else {
175- for _ , e := range catalogEntries {
176- if e .Name == pkgName && e .Org != "" {
177- owner = e .Org
178- break
179- }
180- }
181- }
182-
150+ // Resolve git URL (use githubUser if set, else owdproject/catalog org)
151+ owner := resolveOwner (pkgName , githubUser , catalogEntries )
183152 var gitURL string
184153 if method == "git-ssh" {
185154 gitURL = fmt .Sprintf ("git@github.com:%s/%s.git" , owner , shortName )
@@ -193,10 +162,100 @@ func (m *TuiModel) RunSetupTask(adds map[string]string) {
193162 msgChan <- taskFinishedMsg {Success : false , Err : err }
194163 return
195164 }
196- msgChan <- logLineMsg (fmt .Sprintf (">>> ✓ %s cloned successfully" , shortName ))
165+ msgChan <- logLineMsg (fmt .Sprintf (">>> ✓ %s cloned" , shortName ))
166+ clonedDirs = append (clonedDirs , clonedInfo {shortName , targetDir })
197167 }
198168
199- // 2. Sync workspace with pnpm install
169+ // ── Phase 1.5: Scan cloned packages for kit-* / module-* deps ────────
170+ type depToClone struct {
171+ shortName string
172+ targetDir string
173+ gitURL string
174+ }
175+ var extraDeps []depToClone
176+ seenDep := map [string ]bool {}
177+
178+ // Mark packages already cloned or locally present as seen
179+ for _ , ci := range clonedDirs {
180+ seenDep [ci .shortName ] = true
181+ }
182+ for _ , e := range catalogEntries {
183+ if e .LocalSource {
184+ seenDep [e .ShortName ] = true
185+ }
186+ }
187+
188+ for _ , ci := range clonedDirs {
189+ data , err := os .ReadFile (filepath .Join (ci .targetDir , "package.json" ))
190+ if err != nil {
191+ continue
192+ }
193+ var pkg struct {
194+ Dependencies map [string ]string `json:"dependencies"`
195+ DevDependencies map [string ]string `json:"devDependencies"`
196+ }
197+ if json .Unmarshal (data , & pkg ) != nil {
198+ continue
199+ }
200+
201+ allDeps := make (map [string ]string )
202+ for k , v := range pkg .Dependencies {
203+ allDeps [k ] = v
204+ }
205+ for k , v := range pkg .DevDependencies {
206+ allDeps [k ] = v
207+ }
208+
209+ for depName := range allDeps {
210+ if ! strings .HasPrefix (depName , "@owdproject/" ) {
211+ continue
212+ }
213+ depShort := depName [strings .LastIndex (depName , "/" )+ 1 :]
214+ if ! strings .HasPrefix (depShort , "module-" ) && ! strings .HasPrefix (depShort , "kit-" ) {
215+ continue
216+ }
217+ if seenDep [depShort ] {
218+ continue
219+ }
220+ depKindDir := kindDirForShortName (depShort )
221+ depTarget := filepath .Join (workspaceRoot , depKindDir , depShort )
222+ // Skip if already exists on disk
223+ if _ , err := os .Stat (filepath .Join (depTarget , "package.json" )); err == nil {
224+ seenDep [depShort ] = true
225+ continue
226+ }
227+ // Always clone deps from owdproject via HTTPS (no fork needed)
228+ gitURL := fmt .Sprintf ("https://github.com/owdproject/%s.git" , depShort )
229+ extraDeps = append (extraDeps , depToClone {depShort , depTarget , gitURL })
230+ seenDep [depShort ] = true
231+ msgChan <- logLineMsg (fmt .Sprintf (">>> Detected required dep: %s" , depShort ))
232+ }
233+ }
234+
235+ // Extend progress bar if deps were found
236+ if len (extraDeps ) > 0 {
237+ totalSteps += len (extraDeps )
238+ msgChan <- setupProgressMsg {
239+ Step : step ,
240+ Total : totalSteps ,
241+ Label : fmt .Sprintf ("Found %d additional dependenc%s…" ,
242+ len (extraDeps ), map [bool ]string {true : "y" , false : "ies" }[len (extraDeps ) == 1 ]),
243+ }
244+
245+ for _ , dep := range extraDeps {
246+ step ++
247+ msgChan <- setupProgressMsg {Step : step , Total : totalSteps , Label : fmt .Sprintf ("Cloning %s…" , dep .shortName )}
248+ msgChan <- logLineMsg (fmt .Sprintf (">>> Cloning dep %s from %s" , dep .shortName , dep .gitURL ))
249+ if err := runtime .runProcessAndStreamLogsSilent (workspaceRoot , "git" , []string {"clone" , dep .gitURL , dep .targetDir }); err != nil {
250+ // Warn but don't abort — dep might be optional or already linked via npm
251+ msgChan <- logLineMsg (fmt .Sprintf (">>> Warning: could not clone %s: %v" , dep .shortName , err ))
252+ } else {
253+ msgChan <- logLineMsg (fmt .Sprintf (">>> ✓ %s cloned" , dep .shortName ))
254+ }
255+ }
256+ }
257+
258+ // ── Phase 2: pnpm install ─────────────────────────────────────────────
200259 step ++
201260 msgChan <- setupProgressMsg {Step : step , Total : totalSteps , Label : "Installing dependencies (pnpm install)…" }
202261 msgChan <- logLineMsg (">>> Running pnpm install (syncing workspace)…" )
@@ -205,7 +264,7 @@ func (m *TuiModel) RunSetupTask(adds map[string]string) {
205264 return
206265 }
207266
208- // 3. Rebuild stubs
267+ // ── Phase 3: Rebuild stubs ────────────────────────────────────────────
209268 step ++
210269 msgChan <- setupProgressMsg {Step : step , Total : totalSteps , Label : "Rebuilding stubs (prepare:modules)…" }
211270 msgChan <- logLineMsg (">>> Rebuilding stubs…" )
@@ -214,12 +273,56 @@ func (m *TuiModel) RunSetupTask(adds map[string]string) {
214273 return
215274 }
216275
217- // Small pause so the user sees the progress bar reach 100% before it closes
218276 time .Sleep (500 * time .Millisecond )
219277 msgChan <- taskFinishedMsg {Success : true }
220278 }()
221279}
222280
281+ // kindDirForEntry resolves the workspace subdirectory for a package.
282+ // Uses catalog entries first, falls back to shortname prefix.
283+ func kindDirForEntry (pkgName , shortName string , entries []bridge.CatalogEntry ) string {
284+ for _ , e := range entries {
285+ if e .Name == pkgName {
286+ switch e .Kind {
287+ case "app" :
288+ return "apps"
289+ case "module" :
290+ return "packages"
291+ case "theme" :
292+ return "themes"
293+ }
294+ }
295+ }
296+ return kindDirForShortName (shortName )
297+ }
298+
299+ // kindDirForShortName infers the workspace subdirectory from the package short name.
300+ func kindDirForShortName (shortName string ) string {
301+ switch {
302+ case strings .HasPrefix (shortName , "app-" ):
303+ return "apps"
304+ case strings .HasPrefix (shortName , "theme-" ):
305+ return "themes"
306+ default :
307+ return "packages" // module-*, kit-*, and anything else
308+ }
309+ }
310+
311+ // resolveOwner returns the GitHub owner to use for cloning.
312+ // Priority: explicit githubUser setting > catalog org > "owdproject".
313+ func resolveOwner (pkgName , githubUser string , entries []bridge.CatalogEntry ) string {
314+ if githubUser != "" {
315+ return githubUser
316+ }
317+ for _ , e := range entries {
318+ if e .Name == pkgName && e .Org != "" {
319+ return e .Org
320+ }
321+ }
322+ return "owdproject"
323+ }
324+
325+
223326func (m * TuiModel ) RunUpdatePackageTask (pkgName string , shortName string , kind string , isLocalSource bool ) {
224327 workspaceRoot := m .workspaceRoot
225328 runtime := m .runtime
0 commit comments