Skip to content

Commit 88493f6

Browse files
committed
feat: auto-detect and clone kit-*/module-* deps inside RunSetupTask
Phase 1.5 runs between the initial git clones and pnpm install: - Reads package.json of every freshly cloned package (apps, themes, etc.) - Scans dependencies + devDependencies for @owdproject/* entries - Filters to shortnames starting with 'module-' or 'kit-' - Skips any that are already present locally (disk check + catalog LocalSource) - Extends the progress bar: totalSteps += len(newDeps) before cloning them - Clones each missing dep automatically from github.com/owdproject/<dep> via HTTPS (no user confirmation needed — these are required runtime deps) - Clone failures are warned but don't abort the task (may be optional/npm deps) Adds kindDirForEntry / kindDirForShortName / resolveOwner as standalone helpers to keep the goroutine body readable and reusable by future tasks.
1 parent b1ace85 commit 88493f6

1 file changed

Lines changed: 153 additions & 50 deletions

File tree

go/tui/tasks.go

Lines changed: 153 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tui
33
import (
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+
223326
func (m *TuiModel) RunUpdatePackageTask(pkgName string, shortName string, kind string, isLocalSource bool) {
224327
workspaceRoot := m.workspaceRoot
225328
runtime := m.runtime

0 commit comments

Comments
 (0)