cleaned up build output w/ "debug" lines and filtering (#2545)

This commit is contained in:
Mike Sawka 2025-11-11 11:53:57 -08:00 committed by GitHub
parent 3a08c5518d
commit eb3ba64121
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 296 additions and 151 deletions

View file

@ -363,7 +363,7 @@ class RpcApiType {
}
// command "listalleditableapps" [call]
ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise<string[]> {
ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise<AppInfo[]> {
return client.wshRpcCall("listalleditableapps", null, opts);
}

View file

@ -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<string[]>([]);
const [loading, setLoading] = useState(true);
function CreateNewWaveApp({ onCreateApp }: { onCreateApp: (appName: string) => Promise<void> }) {
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 (
<div className="min-h-[80px]">
<h3 className="text-base font-medium mb-1 text-muted-foreground">Create New WaveApp</h3>
<div className="relative">
<div className="flex w-full">
<input
type="text"
value={newAppName}
onChange={(e) => {
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}
/>
<button
onClick={handleCreate}
disabled={!newAppName.trim() || !!inputError || isCreating}
className={`px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
!newAppName.trim() || inputError || isCreating
? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
: "bg-accent text-black hover:bg-accent-hover cursor-pointer"
}`}
>
Create
</button>
</div>
{inputError && (
<div className="absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap">
<i className="fa-solid fa-circle-exclamation"></i>
<span>{inputError}</span>
</div>
)}
</div>
</div>
);
}
export function AppSelectionModal() {
const [apps, setApps] = useState<AppInfo[]>([]);
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 (
<FlexiModal className="min-w-[600px] w-[600px] max-h-[80vh] overflow-y-auto">
<FlexiModal className="min-w-[600px] w-[600px] max-h-[90vh] overflow-y-auto">
<div className="w-full px-2 pt-0 pb-4">
<h2 className="text-2xl mb-6">Select a WaveApp to Edit</h2>
<h2 className="text-2xl mb-2">Select a WaveApp to Edit</h2>
{error && (
<div className="mb-6 px-4 py-3 bg-panel rounded">
@ -125,18 +176,23 @@ export function AppSelectionModal() {
)}
{apps.length > 0 && (
<div className="mb-6">
<h3 className="text-base font-medium mb-3 text-muted-foreground">Existing WaveApps</h3>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{apps.map((appId) => (
<div className="mb-2">
<h3 className="text-base font-medium mb-1 text-muted-foreground">Existing WaveApps</h3>
<div className="space-y-2 max-h-[220px] overflow-y-auto">
{apps.map((appInfo) => (
<button
key={appId}
onClick={() => handleSelectApp(appId)}
className="w-full text-left px-4 py-3 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
key={appInfo.appid}
onClick={() => handleSelectApp(appInfo.appid)}
className="w-full text-left px-4 py-1.5 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
>
<div className="flex items-center gap-3">
<i className="fa-solid fa-cube"></i>
<span>{getAppDisplayName(appId)}</span>
<i className="fa-solid fa-cube self-center"></i>
<div className="flex flex-col">
<span>{getAppDisplayName(appInfo.appid)}</span>
<span className="text-[11px] text-muted mt-0.5">
Last updated: {formatRelativeTime(appInfo.modtime)}
</span>
</div>
</div>
</button>
))}
@ -145,62 +201,14 @@ export function AppSelectionModal() {
)}
{apps.length > 0 && (
<div className="flex items-center gap-4 my-6">
<div className="flex items-center gap-4 my-2">
<div className="flex-1 border-t border-border"></div>
<span className="text-muted-foreground text-sm">or</span>
<div className="flex-1 border-t border-border"></div>
</div>
)}
<div className="min-h-[80px]">
<h3 className="text-base font-medium mb-4 text-muted-foreground">Create New WaveApp</h3>
<div className="relative">
<div className="flex w-full">
<input
type="text"
value={newAppName}
onChange={(e) => {
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
/>
<button
onClick={handleCreateNew}
disabled={!newAppName.trim() || !!inputError}
className={`px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
!newAppName.trim() || inputError
? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
: "bg-accent text-black hover:bg-accent-hover cursor-pointer"
}`}
>
Create
</button>
</div>
{inputError && (
<div className="absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap">
<i className="fa-solid fa-circle-exclamation"></i>
<span>{inputError}</span>
</div>
)}
</div>
</div>
<CreateNewWaveApp onCreateApp={handleCreateNew} />
</div>
</FlexiModal>
);

View file

@ -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<HTMLDivElement>(null);
const preRef = useRef<HTMLPreElement>(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 (
<div className="w-full h-full flex flex-col bg-black">
<div className="flex-shrink-0 px-3 py-2 border-b border-gray-700">
<div className="flex-shrink-0 px-3 py-2 border-b border-gray-700 flex items-center justify-between">
<span className="text-sm font-semibold text-gray-300">Build Output</span>
<label className="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input
type="checkbox"
checked={showDebug}
onChange={handleDebugToggle}
className="cursor-pointer"
/>
Debug
</label>
</div>
<div ref={scrollRef} className="flex-1 overflow-y-auto overflow-x-auto p-2">
<pre
@ -83,10 +100,11 @@ const BuilderBuildPanel = memo(() => {
onMouseUp={handleMouseUp}
onContextMenu={handleContextMenu}
>
{outputLines.length === 0 ? (
{/* this comment fixes JSX blank line in pre tag */}
{filteredLines.length === 0 ? (
<span className="text-secondary">Waiting for output...</span>
) : (
outputLines.join("\n")
filteredLines.join("\n")
)}
</pre>
</div>

View file

@ -12,6 +12,7 @@ export class BuilderBuildPanelModel {
private static instance: BuilderBuildPanelModel | null = null;
outputLines: PrimitiveAtom<string[]> = atom<string[]>([]);
showDebug: PrimitiveAtom<boolean> = atom<boolean>(false);
outputUnsubFn: (() => void) | null = null;
initialized = false;

View file

@ -56,6 +56,12 @@ declare global {
message?: string;
};
// wshrpc.AppInfo
type AppInfo = {
appid: string;
modtime: number;
};
// waveobj.Block
type Block = WaveObj & {
parentoref?: string;

View file

@ -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,

View file

@ -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))

View file

@ -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) {

View file

@ -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

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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 {