From ba3d05c2fcccd9f31a6d0a0f1867d12a56f07834 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 9 May 2026 10:38:47 +0200 Subject: [PATCH] feat(cli): auto-infer args for `wmill app push` (#9091) Run `wmill app push` from inside an app folder (e.g. `f/foo/my_app.app/`) with no args. The local path defaults to CWD, and the remote path is derived from CWD relative to `wmill.yaml`, with `.app`/`.raw_app`/ `__app`/`__raw_app` suffixes stripped. Either, both, or neither positional argument can be passed. Also resolves `file_path` against the user's original CWD before `resolveWorkspace` may chdir to the wmill.yaml root, so a relative `file_path` argument is interpreted from where the user invoked the command (previously it could resolve against the wrong directory). Co-authored-by: Claude Opus 4.7 (1M context) --- cli/src/commands/app/app.ts | 131 +++++++++++++++--- cli/src/guidance/skills.gen.ts | 2 +- .../auto-generated/cli/cli-commands.md | 2 +- system_prompts/auto-generated/prompts.ts | 2 +- .../skills/cli-commands/SKILL.md | 2 +- 5 files changed, 117 insertions(+), 22 deletions(-) diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index f99df84636..2749c0269c 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -4,7 +4,7 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; import * as log from "../../core/log.ts"; -import { sep as SEP } from "node:path"; +import { sep as SEP, isAbsolute, resolve as pathResolve, relative as pathRelative, basename } from "node:path"; import { stat } from "node:fs/promises"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { yamlParseFile } from "../../utils/yaml.ts"; @@ -12,6 +12,7 @@ import * as wmill from "../../../gen/services.gen.ts"; import { ListableApp, Policy } from "../../../gen/types.gen.ts"; import { GlobalOptions, isSuperset } from "../../types.ts"; +import { getWmillYamlPath } from "../../core/conf.ts"; import { readInlinePathSync } from "../../utils/utils.ts"; import devCommand from "./dev.ts"; import lintCommand from "./lint.ts"; @@ -261,31 +262,122 @@ async function get(opts: GlobalOptions & { json?: boolean }, path: string) { } } -async function push(opts: GlobalOptions, filePath: string, remotePath: string) { - if (!validatePath(remotePath)) { - return; - } +const APP_FOLDER_SUFFIXES = ["__raw_app", ".raw_app", "__app", ".app"] as const; + +async function push( + opts: GlobalOptions, + filePath?: string, + remotePath?: string +) { + // Capture original CWD before resolveWorkspace, which may chdir to the + // wmill.yaml root. We need it to resolve relative inputs and to derive + // the remote path from the user's location when auto-inferring. + const originalCwd = process.cwd(); + const workspace = await resolveWorkspace(opts); await requireLogin(opts); - // Detect raw apps by checking for raw_app.yaml or __raw_app/.raw_app suffix - const normalizedPath = filePath.endsWith(SEP) ? filePath.slice(0, -1) : filePath; - const isRawApp = normalizedPath.endsWith("__raw_app") || normalizedPath.endsWith(".raw_app"); + // Auto-infer file path from CWD when omitted + if (!filePath) { + filePath = originalCwd; + } + const absoluteFilePath = isAbsolute(filePath) + ? filePath + : pathResolve(originalCwd, filePath); + + // Detect app folder type (regular vs raw) + const normalizedPath = absoluteFilePath.endsWith(SEP) + ? absoluteFilePath.slice(0, -1) + : absoluteFilePath; + const dirName = basename(normalizedPath); + const isRawAppByName = + dirName.endsWith("__raw_app") || dirName.endsWith(".raw_app"); + const isAppByName = dirName.endsWith("__app") || dirName.endsWith(".app"); + let hasRawAppYaml = false; - if (!isRawApp) { + let hasAppYaml = false; + try { + await stat(normalizedPath + SEP + "raw_app.yaml"); + hasRawAppYaml = true; + } catch { /* not a raw app */ } + if (!hasRawAppYaml) { try { - const rawAppPath = (filePath.endsWith(SEP) ? filePath : filePath + SEP) + "raw_app.yaml"; - await stat(rawAppPath); - hasRawAppYaml = true; - } catch { /* not a raw app */ } + await stat(normalizedPath + SEP + "app.yaml"); + hasAppYaml = true; + } catch { /* not an app */ } } - if (isRawApp || hasRawAppYaml) { + if (!isRawAppByName && !isAppByName && !hasRawAppYaml && !hasAppYaml) { + log.error( + colors.red( + `'${filePath}' is not an app folder (no app.yaml or raw_app.yaml, and not a *.app/*.raw_app folder).` + ) + ); + return; + } + + // Auto-infer remote path from the folder location relative to wmill.yaml root + if (!remotePath) { + const wmillYamlPath = getWmillYamlPath(); + if (!wmillYamlPath) { + log.error( + colors.red( + "Could not infer remote path: no wmill.yaml found. Run 'wmill init' or pass explicitly." + ) + ); + return; + } + // After resolveWorkspace, process.cwd() is the wmill.yaml dir + const wmillRoot = process.cwd(); + let inferred = pathRelative(wmillRoot, normalizedPath).replaceAll(SEP, "/"); + if (inferred.startsWith("..") || isAbsolute(inferred)) { + log.error( + colors.red( + `Could not infer remote path: '${filePath}' is outside the wmill.yaml root (${wmillRoot}). Move the folder under the root or pass explicitly.` + ) + ); + return; + } + for (const suffix of APP_FOLDER_SUFFIXES) { + if (inferred.endsWith(suffix)) { + inferred = inferred.slice(0, -suffix.length); + break; + } + } + if (!inferred) { + log.error( + colors.red( + "Could not infer remote path: app folder is at the wmill.yaml root. Pass explicitly." + ) + ); + return; + } + if ( + !inferred.startsWith("u/") && + !inferred.startsWith("g/") && + !inferred.startsWith("f/") + ) { + log.error( + colors.red( + `Could not infer remote path: '${inferred}' is not under u/, g/, or f/. Move the app under one of these prefixes or pass explicitly.` + ) + ); + return; + } + remotePath = inferred; + log.info(colors.gray(`Inferred remote path: ${remotePath}`)); + } + + if (!validatePath(remotePath)) { + return; + } + + if (isRawAppByName || hasRawAppYaml) { const { pushRawApp } = await import("./raw_apps.ts"); - await pushRawApp(workspace.workspaceId, remotePath, filePath); + await pushRawApp(workspace.workspaceId, remotePath, absoluteFilePath); log.info(colors.bold.underline.green("Raw app pushed")); } else { - await pushApp(workspace.workspaceId, remotePath, filePath); + await pushApp(workspace.workspaceId, remotePath, absoluteFilePath); log.info(colors.bold.underline.green("App pushed")); } } @@ -301,8 +393,11 @@ const command = new Command() .arguments("") .option("--json", "Output as JSON (for piping to jq)") .action(get as any) - .command("push", "push a local app ") - .arguments(" ") + .command( + "push", + "push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml." + ) + .arguments("[file_path:string] [remote_path:string]") .action(push as any) .command("dev", devCommand) .command("lint", lintCommand) diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index ede19be85b..72d2e9d76b 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -6588,7 +6588,7 @@ app related commands - \`--json\` - Output as JSON (for piping to jq) - \`app get \` - get an app's details - \`--json\` - Output as JSON (for piping to jq) -- \`app push \` - push a local app +- \`app push [file_path:string] [remote_path:string]\` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml. - \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement - \`--port \` - Port to run the dev server on (will find next available port if occupied) - \`--host \` - Host to bind the dev server to diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index 692e2dd310..dc0eb66640 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -26,7 +26,7 @@ app related commands - `--json` - Output as JSON (for piping to jq) - `app get ` - get an app's details - `--json` - Output as JSON (for piping to jq) -- `app push ` - push a local app +- `app push [file_path:string] [remote_path:string]` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml. - `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement - `--port ` - Port to run the dev server on (will find next available port if occupied) - `--host ` - Host to bind the dev server to diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 83f6ae1157..53336762cc 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -2174,7 +2174,7 @@ app related commands - \`--json\` - Output as JSON (for piping to jq) - \`app get \` - get an app's details - \`--json\` - Output as JSON (for piping to jq) -- \`app push \` - push a local app +- \`app push [file_path:string] [remote_path:string]\` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml. - \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement - \`--port \` - Port to run the dev server on (will find next available port if occupied) - \`--host \` - Host to bind the dev server to diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index b4818b4fa1..98ce52efde 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -31,7 +31,7 @@ app related commands - `--json` - Output as JSON (for piping to jq) - `app get ` - get an app's details - `--json` - Output as JSON (for piping to jq) -- `app push ` - push a local app +- `app push [file_path:string] [remote_path:string]` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml. - `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement - `--port ` - Port to run the dev server on (will find next available port if occupied) - `--host ` - Host to bind the dev server to