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) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-09 21:21:45 +00:00
co-authored by Claude Opus 4.7
parent 2c5d661f9c
commit ba3d05c2fc
5 changed files with 117 additions and 22 deletions
+113 -18
View File
@@ -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 <remote_path> 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 <remote_path> 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 <remote_path> 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 <remote_path> 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("<path:string>")
.option("--json", "Output as JSON (for piping to jq)")
.action(get as any)
.command("push", "push a local app ")
.arguments("<file_path:string> <remote_path:string>")
.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)
+1 -1
View File
@@ -6588,7 +6588,7 @@ app related commands
- \`--json\` - Output as JSON (for piping to jq)
- \`app get <path:string>\` - get an app's details
- \`--json\` - Output as JSON (for piping to jq)
- \`app push <file_path:string> <remote_path:string>\` - 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:number>\` - Port to run the dev server on (will find next available port if occupied)
- \`--host <host:string>\` - Host to bind the dev server to
@@ -26,7 +26,7 @@ app related commands
- `--json` - Output as JSON (for piping to jq)
- `app get <path:string>` - get an app's details
- `--json` - Output as JSON (for piping to jq)
- `app push <file_path:string> <remote_path:string>` - 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:number>` - Port to run the dev server on (will find next available port if occupied)
- `--host <host:string>` - Host to bind the dev server to
+1 -1
View File
@@ -2174,7 +2174,7 @@ app related commands
- \`--json\` - Output as JSON (for piping to jq)
- \`app get <path:string>\` - get an app's details
- \`--json\` - Output as JSON (for piping to jq)
- \`app push <file_path:string> <remote_path:string>\` - 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:number>\` - Port to run the dev server on (will find next available port if occupied)
- \`--host <host:string>\` - Host to bind the dev server to
@@ -31,7 +31,7 @@ app related commands
- `--json` - Output as JSON (for piping to jq)
- `app get <path:string>` - get an app's details
- `--json` - Output as JSON (for piping to jq)
- `app push <file_path:string> <remote_path:string>` - 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:number>` - Port to run the dev server on (will find next available port if occupied)
- `--host <host:string>` - Host to bind the dev server to