diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index c42baccf3f..a5a56cdb04 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -363,7 +363,7 @@ class RpcApiType { } // command "listalleditableapps" [call] - ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise { + ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise { return client.wshRpcCall("listalleditableapps", null, opts); } diff --git a/frontend/builder/app-selection-modal.tsx b/frontend/builder/app-selection-modal.tsx index 247230b011..efea628cc2 100644 --- a/frontend/builder/app-selection-modal.tsx +++ b/frontend/builder/app-selection-modal.tsx @@ -6,21 +6,16 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, globalStore } from "@/store/global"; import * as WOS from "@/store/wos"; +import { formatRelativeTime } from "@/util/util"; import { useEffect, useState } from "react"; const MaxAppNameLength = 50; const AppNameRegex = /^[a-zA-Z0-9_-]+$/; -export function AppSelectionModal() { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(true); +function CreateNewWaveApp({ onCreateApp }: { onCreateApp: (appName: string) => Promise }) { const [newAppName, setNewAppName] = useState(""); - const [error, setError] = useState(""); const [inputError, setInputError] = useState(""); - - useEffect(() => { - loadApps(); - }, []); + const [isCreating, setIsCreating] = useState(false); const validateAppName = (name: string) => { if (!name.trim()) { @@ -39,10 +34,83 @@ export function AppSelectionModal() { return true; }; + const handleCreate = async () => { + const trimmedName = newAppName.trim(); + if (!validateAppName(trimmedName)) { + return; + } + + setIsCreating(true); + try { + await onCreateApp(trimmedName); + } finally { + setIsCreating(false); + } + }; + + return ( +
+

Create New WaveApp

+
+
+ { + const value = e.target.value; + setNewAppName(value); + validateAppName(value); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.nativeEvent.isComposing && newAppName.trim() && !inputError) { + handleCreate(); + } + }} + placeholder="my-app" + maxLength={MaxAppNameLength} + className={`flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${ + inputError ? "border-error" : "border-border focus:border-accent" + }`} + autoFocus + disabled={isCreating} + /> + +
+ {inputError && ( +
+ + {inputError} +
+ )} +
+
+ ); +} + +export function AppSelectionModal() { + const [apps, setApps] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + loadApps(); + }, []); + const loadApps = async () => { try { const appList = await RpcApi.ListAllEditableAppsCommand(TabRpcClient); - setApps(appList || []); + const sortedApps = (appList || []).sort((a, b) => b.modtime - a.modtime); + setApps(sortedApps); } catch (err) { console.error("Failed to load apps:", err); setError("Failed to load apps"); @@ -61,25 +129,8 @@ export function AppSelectionModal() { globalStore.set(atoms.builderAppId, appId); }; - const handleCreateNew = async () => { - const trimmedName = newAppName.trim(); - - if (!trimmedName) { - setError("WaveApp name cannot be empty"); - return; - } - - if (trimmedName.length > MaxAppNameLength) { - setError(`WaveApp name must be ${MaxAppNameLength} characters or less`); - return; - } - - if (!AppNameRegex.test(trimmedName)) { - setError("WaveApp name can only contain letters, numbers, hyphens, and underscores"); - return; - } - - const draftAppId = `draft/${trimmedName}`; + const handleCreateNew = async (appName: string) => { + const draftAppId = `draft/${appName}`; const builderId = globalStore.get(atoms.builderId); const oref = WOS.makeORef("builder", builderId); await RpcApi.SetRTInfoCommand(TabRpcClient, { @@ -111,9 +162,9 @@ export function AppSelectionModal() { } return ( - +
-

Select a WaveApp to Edit

+

Select a WaveApp to Edit

{error && (
@@ -125,18 +176,23 @@ export function AppSelectionModal() { )} {apps.length > 0 && ( -
-

Existing WaveApps

-
- {apps.map((appId) => ( +
+

Existing WaveApps

+
+ {apps.map((appInfo) => ( ))} @@ -145,62 +201,14 @@ export function AppSelectionModal() { )} {apps.length > 0 && ( -
+
or
)} -
-

Create New WaveApp

-
-
- { - const value = e.target.value; - setNewAppName(value); - validateAppName(value); - }} - onKeyDown={(e) => { - if ( - e.key === "Enter" && - !e.nativeEvent.isComposing && - newAppName.trim() && - !inputError - ) { - handleCreateNew(); - } - }} - placeholder="my-app" - maxLength={MaxAppNameLength} - className={`flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${ - inputError ? "border-error" : "border-border focus:border-accent" - }`} - autoFocus - /> - -
- {inputError && ( -
- - {inputError} -
- )} -
-
+
); diff --git a/frontend/builder/builder-buildpanel.tsx b/frontend/builder/builder-buildpanel.tsx index cafb5a19d5..93d7e14647 100644 --- a/frontend/builder/builder-buildpanel.tsx +++ b/frontend/builder/builder-buildpanel.tsx @@ -3,6 +3,7 @@ import { WaveAIModel } from "@/app/aipanel/waveai-model"; import { ContextMenuModel } from "@/app/store/contextmenu"; +import { globalStore } from "@/app/store/jotaiStore"; import { BuilderBuildPanelModel } from "@/builder/store/builder-buildpanel-model"; import { useAtomValue } from "jotai"; import { memo, useCallback, useEffect, useRef } from "react"; @@ -35,6 +36,7 @@ function handleBuildPanelContextMenu(e: React.MouseEvent, selectedText: string): const BuilderBuildPanel = memo(() => { const model = BuilderBuildPanelModel.getInstance(); const outputLines = useAtomValue(model.outputLines); + const showDebug = useAtomValue(model.showDebug); const scrollRef = useRef(null); const preRef = useRef(null); @@ -71,10 +73,25 @@ const BuilderBuildPanel = memo(() => { handleBuildPanelContextMenu(e, selectedText); }, []); + const handleDebugToggle = useCallback(() => { + globalStore.set(model.showDebug, !showDebug); + }, [model, showDebug]); + + const filteredLines = showDebug ? outputLines : outputLines.filter((line) => !line.startsWith("[debug]")); + return (
-
+
Build Output +
 {
                     onMouseUp={handleMouseUp}
                     onContextMenu={handleContextMenu}
                 >
-                    {outputLines.length === 0 ? (
+                    {/* this comment fixes JSX blank line in pre tag */}
+                    {filteredLines.length === 0 ? (
                         Waiting for output...
                     ) : (
-                        outputLines.join("\n")
+                        filteredLines.join("\n")
                     )}
                 
diff --git a/frontend/builder/store/builder-buildpanel-model.ts b/frontend/builder/store/builder-buildpanel-model.ts index e1e376f160..93f4ca734b 100644 --- a/frontend/builder/store/builder-buildpanel-model.ts +++ b/frontend/builder/store/builder-buildpanel-model.ts @@ -12,6 +12,7 @@ export class BuilderBuildPanelModel { private static instance: BuilderBuildPanelModel | null = null; outputLines: PrimitiveAtom = atom([]); + showDebug: PrimitiveAtom = atom(false); outputUnsubFn: (() => void) | null = null; initialized = false; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index f046fafa1a..bc29a2e96d 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -56,6 +56,12 @@ declare global { message?: string; }; + // wshrpc.AppInfo + type AppInfo = { + appid: string; + modtime: number; + }; + // waveobj.Block type Block = WaveObj & { parentoref?: string; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index f940614712..2c02dce2cf 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -452,6 +452,29 @@ function parseDataUrl(dataUrl: string): ParsedDataUrl { return { mimeType, buffer }; } +function formatRelativeTime(timestamp: number): string { + if (!timestamp) { + return "never"; + } + const now = Date.now(); + const diffInSeconds = Math.floor((now - timestamp) / 1000); + const diffInMinutes = Math.floor(diffInSeconds / 60); + const diffInHours = Math.floor(diffInMinutes / 60); + const diffInDays = Math.floor(diffInHours / 24); + + if (diffInMinutes <= 0) { + return "Just now"; + } else if (diffInMinutes < 60) { + return `${diffInMinutes} min${diffInMinutes !== 1 ? "s" : ""} ago`; + } else if (diffInHours < 24) { + return `${diffInHours} hr${diffInHours !== 1 ? "s" : ""} ago`; + } else if (diffInDays < 7) { + return `${diffInDays} day${diffInDays !== 1 ? "s" : ""} ago`; + } else { + return new Date(timestamp).toLocaleDateString(); + } +} + export { atomWithDebounce, atomWithThrottle, @@ -464,6 +487,7 @@ export { deepCompareReturnPrev, escapeBytes, fireAndForget, + formatRelativeTime, getPrefixedSettings, getPromiseState, getPromiseValue, diff --git a/pkg/buildercontroller/buildercontroller.go b/pkg/buildercontroller/buildercontroller.go index 63f98a8623..ba06057a5a 100644 --- a/pkg/buildercontroller/buildercontroller.go +++ b/pkg/buildercontroller/buildercontroller.go @@ -19,8 +19,8 @@ import ( "github.com/wavetermdev/waveterm/pkg/utilds" "github.com/wavetermdev/waveterm/pkg/waveappstore" "github.com/wavetermdev/waveterm/pkg/wavebase" - "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/tsunami/build" ) @@ -189,6 +189,12 @@ func (bc *BuilderController) Start(ctx context.Context, appId string, builderEnv } func (bc *BuilderController) buildAndRun(ctx context.Context, appId string, builderEnv map[string]string) { + appNS, _, err := waveappstore.ParseAppId(appId) + if err != nil { + bc.handleBuildError(fmt.Errorf("failed to parse app id: %w", err)) + return + } + appPath, err := waveappstore.GetAppDir(appId) if err != nil { bc.handleBuildError(fmt.Errorf("failed to get app directory: %w", err)) @@ -224,6 +230,7 @@ func (bc *BuilderController) buildAndRun(ctx context.Context, appId string, buil outputCapture := build.MakeOutputCapture() _, err = build.TsunamiBuildInternal(build.BuildOpts{ AppPath: appPath, + AppNS: appNS, Verbose: true, Open: false, KeepTemp: false, @@ -235,15 +242,16 @@ func (bc *BuilderController) buildAndRun(ctx context.Context, appId string, buil GoPath: goPath, OutputCapture: outputCapture, }) - if err != nil { - bc.handleBuildError(fmt.Errorf("build failed: %w", err)) - return - } for _, line := range outputCapture.GetLines() { bc.outputBuffer.AddLine(line) } + if err != nil { + bc.handleBuildError(fmt.Errorf("build failed: %w", err)) + return + } + info, err := os.Stat(cachePath) if err != nil { bc.handleBuildError(fmt.Errorf("build output not found: %w", err)) diff --git a/pkg/waveappstore/waveappstore.go b/pkg/waveappstore/waveappstore.go index a4ab7f1176..4f63d9e9a7 100644 --- a/pkg/waveappstore/waveappstore.go +++ b/pkg/waveappstore/waveappstore.go @@ -12,6 +12,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/util/fileutil" "github.com/wavetermdev/waveterm/pkg/wavebase" + "github.com/wavetermdev/waveterm/pkg/wshrpc" ) const ( @@ -441,12 +442,39 @@ func ListAllApps() ([]string, error) { return appIds, nil } -func ListAllEditableApps() ([]string, error) { +func GetAppModTime(appId string) (int64, error) { + if err := ValidateAppId(appId); err != nil { + return 0, err + } + + homeDir := wavebase.GetHomeDir() + appNS, appName, err := ParseAppId(appId) + if err != nil { + return 0, err + } + + appPath := filepath.Join(homeDir, "waveapps", appNS, appName) + appGoPath := filepath.Join(appPath, "app.go") + + fileInfo, err := os.Stat(appGoPath) + if err == nil { + return fileInfo.ModTime().UnixMilli(), nil + } + + dirInfo, err := os.Stat(appPath) + if err != nil { + return 0, nil + } + + return dirInfo.ModTime().UnixMilli(), nil +} + +func ListAllEditableApps() ([]wshrpc.AppInfo, error) { homeDir := wavebase.GetHomeDir() waveappsDir := filepath.Join(homeDir, "waveapps") if _, err := os.Stat(waveappsDir); os.IsNotExist(err) { - return []string{}, nil + return []wshrpc.AppInfo{}, nil } localApps := make(map[string]bool) @@ -486,16 +514,31 @@ func ListAllEditableApps() ([]string, error) { allAppNames[appName] = true } - var appIds []string + var appInfos []wshrpc.AppInfo for appName := range allAppNames { + var appId string + var modTimeAppId string if localApps[appName] { - appIds = append(appIds, MakeAppId(AppNSLocal, appName)) + appId = MakeAppId(AppNSLocal, appName) } else { - appIds = append(appIds, MakeAppId(AppNSDraft, appName)) + appId = MakeAppId(AppNSDraft, appName) } + + if draftApps[appName] { + modTimeAppId = MakeAppId(AppNSDraft, appName) + } else { + modTimeAppId = appId + } + + modTime, _ := GetAppModTime(modTimeAppId) + + appInfos = append(appInfos, wshrpc.AppInfo{ + AppId: appId, + ModTime: modTime, + }) } - return appIds, nil + return appInfos, nil } func DraftHasLocalVersion(draftAppId string) (bool, error) { diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index ed1e6b2b47..838cc7919d 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -399,14 +399,10 @@ func ListWorkspaces(ctx context.Context) (waveobj.WorkspaceList, error) { if err != nil { return nil, err } - - log.Println("got workspaces") - windows, err := wstore.DBGetAllObjsByType[*waveobj.Window](ctx, waveobj.OType_Window) if err != nil { return nil, err } - workspaceToWindow := make(map[string]string) for _, window := range windows { workspaceToWindow[window.WorkspaceId] = window.OID diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 2e3c0e7cff..92f4f9dd2c 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -441,8 +441,8 @@ func ListAllAppFilesCommand(w *wshutil.WshRpc, data wshrpc.CommandListAllAppFile } // command "listalleditableapps", wshserver.ListAllEditableAppsCommand -func ListAllEditableAppsCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]string, error) { - resp, err := sendRpcRequestCallHelper[[]string](w, "listalleditableapps", nil, opts) +func ListAllEditableAppsCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]wshrpc.AppInfo, error) { + resp, err := sendRpcRequestCallHelper[[]wshrpc.AppInfo](w, "listalleditableapps", nil, opts) return resp, err } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 2532324699..a0c8479533 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -326,7 +326,7 @@ type WshRpcInterface interface { TermGetScrollbackLinesCommand(ctx context.Context, data CommandTermGetScrollbackLinesData) (*CommandTermGetScrollbackLinesRtnData, error) // builder - ListAllEditableAppsCommand(ctx context.Context) ([]string, error) + ListAllEditableAppsCommand(ctx context.Context) ([]AppInfo, error) ListAllAppFilesCommand(ctx context.Context, data CommandListAllAppFilesData) (*CommandListAllAppFilesRtnData, error) ReadAppFileCommand(ctx context.Context, data CommandReadAppFileData) (*CommandReadAppFileRtnData, error) WriteAppFileCommand(ctx context.Context, data CommandWriteAppFileData) error @@ -956,6 +956,11 @@ type CommandTermGetScrollbackLinesRtnData struct { } // builder +type AppInfo struct { + AppId string `json:"appid"` + ModTime int64 `json:"modtime"` +} + type CommandListAllAppFilesData struct { AppId string `json:"appid"` } diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 572c85ab16..2ce7a509eb 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -948,7 +948,7 @@ func (ws *WshServer) WorkspaceListCommand(ctx context.Context) ([]wshrpc.Workspa return rtn, nil } -func (ws *WshServer) ListAllEditableAppsCommand(ctx context.Context) ([]string, error) { +func (ws *WshServer) ListAllEditableAppsCommand(ctx context.Context) ([]wshrpc.AppInfo, error) { return waveappstore.ListAllEditableApps() } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index f36a441a9f..2467a09fe5 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -207,7 +207,6 @@ func DBGetAllOIDsByType(ctx context.Context, otype string) ([]string, error) { return WithTxRtn(ctx, func(tx *TxWrap) ([]string, error) { rtn := make([]string, 0) table := tableNameFromOType(otype) - log.Printf("DBGetAllOIDsByType table: %s\n", table) query := fmt.Sprintf("SELECT oid FROM %s", table) var rows []idDataType tx.Select(&rows, query) @@ -222,7 +221,6 @@ func DBGetAllObjsByType[T waveobj.WaveObj](ctx context.Context, otype string) ([ return WithTxRtn(ctx, func(tx *TxWrap) ([]T, error) { rtn := make([]T, 0) table := tableNameFromOType(otype) - log.Printf("DBGetAllObjsByType table: %s\n", table) query := fmt.Sprintf("SELECT oid, version, data FROM %s", table) var rows []idDataType tx.Select(&rows, query) diff --git a/tsunami/build/build.go b/tsunami/build/build.go index 96b9972f4d..402f922028 100644 --- a/tsunami/build/build.go +++ b/tsunami/build/build.go @@ -88,6 +88,7 @@ func (oc *OutputCapture) GetLines() []string { type BuildOpts struct { AppPath string + AppNS string Verbose bool Open bool KeepTemp bool @@ -120,10 +121,10 @@ func (opts BuildOpts) getNodePath() string { } type GoVersionCheckResult struct { - GoStatus string - GoPath string - GoVersion string - ErrorString string + GoStatus string + GoPath string + GoVersion string + ErrorString string } func FindGoExecutable() (string, error) { @@ -271,11 +272,11 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { case "ok": if verbose { if opts.GoPath != "" { - oc.Printf("Using custom go path: %s", result.GoPath) + oc.Printf("[debug] Using custom go path: %s", result.GoPath) } else { - oc.Printf("Using go path: %s", result.GoPath) + oc.Printf("[debug] Using go path: %s", result.GoPath) } - oc.Printf("Found %s", result.GoVersion) + oc.Printf("[debug] Found %s", result.GoVersion) } default: return nil, fmt.Errorf("unexpected go status: %s", result.GoStatus) @@ -312,7 +313,7 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { } if verbose { - oc.Printf("Using custom node path: %s", opts.NodePath) + oc.Printf("[debug] Using custom node path: %s", opts.NodePath) } } else { // Use standard PATH lookup @@ -322,7 +323,7 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { } if verbose { - oc.Printf("Found node in PATH") + oc.Printf("[debug] Found node in PATH") } } @@ -332,9 +333,12 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { }, nil } -func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose bool) error { +func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verbose bool) error { oc := opts.OutputCapture - modulePath := fmt.Sprintf("tsunami/app/%s", appName) + if appNS == "" { + appNS = "app" + } + modulePath := fmt.Sprintf("tsunami/%s/%s", appNS, appName) // Check if go.mod already exists in temp directory (copied from app path) tempGoModPath := filepath.Join(tempDir, "go.mod") @@ -344,7 +348,7 @@ func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose boo if _, err := os.Stat(tempGoModPath); err == nil { // go.mod exists in temp dir, parse it if verbose { - oc.Printf("Found existing go.mod in temp directory, parsing it") + oc.Printf("[debug] Found existing go.mod in temp directory, parsing it") } // Parse the existing go.mod @@ -360,7 +364,7 @@ func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose boo } else if os.IsNotExist(err) { // go.mod doesn't exist, create new one if verbose { - oc.Printf("No existing go.mod found, creating new one") + oc.Printf("[debug] No existing go.mod found, creating new one") } modFile = &modfile.File{} @@ -400,10 +404,10 @@ func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose boo } if verbose { - oc.Printf("Created go.mod with module path: %s", modulePath) - oc.Printf("Added require: github.com/wavetermdev/waveterm/tsunami %s", opts.SdkVersion) + oc.Printf("[debug] Created go.mod with module path: %s", modulePath) + oc.Printf("[debug] Added require: github.com/wavetermdev/waveterm/tsunami %s", opts.SdkVersion) if opts.SdkReplacePath != "" { - oc.Printf("Added replace directive: github.com/wavetermdev/waveterm/tsunami => %s", opts.SdkReplacePath) + oc.Printf("[debug] Added replace directive: github.com/wavetermdev/waveterm/tsunami => %s", opts.SdkReplacePath) } } @@ -411,19 +415,28 @@ func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose boo tidyCmd := exec.Command("go", "mod", "tidy") tidyCmd.Dir = tempDir - if oc != nil || verbose { - oc.Printf("Running go mod tidy") + if verbose { + oc.Printf("[debug] Running go mod tidy") + } + + if oc != nil { tidyCmd.Stdout = oc tidyCmd.Stderr = oc + } else { + tidyCmd.Stdout = os.Stdout + tidyCmd.Stderr = os.Stderr } if err := tidyCmd.Run(); err != nil { - return fmt.Errorf("failed to run go mod tidy: %w", err) + return fmt.Errorf("go mod tidy failed (see output for errors)") + } + + if oc != nil { + oc.Flush() } - oc.Flush() if verbose { - oc.Printf("Successfully ran go mod tidy") + oc.Printf("[debug] Successfully ran go mod tidy") } return nil @@ -610,7 +623,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { oc.Printf("Building tsunami app from %s", opts.AppPath) if opts.Verbose || opts.KeepTemp { - oc.Printf("Temp dir: %s", tempDir) + oc.Printf("[debug] Temp dir: %s", tempDir) } // Copy files from app path (go.mod, go.sum, static/, *.go) @@ -626,7 +639,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { } if opts.Verbose { - oc.Printf("Copied %d go files, %d static files, %d scaffold files (go.mod: %t, go.sum: %t)", + oc.Printf("[debug] Copied %d go files, %d static files, %d scaffold files (go.mod: %t, go.sum: %t)", copyStats.GoFiles, copyStats.StaticFiles, scaffoldCount, copyStats.GoMod, copyStats.GoSum) } @@ -639,18 +652,18 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { // Create go.mod file appName := GetAppName(opts.AppPath) - if err := createGoMod(tempDir, appName, buildEnv.GoVersion, opts, opts.Verbose); err != nil { - return buildEnv, fmt.Errorf("failed to create go.mod: %w", err) + if err := createGoMod(tempDir, opts.AppNS, appName, buildEnv.GoVersion, opts, opts.Verbose); err != nil { + return buildEnv, err } // Generate Tailwind CSS if err := generateAppTailwindCss(tempDir, opts.Verbose, opts); err != nil { - return buildEnv, fmt.Errorf("failed to generate tailwind css: %w", err) + return buildEnv, err } // Build the Go application if err := runGoBuild(tempDir, opts); err != nil { - return buildEnv, fmt.Errorf("failed to build application: %w", err) + return buildEnv, err } // Move generated files back to original directory @@ -740,20 +753,28 @@ func runGoBuild(tempDir string, opts BuildOpts) error { } // Build command with explicit go files - args := append([]string{"build", "-o", outputPath}, goFiles...) + args := append([]string{"build", "-o", outputPath}, ".") buildCmd := exec.Command("go", args...) buildCmd.Dir = tempDir if oc != nil || opts.Verbose { - oc.Printf("Running: %s", strings.Join(buildCmd.Args, " ")) + oc.Printf("[debug] Running: %s", strings.Join(buildCmd.Args, " ")) + oc.Printf("Building application...") + } + if oc != nil { buildCmd.Stdout = oc buildCmd.Stderr = oc + } else { + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr } if err := buildCmd.Run(); err != nil { - return fmt.Errorf("failed to build application: %w", err) + return fmt.Errorf("compilation failed (see output for errors)") + } + if oc != nil { + oc.Flush() } - oc.Flush() if opts.Verbose { if opts.OutputFile != "" { @@ -770,7 +791,6 @@ func generateAppTailwindCss(tempDir string, verbose bool, opts BuildOpts) error oc := opts.OutputCapture // tailwind.css is already in tempDir from scaffold copy tailwindOutput := filepath.Join(tempDir, "static", "tw.css") - tailwindCmd := exec.Command(opts.getNodePath(), "node_modules/@tailwindcss/cli/dist/index.mjs", "-i", "./tailwind.css", "-o", tailwindOutput) @@ -778,17 +798,35 @@ func generateAppTailwindCss(tempDir string, verbose bool, opts BuildOpts) error tailwindCmd.Env = append(os.Environ(), "ELECTRON_RUN_AS_NODE=1") if verbose { - oc.Printf("Running: %s", strings.Join(tailwindCmd.Args, " ")) + oc.Printf("[debug] Running: %s", strings.Join(tailwindCmd.Args, " ")) } - if err := tailwindCmd.Run(); err != nil { - return fmt.Errorf("failed to run tailwind command: %w", err) + output, err := tailwindCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("tailwind CSS generation failed (see output for errors)") } + // Process and filter tailwind output + lines := strings.Split(string(output), "\n") + for _, line := range lines { + // Skip empty lines + if strings.TrimSpace(line) == "" { + continue + } + // Skip version line (contains ≈ and tailwindcss) + if strings.Contains(line, "≈") && strings.Contains(line, "tailwindcss") { + continue + } + // Skip "Done in" timing line + if strings.HasPrefix(strings.TrimSpace(line), "Done in") { + continue + } + // Write remaining lines to output + oc.Printf("%s", line) + } if verbose { oc.Printf("Tailwind CSS generated successfully") } - return nil } @@ -1000,7 +1038,7 @@ func copyScaffoldFS(scaffoldFS fs.FS, destDir string, verbose bool, oc *OutputCa return 0, fmt.Errorf("failed to create symlink for node_modules: %w", err) } if verbose { - oc.Printf("Symlinked node_modules directory") + oc.Printf("[debug] Symlinked node_modules directory") } fileCount++ } else {