mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-23 08:48:28 +00:00
cleaned up build output w/ "debug" lines and filtering (#2545)
This commit is contained in:
parent
3a08c5518d
commit
eb3ba64121
14 changed files with 296 additions and 151 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
6
frontend/types/gotypes.d.ts
vendored
6
frontend/types/gotypes.d.ts
vendored
|
|
@ -56,6 +56,12 @@ declare global {
|
|||
message?: string;
|
||||
};
|
||||
|
||||
// wshrpc.AppInfo
|
||||
type AppInfo = {
|
||||
appid: string;
|
||||
modtime: number;
|
||||
};
|
||||
|
||||
// waveobj.Block
|
||||
type Block = WaveObj & {
|
||||
parentoref?: string;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue