fix tsunami scaffold in build (#2564)

This commit is contained in:
Mike Sawka 2025-11-14 16:35:37 -08:00 committed by GitHub
parent 1a190da638
commit 3b97084471
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 145 additions and 129 deletions

View file

@ -536,6 +536,7 @@ tasks:
- mkdir -p scaffold
- cp ../templates/package.json.tmpl scaffold/package.json
- cd scaffold && npm install
- mv scaffold/node_modules scaffold/nm
- cp -r dist scaffold/
- mkdir -p scaffold/dist/tw
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
@ -556,6 +557,7 @@ tasks:
- powershell New-Item -ItemType Directory -Force -Path scaffold
- powershell Copy-Item -Path ../templates/package.json.tmpl -Destination scaffold/package.json
- powershell -Command "Set-Location scaffold; npm install"
- powershell Move-Item -Path scaffold/node_modules -Destination scaffold/nm
- powershell Copy-Item -Recurse -Force -Path dist -Destination scaffold/
- powershell New-Item -ItemType Directory -Force -Path scaffold/dist/tw
- powershell Copy-Item -Path ../templates/app-main.go.tmpl -Destination scaffold/app-main.go

View file

@ -12,6 +12,7 @@ Patch release with Wave AI model upgrade, new secret management features, and im
**Wave AI Updates:**
- **GPT-5.1 Model** - Upgraded to use OpenAI's GPT-5.1 model for improved responses
- **Thinking Mode Toggle** - New dropdown to select between Quick, Balanced, and Deep thinking modes for optimal response quality vs speed
- [bugfix] Fixed path mismatch issue when restoring AI write file backups
**New Features:**

View file

@ -22,7 +22,7 @@ const config = {
{
from: "./dist",
to: "./dist",
filter: ["**/*", "!bin/*", "bin/wavesrv.${arch}*", "bin/wsh*"],
filter: ["**/*", "!bin/*", "bin/wavesrv.${arch}*", "bin/wsh*", "!tsunamiscaffold/**/*"],
},
{
from: ".",
@ -31,13 +31,18 @@ const config = {
},
"!node_modules", // We don't need electron-builder to package in Node modules as Vite has already bundled any code that our program is using.
],
extraResources: [
{
from: "dist/tsunamiscaffold",
to: "tsunamiscaffold",
},
],
directories: {
output: "make",
},
asarUnpack: [
"dist/bin/**/*", // wavesrv and wsh binaries
"dist/schema/**/*", // schema files for Monaco editor
"dist/tsunamiscaffold/**/*", // tsunami scaffold files
],
mac: {
target: [

View file

@ -110,7 +110,7 @@ function makeEditMenu(): Electron.MenuItemConstructorOptions[] {
];
}
function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks): Electron.MenuItemConstructorOptions[] {
function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks, fullConfig: FullConfigType): Electron.MenuItemConstructorOptions[] {
const fileMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "New Window",
@ -125,7 +125,8 @@ function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks): Elec
},
},
];
if (isDev) {
const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"];
if (isDev || featureWaveAppBuilder) {
fileMenu.splice(1, 0, {
label: "New WaveApp Builder Window",
accelerator: unamePlatform === "darwin" ? "Command+Shift+B" : "Alt+Shift+B",
@ -310,18 +311,19 @@ function makeViewMenu(
async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId?: string): Promise<Electron.Menu> {
const numWaveWindows = getAllWaveWindows().length;
const webContents = workspaceOrBuilderId && getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId);
const fileMenu = makeFileMenu(numWaveWindows, callbacks);
const appMenuItems = makeAppMenuItems(webContents);
const editMenu = makeEditMenu();
const isBuilderWindowFocused = focusedBuilderWindow != null;
let fullscreenOnLaunch = false;
let fullConfig: FullConfigType = null;
try {
const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
fullscreenOnLaunch = fullConfig?.settings["window:fullscreenonlaunch"];
} catch (e) {
console.error("Error fetching fullscreen launch config:", e);
console.error("Error fetching config:", e);
}
const fileMenu = makeFileMenu(numWaveWindows, callbacks, fullConfig);
const viewMenu = makeViewMenu(webContents, callbacks, isBuilderWindowFocused, fullscreenOnLaunch);
let workspaceMenu: Electron.MenuItemConstructorOptions[] = null;
try {

View file

@ -149,6 +149,7 @@ function getWaveDataDir(): string {
}
function getElectronAppBasePath(): string {
// import.meta.dirname in dev points to waveterm/dist/main
return path.dirname(import.meta.dirname);
}
@ -156,6 +157,14 @@ function getElectronAppUnpackedBasePath(): string {
return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked");
}
function getElectronAppResourcesPath(): string {
if (isDev) {
// import.meta.dirname in dev points to waveterm/dist/main
return path.dirname(import.meta.dirname);
}
return process.resourcesPath;
}
const wavesrvBinName = `wavesrv.${unameArch}`;
function getWaveSrvPath(): string {
@ -261,6 +270,7 @@ export {
callWithOriginalXdgCurrentDesktop,
callWithOriginalXdgCurrentDesktopAsync,
getElectronAppBasePath,
getElectronAppResourcesPath,
getElectronAppUnpackedBasePath,
getWaveConfigDir,
getWaveDataDir,

View file

@ -5,6 +5,7 @@ import * as electron from "electron";
import { getWebServerEndpoint } from "../frontend/util/endpoints";
export const WaveAppPathVarName = "WAVETERM_APP_PATH";
export const WaveAppResourcesPathVarName = "WAVETERM_RESOURCES_PATH";
export const WaveAppElectronExecPath = "WAVETERM_ELECTRONEXECPATH";
export function getElectronExecPath(): string {

View file

@ -8,6 +8,7 @@ import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/u
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
import { setForceQuit } from "./emain-activity";
import {
getElectronAppResourcesPath,
getElectronAppUnpackedBasePath,
getWaveConfigDir,
getWaveDataDir,
@ -17,7 +18,12 @@ import {
WaveConfigHomeVarName,
WaveDataHomeVarName,
} from "./emain-platform";
import { getElectronExecPath, WaveAppElectronExecPath, WaveAppPathVarName } from "./emain-util";
import {
getElectronExecPath,
WaveAppElectronExecPath,
WaveAppPathVarName,
WaveAppResourcesPathVarName,
} from "./emain-util";
import { updater } from "./updater";
let isWaveSrvDead = false;
@ -59,6 +65,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
envCopy["XDG_CURRENT_DESKTOP"] = xdgCurrentDesktop;
}
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
envCopy[WaveAppResourcesPathVarName] = getElectronAppResourcesPath();
envCopy[WaveAppElectronExecPath] = getElectronExecPath();
envCopy[WaveAuthKeyEnv] = AuthKey;
envCopy[WaveDataHomeVarName] = getWaveDataDir();

View file

@ -62,14 +62,8 @@ class TsunamiViewModel extends WebViewModel {
oref: WOS.makeORef("block", blockId),
});
initialRTInfo.then((rtInfo) => {
if (rtInfo) {
const meta: AppMeta = {
title: rtInfo["tsunami:title"],
shortdesc: rtInfo["tsunami:shortdesc"],
icon: rtInfo["tsunami:icon"],
iconcolor: rtInfo["tsunami:iconcolor"],
};
globalStore.set(this.appMeta, meta);
if (rtInfo && rtInfo["tsunami:appmeta"]) {
globalStore.set(this.appMeta, rtInfo["tsunami:appmeta"]);
}
});
this.appMetaUnsubFn = waveEventSubscribe({

View file

@ -229,6 +229,7 @@ const Widgets = memo(() => {
magnified: true,
};
const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true;
const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"] ?? false;
const widgetsMap = fullConfig?.widgets ?? {};
const filteredWidgets = hasCustomAIPresets
? widgetsMap
@ -342,9 +343,9 @@ const Widgets = memo(() => {
))}
</div>
<div className="flex-grow" />
{isDev() || showHelp ? (
{isDev() || featureWaveAppBuilder || showHelp ? (
<div className="grid grid-cols-2 gap-0 w-full">
{isDev() ? (
{isDev() || featureWaveAppBuilder ? (
<div
ref={appsButtonRef}
className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-sm overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
@ -372,7 +373,7 @@ const Widgets = memo(() => {
<Widget key={`widget-${idx}`} widget={data} mode={mode} />
))}
<div className="flex-grow" />
{isDev() ? (
{isDev() || featureWaveAppBuilder ? (
<div
ref={appsButtonRef}
className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-lg overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
@ -407,7 +408,7 @@ const Widgets = memo(() => {
</div>
) : null}
</div>
{isDev() && appsButtonRef.current && (
{(isDev() || featureWaveAppBuilder) && appsButtonRef.current && (
<AppsFloatingWindow
isOpen={isAppsOpen}
onClose={() => setIsAppsOpen(false)}

View file

@ -914,10 +914,7 @@ declare global {
// waveobj.ObjRTInfo
type ObjRTInfo = {
"tsunami:title"?: string;
"tsunami:shortdesc"?: string;
"tsunami:icon"?: string;
"tsunami:iconcolor"?: string;
"tsunami:appmeta"?: AppMeta;
"tsunami:schemas"?: any;
"shell:hascurcwd"?: boolean;
"shell:state"?: string;
@ -1030,6 +1027,7 @@ declare global {
"app:dismissarchitecturewarning"?: boolean;
"app:defaultnewblock"?: string;
"app:showoverlayblocknums"?: boolean;
"feature:waveappbuilder"?: boolean;
"ai:*"?: boolean;
"ai:preset"?: string;
"ai:apitype"?: string;

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "waveterm",
"version": "0.12.2",
"version": "0.12.3-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "waveterm",
"version": "0.12.2",
"version": "0.12.3-beta.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"workspaces": [

View file

@ -13,10 +13,23 @@ import (
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
func getTsunamiShortDesc(rtInfo *waveobj.ObjRTInfo) string {
if rtInfo == nil || rtInfo.TsunamiAppMeta == nil {
return ""
}
var appMeta wshrpc.AppMeta
if err := utilfn.ReUnmarshal(&appMeta, rtInfo.TsunamiAppMeta); err == nil && appMeta.ShortDesc != "" {
return appMeta.ShortDesc
}
return ""
}
func handleTsunamiBlockDesc(block *waveobj.Block) string {
status := blockcontroller.GetBlockControllerRuntimeStatus(block.OID)
if status == nil || status.ShellProcStatus != blockcontroller.Status_Running {
@ -25,8 +38,8 @@ func handleTsunamiBlockDesc(block *waveobj.Block) string {
blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID)
rtInfo := wstore.GetRTInfo(blockORef)
if rtInfo != nil && rtInfo.TsunamiShortDesc != "" {
return fmt.Sprintf("tsunami widget - %s", rtInfo.TsunamiShortDesc)
if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" {
return fmt.Sprintf("tsunami widget - %s", shortDesc)
}
return "tsunami widget - unknown description"
}
@ -111,8 +124,8 @@ func GetTsunamiGetDataToolDefinition(block *waveobj.Block, rtInfo *waveobj.ObjRT
toolName := fmt.Sprintf("tsunami_getdata_%s", blockIdPrefix)
desc := "tsunami widget"
if rtInfo != nil && rtInfo.TsunamiShortDesc != "" {
desc = rtInfo.TsunamiShortDesc
if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" {
desc = shortDesc
}
return &uctypes.ToolDefinition{
@ -136,8 +149,8 @@ func GetTsunamiGetConfigToolDefinition(block *waveobj.Block, rtInfo *waveobj.Obj
toolName := fmt.Sprintf("tsunami_getconfig_%s", blockIdPrefix)
desc := "tsunami widget"
if rtInfo != nil && rtInfo.TsunamiShortDesc != "" {
desc = rtInfo.TsunamiShortDesc
if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" {
desc = shortDesc
}
return &uctypes.ToolDefinition{
@ -174,8 +187,8 @@ func GetTsunamiSetConfigToolDefinition(block *waveobj.Block, rtInfo *waveobj.Obj
}
desc := "tsunami widget"
if rtInfo != nil && rtInfo.TsunamiShortDesc != "" {
desc = rtInfo.TsunamiShortDesc
if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" {
desc = shortDesc
}
return &uctypes.ToolDefinition{

View file

@ -5,18 +5,15 @@ package blockcontroller
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"github.com/wavetermdev/waveterm/pkg/tsunamiutil"
"github.com/wavetermdev/waveterm/pkg/utilds"
@ -51,37 +48,31 @@ type TsunamiController struct {
port int
}
func (c *TsunamiController) fetchAndSetSchemas(port int) {
url := fmt.Sprintf("http://localhost:%d/api/schemas", port)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
func (c *TsunamiController) setManifestMetadata(appId string) {
manifest, err := waveappstore.ReadAppManifest(appId)
if err != nil {
log.Printf("TsunamiController: failed to fetch schemas from %s: %v", url, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("TsunamiController: received non-200 status %d from %s", resp.StatusCode, url)
return
}
var schemas any
if err := json.NewDecoder(resp.Body).Decode(&schemas); err != nil {
log.Printf("TsunamiController: failed to decode schemas response: %v", err)
return
}
blockRef := waveobj.MakeORef(waveobj.OType_Block, c.blockId)
wstore.SetRTInfo(blockRef, map[string]any{
"tsunami:schemas": schemas,
rtInfo := make(map[string]any)
rtInfo["tsunami:appmeta"] = manifest.AppMeta
if manifest.ConfigSchema != nil || manifest.DataSchema != nil {
schemas := make(map[string]any)
if manifest.ConfigSchema != nil {
schemas["config"] = manifest.ConfigSchema
}
if manifest.DataSchema != nil {
schemas["data"] = manifest.DataSchema
}
rtInfo["tsunami:schemas"] = schemas
}
wstore.SetRTInfo(blockRef, rtInfo)
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_TsunamiUpdateMeta,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, c.blockId).String()},
Data: manifest.AppMeta,
})
log.Printf("TsunamiController: successfully fetched and cached schemas for block %s", c.blockId)
}
func (c *TsunamiController) clearSchemas() {
@ -92,7 +83,6 @@ func (c *TsunamiController) clearSchemas() {
log.Printf("TsunamiController: cleared schemas for block %s", c.blockId)
}
func isBuildCacheUpToDate(appPath string) (bool, error) {
appName := build.GetAppName(appPath)
@ -136,7 +126,7 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap
appPath := blockMeta.GetString(waveobj.MetaKey_TsunamiAppPath, "")
appId := blockMeta.GetString(waveobj.MetaKey_TsunamiAppId, "")
if appPath == "" {
if appId == "" {
return fmt.Errorf("tsunami:apppath or tsunami:appid is required")
@ -157,32 +147,8 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap
}
}
// Read and set app metadata from manifest if appId is available
if appId != "" {
if manifest, err := waveappstore.ReadAppManifest(appId); err == nil {
blockRef := waveobj.MakeORef(waveobj.OType_Block, c.blockId)
rtInfo := make(map[string]any)
if manifest.AppMeta.Title != "" {
rtInfo["tsunami:title"] = manifest.AppMeta.Title
}
if manifest.AppMeta.ShortDesc != "" {
rtInfo["tsunami:shortdesc"] = manifest.AppMeta.ShortDesc
}
if manifest.AppMeta.Icon != "" {
rtInfo["tsunami:icon"] = manifest.AppMeta.Icon
}
if manifest.AppMeta.IconColor != "" {
rtInfo["tsunami:iconcolor"] = manifest.AppMeta.IconColor
}
if len(rtInfo) > 0 {
wstore.SetRTInfo(blockRef, rtInfo)
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_TsunamiUpdateMeta,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, c.blockId).String()},
Data: manifest.AppMeta,
})
}
}
c.setManifestMetadata(appId)
}
appName := build.GetAppName(appPath)
@ -220,6 +186,7 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap
err = build.TsunamiBuild(opts)
if err != nil {
log.Printf("TsunamiController build error for block %s: %v", c.blockId, err)
log.Printf("BuildOpts %#v\n", opts)
return fmt.Errorf("failed to build tsunami app: %w", err)
}
}
@ -248,11 +215,6 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap
})
go c.sendStatusUpdate()
// Asynchronously fetch schemas after port is detected
go func() {
c.fetchAndSetSchemas(tsunamiProc.Port)
}()
// Monitor process completion
go func() {
<-tsunamiProc.WaitCh

View file

@ -22,7 +22,7 @@ func GetTsunamiScaffoldPath() string {
settings := wconfig.GetWatcher().GetFullConfig().Settings
scaffoldPath := settings.TsunamiScaffoldPath
if scaffoldPath == "" {
scaffoldPath = filepath.Join(wavebase.GetWaveAppPath(), "tsunamiscaffold")
scaffoldPath = filepath.Join(wavebase.GetWaveAppResourcesPath(), "tsunamiscaffold")
}
return scaffoldPath
}
@ -76,4 +76,4 @@ func FormatGoCode(contents []byte) []byte {
}
return formattedOutput
}
}

View file

@ -29,6 +29,7 @@ const (
WaveConfigHomeEnvVar = "WAVETERM_CONFIG_HOME"
WaveDataHomeEnvVar = "WAVETERM_DATA_HOME"
WaveAppPathVarName = "WAVETERM_APP_PATH"
WaveAppResourcesPathVarName = "WAVETERM_RESOURCES_PATH"
WaveAppElectronExecPathVarName = "WAVETERM_ELECTRONEXECPATH"
WaveDevVarName = "WAVETERM_DEV"
WaveDevViteVarName = "WAVETERM_DEV_VITE"
@ -50,6 +51,7 @@ const NeedJwtConst = "NEED-JWT"
var ConfigHome_VarCache string // caches WAVETERM_CONFIG_HOME
var DataHome_VarCache string // caches WAVETERM_DATA_HOME
var AppPath_VarCache string // caches WAVETERM_APP_PATH
var AppResourcesPath_VarCache string // caches WAVETERM_RESOURCES_PATH
var AppElectronExecPath_VarCache string // caches WAVETERM_ELECTRONEXECPATH
var Dev_VarCache string // caches WAVETERM_DEV
@ -98,6 +100,8 @@ func CacheAndRemoveEnvVars() error {
os.Unsetenv(WaveDataHomeEnvVar)
AppPath_VarCache = os.Getenv(WaveAppPathVarName)
os.Unsetenv(WaveAppPathVarName)
AppResourcesPath_VarCache = os.Getenv(WaveAppResourcesPathVarName)
os.Unsetenv(WaveAppResourcesPathVarName)
AppElectronExecPath_VarCache = os.Getenv(WaveAppElectronExecPathVarName)
os.Unsetenv(WaveAppElectronExecPathVarName)
Dev_VarCache = os.Getenv(WaveDevVarName)
@ -114,6 +118,10 @@ func GetWaveAppPath() string {
return AppPath_VarCache
}
func GetWaveAppResourcesPath() string {
return AppResourcesPath_VarCache
}
func GetWaveDataDir() string {
return DataHome_VarCache
}

View file

@ -4,11 +4,8 @@
package waveobj
type ObjRTInfo struct {
TsunamiTitle string `json:"tsunami:title,omitempty"`
TsunamiShortDesc string `json:"tsunami:shortdesc,omitempty"`
TsunamiIcon string `json:"tsunami:icon,omitempty"`
TsunamiIconColor string `json:"tsunami:iconcolor,omitempty"`
TsunamiSchemas any `json:"tsunami:schemas,omitempty"`
TsunamiAppMeta any `json:"tsunami:appmeta,omitempty" tstype:"AppMeta"`
TsunamiSchemas any `json:"tsunami:schemas,omitempty"`
ShellHasCurCwd bool `json:"shell:hascurcwd,omitempty"`
ShellState string `json:"shell:state,omitempty"`

View file

@ -12,6 +12,8 @@ const (
ConfigKey_AppDefaultNewBlock = "app:defaultnewblock"
ConfigKey_AppShowOverlayBlockNums = "app:showoverlayblocknums"
ConfigKey_FeatureWaveAppBuilder = "feature:waveappbuilder"
ConfigKey_AiClear = "ai:*"
ConfigKey_AiPreset = "ai:preset"
ConfigKey_AiApiType = "ai:apitype"

View file

@ -58,6 +58,8 @@ type SettingsType struct {
AppDefaultNewBlock string `json:"app:defaultnewblock,omitempty"`
AppShowOverlayBlockNums *bool `json:"app:showoverlayblocknums,omitempty"`
FeatureWaveAppBuilder bool `json:"feature:waveappbuilder,omitempty"`
AiClear bool `json:"ai:*,omitempty"`
AiPreset string `json:"ai:preset,omitempty"`
AiApiType string `json:"ai:apitype,omitempty"`

View file

@ -20,6 +20,9 @@
"app:showoverlayblocknums": {
"type": "boolean"
},
"feature:waveappbuilder": {
"type": "boolean"
},
"ai:*": {
"type": "boolean"
},

View file

@ -109,6 +109,7 @@ func GetAppName(appPath string) string {
type BuildEnv struct {
GoVersion string
GoPath string
TempDir string
cleanupOnce *sync.Once
}
@ -329,11 +330,12 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) {
return &BuildEnv{
GoVersion: goVersion,
GoPath: result.GoPath,
cleanupOnce: &sync.Once{},
}, nil
}
func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verbose bool) error {
func createGoMod(tempDir, appNS, appName string, buildEnv *BuildEnv, opts BuildOpts, verbose bool) error {
oc := opts.OutputCapture
if appNS == "" {
appNS = "app"
@ -372,7 +374,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb
return fmt.Errorf("failed to add module statement: %w", err)
}
if err := modFile.AddGoStmt(goVersion); err != nil {
if err := modFile.AddGoStmt(buildEnv.GoVersion); err != nil {
return fmt.Errorf("failed to add go version: %w", err)
}
@ -412,7 +414,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb
}
// Run go mod tidy to clean up dependencies
tidyCmd := exec.Command("go", "mod", "tidy")
tidyCmd := exec.Command(buildEnv.GoPath, "mod", "tidy")
tidyCmd.Dir = tempDir
if verbose {
@ -428,6 +430,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb
}
if err := tidyCmd.Run(); err != nil {
oc.Flush()
return fmt.Errorf("go mod tidy failed (see output for errors)")
}
@ -498,13 +501,13 @@ func verifyScaffoldFs(fsys fs.FS) error {
return fmt.Errorf("package.json check failed: %w", err)
}
// Check for node_modules directory
if err := isDirOrNotFoundFS(fsys, "node_modules"); err != nil {
return fmt.Errorf("node_modules directory check failed: %w", err)
// Check for nm directory
if err := isDirOrNotFoundFS(fsys, "nm"); err != nil {
return fmt.Errorf("nm (node_modules) directory check failed: %w", err)
}
info, err = fs.Stat(fsys, "node_modules")
info, err = fs.Stat(fsys, "nm")
if err != nil || !info.IsDir() {
return fmt.Errorf("node_modules directory must exist in scaffold")
return fmt.Errorf("nm (node_modules) directory must exist in scaffold")
}
return nil
@ -621,6 +624,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) {
buildEnv.TempDir = tempDir
oc.Printf("Building tsunami app from %s", opts.AppPath)
oc.Printf("[debug] using scaffold path %s", opts.ScaffoldPath)
if opts.Verbose || opts.KeepTemp {
oc.Printf("[debug] Temp dir: %s", tempDir)
@ -652,7 +656,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) {
// Create go.mod file
appName := GetAppName(opts.AppPath)
if err := createGoMod(tempDir, opts.AppNS, appName, buildEnv.GoVersion, opts, opts.Verbose); err != nil {
if err := createGoMod(tempDir, opts.AppNS, appName, buildEnv, opts, opts.Verbose); err != nil {
return buildEnv, err
}
@ -662,7 +666,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) {
}
// Build the Go application
outputPath, err := runGoBuild(tempDir, opts)
outputPath, err := runGoBuild(tempDir, buildEnv, opts)
if err != nil {
return buildEnv, err
}
@ -743,7 +747,7 @@ func moveFilesBack(tempDir, originalDir string, verbose bool, oc *OutputCapture)
return nil
}
func runGoBuild(tempDir string, opts BuildOpts) (string, error) {
func runGoBuild(tempDir string, buildEnv *BuildEnv, opts BuildOpts) (string, error) {
oc := opts.OutputCapture
var outputPath string
var absOutputPath string
@ -775,7 +779,7 @@ func runGoBuild(tempDir string, opts BuildOpts) (string, error) {
// Build command with explicit go files
args := append([]string{"build", "-o", outputPath}, ".")
buildCmd := exec.Command("go", args...)
buildCmd := exec.Command(buildEnv.GoPath, args...)
buildCmd.Dir = tempDir
if oc != nil || opts.Verbose {
@ -838,7 +842,8 @@ 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",
tailwindCmd := exec.Command(opts.getNodePath(), "--preserve-symlinks-main", "--preserve-symlinks",
"node_modules/@tailwindcss/cli/dist/index.mjs",
"-i", "./tailwind.css",
"-o", tailwindOutput)
tailwindCmd.Dir = tempDir
@ -1074,33 +1079,36 @@ func ParseTsunamiPort(line string) int {
func copyScaffoldFS(scaffoldFS fs.FS, destDir string, verbose bool, oc *OutputCapture) (int, error) {
fileCount := 0
// Handle node_modules directory - prefer symlink if possible, otherwise copy
if _, err := fs.Stat(scaffoldFS, "node_modules"); err == nil {
// Handle nm (node_modules) directory - prefer symlink if possible, otherwise copy
if _, err := fs.Stat(scaffoldFS, "nm"); err == nil {
destPath := filepath.Join(destDir, "node_modules")
// Try to create symlink if we have DirFS
symlinked := false
if dirFS, ok := scaffoldFS.(DirFS); ok {
srcPath := dirFS.JoinOS("node_modules")
if err := os.Symlink(srcPath, destPath); err != nil {
return 0, fmt.Errorf("failed to create symlink for node_modules: %w", err)
srcPath := dirFS.JoinOS("nm")
if err := os.Symlink(srcPath, destPath); err == nil {
if verbose {
oc.Printf("[debug] Symlinked nm to node_modules directory")
}
fileCount++
symlinked = true
}
if verbose {
oc.Printf("[debug] Symlinked node_modules directory")
}
fileCount++
} else {
// Fallback to recursive copy
dirCount, err := copyDirFromFS(scaffoldFS, "node_modules", destPath, false)
}
// Fallback to recursive copy if symlink failed or not attempted
if !symlinked {
dirCount, err := copyDirFromFS(scaffoldFS, "nm", destPath, false)
if err != nil {
return 0, fmt.Errorf("failed to copy node_modules directory: %w", err)
return 0, fmt.Errorf("failed to copy nm (node_modules) directory: %w", err)
}
if verbose {
oc.Printf("Copied node_modules directory (%d files)", dirCount)
oc.Printf("Copied nm to node_modules directory (%d files)", dirCount)
}
fileCount += dirCount
}
} else if !os.IsNotExist(err) {
return 0, fmt.Errorf("error checking node_modules: %w", err)
return 0, fmt.Errorf("error checking nm (node_modules): %w", err)
}
// Copy package files instead of symlinking