feat(cli): detect missing folders on sync push and add 'wmill folder add-missing' (#8011)
* fix: auto-create missing folders during sync push for non-admin users Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show missing folders in sync push summary before confirmation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve sync push folder auto-creation error handling and json output Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: only treat 404 as missing folder in getFolder check Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove obsolete Deno compatibility layer from yaml-validator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(cli): add @types/bun dev dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): replace auto-create folders with `wmill folder add-missing` command Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): improve folder commands with summary field and simpler push API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): add confirmation prompt to folder add-missing command Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(cli): simplify missing folder check to use local stat instead of remote API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * update skills * feat(cli): warn admins but block non-admins on missing folder.meta.yaml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * cleaning * cleaning * test(cli): add tests for missing folder detection and folder commands - Add tests for `folder new`, `folder push`, `folder add-missing` commands - Add tests for sync push missing folder.meta.yaml detection (admin warning, non-admin block) - Fix getBasePostgresUrl to strip query params (e.g. ?sslmode=disable) from DATABASE_URL - Add createNonAdminUser and runCLIWithToken test utilities to test_backend.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(cli): unify runCLICommand with optional token parameter Replace separate runCLIWithToken utility with an optional { workspace?, token? } options object on the existing runCLICommand across all backends. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * own workspace * test(cli): isolate folder_missing_meta tests with per-test workspace * test(cli): shorten isolated workspace id/name for workspace limits * test(cli): archive temp isolated workspaces after each folder test --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,39 +112,15 @@ source <(wmill completions zsh)
|
|||||||
|
|
||||||
### Testing with a local `windmill-yaml-validator`
|
### Testing with a local `windmill-yaml-validator`
|
||||||
|
|
||||||
The CLI imports `windmill-yaml-validator` from npm (`npm:windmill-yaml-validator@1.1.0`).
|
To test local changes to the validator before publishing, use `npm link`:
|
||||||
To test local changes to the validator before publishing, use the Deno compatibility
|
|
||||||
script and import map override:
|
|
||||||
|
|
||||||
1. Make the validator sources Deno-compatible:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ../windmill-yaml-validator
|
# In windmill-yaml-validator/
|
||||||
./deno-compat.sh
|
npm run build
|
||||||
```
|
npm link
|
||||||
|
|
||||||
2. Add the following entries to `cli/deno.json` imports:
|
# In cli/
|
||||||
|
npm link windmill-yaml-validator
|
||||||
```json
|
|
||||||
"npm:windmill-yaml-validator@1.1.0": "../windmill-yaml-validator/src/index.ts",
|
|
||||||
"ajv": "npm:ajv@^8.17.1",
|
|
||||||
"@stoplight/yaml": "npm:@stoplight/yaml@^4.3.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the CLI directly with Deno:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deno run -A src/main.ts lint
|
|
||||||
```
|
|
||||||
|
|
||||||
4. When done, restore everything:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restore validator sources
|
|
||||||
cd ../windmill-yaml-validator
|
|
||||||
./deno-compat.sh -r
|
|
||||||
|
|
||||||
# Remove the 3 import map lines from cli/deno.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
@@ -156,13 +132,13 @@ cd ../windmill-yaml-validator
|
|||||||
**Run tests locally (full features):**
|
**Run tests locally (full features):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
deno test --allow-all --no-check
|
bun test test/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Run tests in CI mode (minimal features, skips EE tests):**
|
**Run tests in CI mode (minimal features, skips EE tests):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CI_MINIMAL_FEATURES=true deno test --allow-all --no-check
|
CI_MINIMAL_FEATURES=true bun test test/
|
||||||
```
|
```
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.9",
|
||||||
"@types/diff": "^5.2.3",
|
"@types/diff": "^5.2.3",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/tar-stream": "^3.1.4",
|
"@types/tar-stream": "^3.1.4",
|
||||||
@@ -151,6 +152,8 @@
|
|||||||
|
|
||||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
"@types/diff": ["@types/diff@5.2.3", "", {}, "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ=="],
|
"@types/diff": ["@types/diff@5.2.3", "", {}, "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
@@ -183,6 +186,8 @@
|
|||||||
|
|
||||||
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
|
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"yaml": "^2.7.0"
|
"yaml": "^2.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.9",
|
||||||
"@types/diff": "^5.2.3",
|
"@types/diff": "^5.2.3",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/tar-stream": "^3.1.4",
|
"@types/tar-stream": "^3.1.4",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { stat, writeFile, mkdir } from "node:fs/promises";
|
import { stat, readdir, writeFile, mkdir } from "node:fs/promises";
|
||||||
import { stringify as yamlStringify } from "yaml";
|
import { stringify as yamlStringify } from "yaml";
|
||||||
|
|
||||||
import { colors } from "@cliffy/ansi/colors";
|
import { colors } from "@cliffy/ansi/colors";
|
||||||
@@ -6,17 +6,19 @@ import { Command } from "@cliffy/command";
|
|||||||
import { Table } from "@cliffy/table";
|
import { Table } from "@cliffy/table";
|
||||||
import * as log from "../../core/log.ts";
|
import * as log from "../../core/log.ts";
|
||||||
import { sep as SEP } from "node:path";
|
import { sep as SEP } from "node:path";
|
||||||
|
import { Confirm } from "@cliffy/prompt/confirm";
|
||||||
import * as wmill from "../../../gen/services.gen.ts";
|
import * as wmill from "../../../gen/services.gen.ts";
|
||||||
|
|
||||||
import { requireLogin } from "../../core/auth.ts";
|
import { requireLogin } from "../../core/auth.ts";
|
||||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
import { resolveWorkspace } from "../../core/context.ts";
|
||||||
import { GlobalOptions, isSuperset, parseFromFile } from "../../types.ts";
|
import { GlobalOptions, isSuperset, parseFromFile } from "../../types.ts";
|
||||||
import { Folder } from "../../../gen/types.gen.ts";
|
import { Folder } from "../../../gen/types.gen.ts";
|
||||||
|
|
||||||
export interface FolderFile {
|
export interface FolderFile {
|
||||||
|
summary: string | undefined;
|
||||||
|
display_name: string | undefined;
|
||||||
owners: Array<string> | undefined;
|
owners: Array<string> | undefined;
|
||||||
extra_perms: { [record: string]: boolean } | undefined;
|
extra_perms: { [record: string]: boolean } | undefined;
|
||||||
display_name: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||||
@@ -45,7 +47,7 @@ async function list(opts: GlobalOptions & { json?: boolean }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newFolder(opts: GlobalOptions, name: string) {
|
async function newFolder(opts: GlobalOptions & { summary?: string }, name: string) {
|
||||||
const dirPath = `f${SEP}${name}`;
|
const dirPath = `f${SEP}${name}`;
|
||||||
const filePath = `${dirPath}${SEP}folder.meta.yaml`;
|
const filePath = `${dirPath}${SEP}folder.meta.yaml`;
|
||||||
try {
|
try {
|
||||||
@@ -54,7 +56,9 @@ async function newFolder(opts: GlobalOptions, name: string) {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message?.startsWith("File already exists")) throw e;
|
if (e.message?.startsWith("File already exists")) throw e;
|
||||||
}
|
}
|
||||||
const template: Omit<FolderFile, "display_name"> = {
|
const template: FolderFile = {
|
||||||
|
summary: opts.summary ?? "",
|
||||||
|
display_name: name,
|
||||||
owners: [],
|
owners: [],
|
||||||
extra_perms: {},
|
extra_perms: {},
|
||||||
};
|
};
|
||||||
@@ -143,30 +147,72 @@ export async function pushFolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
async function push(opts: GlobalOptions, name: string) {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!validatePath(remotePath)) {
|
const metaPath = `f${SEP}${name}${SEP}folder.meta.yaml`;
|
||||||
return;
|
try {
|
||||||
}
|
await stat(metaPath);
|
||||||
|
} catch {
|
||||||
const fstat = await stat(filePath);
|
throw new Error(`Could not find ${metaPath}. Does the folder exist locally?`);
|
||||||
if (!fstat.isFile()) {
|
|
||||||
throw new Error("file path must refer to a file.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(colors.bold.yellow("Pushing folder..."));
|
console.log(colors.bold.yellow("Pushing folder..."));
|
||||||
|
|
||||||
await pushFolder(
|
await pushFolder(
|
||||||
workspace.workspaceId,
|
workspace.workspaceId,
|
||||||
remotePath,
|
name,
|
||||||
undefined,
|
undefined,
|
||||||
parseFromFile(filePath)
|
parseFromFile(metaPath)
|
||||||
);
|
);
|
||||||
console.log(colors.bold.underline.green("Folder pushed"));
|
console.log(colors.bold.underline.green("Folder pushed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addMissing(opts: GlobalOptions & { yes?: boolean }) {
|
||||||
|
const fDir = `f`;
|
||||||
|
try {
|
||||||
|
await stat(fDir);
|
||||||
|
} catch {
|
||||||
|
log.info("No 'f/' directory found. Nothing to do.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entries = await readdir(fDir, { withFileTypes: true });
|
||||||
|
const missing: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const metaPath = `${fDir}${SEP}${entry.name}${SEP}folder.meta.yaml`;
|
||||||
|
try {
|
||||||
|
await stat(metaPath);
|
||||||
|
} catch {
|
||||||
|
missing.push(entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.length === 0) {
|
||||||
|
log.info("All folders already have a folder.meta.yaml. Nothing to do.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info(`Missing folder.meta.yaml for:`);
|
||||||
|
for (const name of missing) {
|
||||||
|
log.info(` - ${name}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!opts.yes &&
|
||||||
|
!(await Confirm.prompt({
|
||||||
|
message: `Create ${missing.length} folder.meta.yaml file(s)?`,
|
||||||
|
default: true,
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const name of missing) {
|
||||||
|
await newFolder(opts, name);
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
`\nCreated ${missing.length} folder.meta.yaml file(s). You can now run 'wmill sync push' to push them.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
.description("folder related commands")
|
.description("folder related commands")
|
||||||
.option("--json", "Output as JSON (for piping to jq)")
|
.option("--json", "Output as JSON (for piping to jq)")
|
||||||
@@ -180,12 +226,19 @@ const command = new Command()
|
|||||||
.action(get as any)
|
.action(get as any)
|
||||||
.command("new", "create a new folder locally")
|
.command("new", "create a new folder locally")
|
||||||
.arguments("<name:string>")
|
.arguments("<name:string>")
|
||||||
|
.option("--summary <summary:string>", "folder summary")
|
||||||
.action(newFolder as any)
|
.action(newFolder as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local folder spec. This overrides any remote versions."
|
"push a local folder to the remote by name. This overrides any remote versions."
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<name:string>")
|
||||||
.action(push as any);
|
.action(push as any)
|
||||||
|
.command(
|
||||||
|
"add-missing",
|
||||||
|
"create default folder.meta.yaml for all subdirectories of f/ that are missing one"
|
||||||
|
)
|
||||||
|
.option("-y, --yes", "skip confirmation prompt")
|
||||||
|
.action(addMissing as any);
|
||||||
|
|
||||||
export default command;
|
export default command;
|
||||||
|
|||||||
@@ -2480,6 +2480,45 @@ export async function push(
|
|||||||
log.info(
|
log.info(
|
||||||
`remote (${workspace.name}) <- local: ${changes.length} changes to apply`,
|
`remote (${workspace.name}) <- local: ${changes.length} changes to apply`,
|
||||||
);
|
);
|
||||||
|
// Check that every folder referenced in the changeset has a local folder.meta.yaml
|
||||||
|
const missingFolders: string[] = [];
|
||||||
|
if (changes.length > 0) {
|
||||||
|
const folderNames = new Set<string>();
|
||||||
|
for (const change of changes) {
|
||||||
|
const parts = change.path.split(SEP);
|
||||||
|
if (parts.length >= 3 && parts[0] === "f" && change.name !== "deleted") {
|
||||||
|
folderNames.add(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const folderName of folderNames) {
|
||||||
|
try {
|
||||||
|
await stat(path.join("f", folderName, "folder.meta.yaml"));
|
||||||
|
} catch {
|
||||||
|
missingFolders.push(folderName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingFolders.length > 0) {
|
||||||
|
const folderList = missingFolders.map((f) => ` - ${f}`).join("\n");
|
||||||
|
const user = await wmill.whoami({ workspace: workspace.workspaceId });
|
||||||
|
const userIsAdmin = user.is_admin;
|
||||||
|
const msg =
|
||||||
|
`${userIsAdmin ? "Warning: " : ""}Missing folder.meta.yaml for:\n${folderList}\n` +
|
||||||
|
`Run 'wmill folder add-missing' to create them locally, then push again.`;
|
||||||
|
if (!userIsAdmin) {
|
||||||
|
if (opts.jsonOutput) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "missing_folders", missing_folders: missingFolders, message: msg }, null, 2));
|
||||||
|
} else {
|
||||||
|
log.error(msg);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!opts.jsonOutput) {
|
||||||
|
log.warn(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle JSON output for dry-run
|
// Handle JSON output for dry-run
|
||||||
if (opts.dryRun && opts.jsonOutput) {
|
if (opts.dryRun && opts.jsonOutput) {
|
||||||
const result = {
|
const result = {
|
||||||
@@ -2511,6 +2550,7 @@ export async function push(
|
|||||||
if (!opts.jsonOutput) {
|
if (!opts.jsonOutput) {
|
||||||
prettyChanges(changes, specificItems, opts.branch);
|
prettyChanges(changes, specificItems, opts.branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.dryRun) {
|
if (opts.dryRun) {
|
||||||
log.info(colors.gray(`Dry run complete.`));
|
log.info(colors.gray(`Dry run complete.`));
|
||||||
return;
|
return;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -232,8 +232,9 @@ export class CargoBackend {
|
|||||||
*/
|
*/
|
||||||
private getBasePostgresUrl(): string {
|
private getBasePostgresUrl(): string {
|
||||||
const url = new URL(this.config.postgresUrl);
|
const url = new URL(this.config.postgresUrl);
|
||||||
// Remove any existing database path
|
// Remove any existing database path and query params (e.g. ?sslmode=disable)
|
||||||
url.pathname = "";
|
url.pathname = "";
|
||||||
|
url.search = "";
|
||||||
return url.toString().replace(/\/$/, ""); // Remove trailing slash
|
return url.toString().replace(/\/$/, ""); // Remove trailing slash
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,13 +630,13 @@ export class CargoBackend {
|
|||||||
/**
|
/**
|
||||||
* Create CLI command with proper authentication
|
* Create CLI command with proper authentication
|
||||||
*/
|
*/
|
||||||
createCLICommand(args: string[], workingDir: string, workspaceName?: string): { command: string, args: string[], cwd: string, env: Record<string, string> } {
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): { command: string, args: string[], cwd: string, env: Record<string, string> } {
|
||||||
const workspace = workspaceName || this.config.workspace;
|
const workspace = opts?.workspace || this.config.workspace;
|
||||||
const cliDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
const cliDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const fullArgs = [
|
const fullArgs = [
|
||||||
"--base-url", this.baseUrl,
|
"--base-url", this.baseUrl,
|
||||||
"--workspace", workspace,
|
"--workspace", workspace,
|
||||||
"--token", this.token,
|
"--token", opts?.token || this.token,
|
||||||
"--config-dir", this.config.testConfigDir,
|
"--config-dir", this.config.testConfigDir,
|
||||||
...args,
|
...args,
|
||||||
];
|
];
|
||||||
@@ -660,12 +661,12 @@ export class CargoBackend {
|
|||||||
/**
|
/**
|
||||||
* Run CLI command and return result
|
* Run CLI command and return result
|
||||||
*/
|
*/
|
||||||
async runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
code: number;
|
code: number;
|
||||||
}> {
|
}> {
|
||||||
const cmd = this.createCLICommand(args, workingDir, workspaceName);
|
const cmd = this.createCLICommand(args, workingDir, opts);
|
||||||
const proc = Bun.spawn([cmd.command, ...cmd.args], {
|
const proc = Bun.spawn([cmd.command, ...cmd.args], {
|
||||||
cwd: cmd.cwd,
|
cwd: cmd.cwd,
|
||||||
env: cmd.env,
|
env: cmd.env,
|
||||||
|
|||||||
@@ -1020,12 +1020,12 @@ export async function main(
|
|||||||
/**
|
/**
|
||||||
* Create CLI command with proper authentication
|
* Create CLI command with proper authentication
|
||||||
*/
|
*/
|
||||||
createCLICommand(args: string[], workingDir: string, workspaceName?: string): { cmd: string[], cwd: string } {
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): { cmd: string[], cwd: string } {
|
||||||
const workspace = workspaceName || this.config.workspace;
|
const workspace = opts?.workspace || this.config.workspace;
|
||||||
const fullArgs = [
|
const fullArgs = [
|
||||||
'--base-url', this.config.baseUrl,
|
'--base-url', this.config.baseUrl,
|
||||||
'--workspace', workspace,
|
'--workspace', workspace,
|
||||||
'--token', this.config.token,
|
'--token', opts?.token || this.config.token,
|
||||||
'--config-dir', this.config.testConfigDir,
|
'--config-dir', this.config.testConfigDir,
|
||||||
...args
|
...args
|
||||||
];
|
];
|
||||||
@@ -1049,12 +1049,12 @@ export async function main(
|
|||||||
/**
|
/**
|
||||||
* Run CLI command and return result
|
* Run CLI command and return result
|
||||||
*/
|
*/
|
||||||
async runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
code: number;
|
code: number;
|
||||||
}> {
|
}> {
|
||||||
const { cmd, cwd } = this.createCLICommand(args, workingDir, workspaceName);
|
const { cmd, cwd } = this.createCLICommand(args, workingDir, opts);
|
||||||
const proc = Bun.spawn(cmd, {
|
const proc = Bun.spawn(cmd, {
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
|
|||||||
400
cli/test/folder_missing_meta.test.ts
Normal file
400
cli/test/folder_missing_meta.test.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
/**
|
||||||
|
* Tests for missing folder.meta.yaml detection during sync push,
|
||||||
|
* the `folder add-missing` command, and the simplified `folder push` command.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test, describe } from "bun:test";
|
||||||
|
import { writeFile, mkdir, readFile, rm, mkdtemp } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { getTestBackend, createNonAdminUser } from "./test_backend.ts";
|
||||||
|
|
||||||
|
type IsolatedWorkspaceTestContext = {
|
||||||
|
backend: any;
|
||||||
|
tempDir: string;
|
||||||
|
workspaceId: string;
|
||||||
|
runCLICommand: (
|
||||||
|
args: string[],
|
||||||
|
opts?: { token?: string }
|
||||||
|
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
||||||
|
apiRequest: (path: string, options?: RequestInit) => Promise<Response>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createWorkspace(backend: any, workspaceId: string): Promise<void> {
|
||||||
|
const response = await backend.apiRequest!("/api/workspaces/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: workspaceId,
|
||||||
|
// Workspace name has a 50-char DB limit; keep it identical to the short ID.
|
||||||
|
name: workspaceId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
if (!error.includes("already exists") && !error.includes("duplicate")) {
|
||||||
|
throw new Error(`Failed to create workspace ${workspaceId}: ${error}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withIsolatedWorkspace(
|
||||||
|
testFn: (ctx: IsolatedWorkspaceTestContext) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const backend = await getTestBackend();
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_"));
|
||||||
|
const workspaceId = `fmeta_${Date.now().toString(36)}_${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.slice(2, 6)}`;
|
||||||
|
let workspaceCreated = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createWorkspace(backend, workspaceId);
|
||||||
|
workspaceCreated = true;
|
||||||
|
|
||||||
|
await testFn({
|
||||||
|
backend,
|
||||||
|
tempDir,
|
||||||
|
workspaceId,
|
||||||
|
runCLICommand: (args: string[], opts?: { token?: string }) =>
|
||||||
|
backend.runCLICommand(args, tempDir, {
|
||||||
|
workspace: workspaceId,
|
||||||
|
token: opts?.token,
|
||||||
|
}),
|
||||||
|
apiRequest: (path: string, options?: RequestInit) =>
|
||||||
|
backend.apiRequest!(`/api/w/${workspaceId}${path}`, options),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (workspaceCreated) {
|
||||||
|
try {
|
||||||
|
const archiveResponse = await backend.apiRequest!(
|
||||||
|
`/api/w/${workspaceId}/workspaces/archive`,
|
||||||
|
{ method: "POST" }
|
||||||
|
);
|
||||||
|
await archiveResponse.text();
|
||||||
|
} catch {
|
||||||
|
// Best-effort cleanup to avoid exceeding non-enterprise workspace limits.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wmillYaml(): string {
|
||||||
|
return `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// folder new — creates folder.meta.yaml with summary and display_name
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe("folder new", () => {
|
||||||
|
test("creates folder.meta.yaml with summary and display_name", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
const folderName = `newfolder${Date.now()}`;
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "new", folderName, "--summary", "My summary"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
|
||||||
|
const metaPath = join(tempDir, "f", folderName, "folder.meta.yaml");
|
||||||
|
const content = await readFile(metaPath, "utf-8");
|
||||||
|
expect(content).toContain("summary: My summary");
|
||||||
|
expect(content).toContain(`display_name: ${folderName}`);
|
||||||
|
expect(content).toContain("owners:");
|
||||||
|
expect(content).toContain("extra_perms:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates folder.meta.yaml with empty summary when none provided", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
const folderName = `nosummary${Date.now()}`;
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "new", folderName],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
|
||||||
|
const content = await readFile(
|
||||||
|
join(tempDir, "f", folderName, "folder.meta.yaml"),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
expect(content).toContain('summary: ""');
|
||||||
|
expect(content).toContain(`display_name: ${folderName}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails if folder.meta.yaml already exists", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ runCLICommand }) => {
|
||||||
|
const folderName = `dupfolder${Date.now()}`;
|
||||||
|
// Create first
|
||||||
|
await runCLICommand(["folder", "new", folderName]);
|
||||||
|
|
||||||
|
// Try again — should fail
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "new", folderName],
|
||||||
|
);
|
||||||
|
expect(result.code).not.toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// folder add-missing — scaffolds missing folder.meta.yaml files
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe("folder add-missing", () => {
|
||||||
|
test("creates folder.meta.yaml for directories missing one", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
// Create two folders: one with meta, one without
|
||||||
|
const withMeta = `withmeta${Date.now()}`;
|
||||||
|
const withoutMeta = `withoutmeta${Date.now()}`;
|
||||||
|
|
||||||
|
await mkdir(join(tempDir, "f", withMeta), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", withMeta, "folder.meta.yaml"),
|
||||||
|
'summary: ""\ndisplay_name: existing\nowners: []\nextra_perms: {}\n',
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
await mkdir(join(tempDir, "f", withoutMeta), { recursive: true });
|
||||||
|
// No folder.meta.yaml for withoutMeta
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "add-missing", "-y"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
|
||||||
|
// withoutMeta should now have a folder.meta.yaml
|
||||||
|
const createdMeta = await readFile(
|
||||||
|
join(tempDir, "f", withoutMeta, "folder.meta.yaml"),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
expect(createdMeta).toContain(`display_name: ${withoutMeta}`);
|
||||||
|
expect(createdMeta).toContain("owners:");
|
||||||
|
|
||||||
|
// withMeta should be unchanged
|
||||||
|
const existingMeta = await readFile(
|
||||||
|
join(tempDir, "f", withMeta, "folder.meta.yaml"),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
expect(existingMeta).toContain("display_name: existing");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports nothing to do when all folders have meta", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
const folderName = `alldone${Date.now()}`;
|
||||||
|
await mkdir(join(tempDir, "f", folderName), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "folder.meta.yaml"),
|
||||||
|
'summary: ""\ndisplay_name: done\nowners: []\nextra_perms: {}\n',
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "add-missing", "-y"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
expect(result.stdout + result.stderr).toContain("Nothing to do");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports nothing to do when no f/ directory exists", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ runCLICommand }) => {
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "add-missing", "-y"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
expect(result.stdout + result.stderr).toContain("Nothing to do");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// folder push — simplified single-arg signature
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe("folder push", () => {
|
||||||
|
test("pushes a folder by name", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
|
||||||
|
const folderName = `pushbyname${Date.now()}`;
|
||||||
|
|
||||||
|
// Create local folder meta
|
||||||
|
await mkdir(join(tempDir, "f", folderName), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "folder.meta.yaml"),
|
||||||
|
`summary: "pushed"\ndisplay_name: "${folderName}"\nowners:\n - "admin@windmill.dev"\nextra_perms: {}\n`,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "push", folderName],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
expect(result.stdout + result.stderr).toContain("Folder pushed");
|
||||||
|
|
||||||
|
// Verify via API
|
||||||
|
const apiResp = await apiRequest(`/folders/get/${folderName}`);
|
||||||
|
expect(apiResp.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when folder does not exist locally", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ runCLICommand }) => {
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["folder", "push", "nonexistent"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).not.toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// sync push — missing folder.meta.yaml detection
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe("sync push missing folder detection", () => {
|
||||||
|
test("admin user gets warning but push succeeds", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
const uniqueId = Date.now();
|
||||||
|
const folderName = `nometaadmin${uniqueId}`;
|
||||||
|
|
||||||
|
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
|
||||||
|
|
||||||
|
// Create a script inside a folder WITHOUT folder.meta.yaml
|
||||||
|
await mkdir(join(tempDir, "f", folderName), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "test_script.ts"),
|
||||||
|
'export async function main() { return "hello"; }',
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Admin should get a warning but push succeeds (exit 0)
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
expect(output).toContain("Missing folder.meta.yaml");
|
||||||
|
expect(output).toContain(folderName);
|
||||||
|
expect(output).toContain("wmill folder add-missing");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no warning when folder.meta.yaml exists", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
|
||||||
|
const uniqueId = Date.now();
|
||||||
|
const folderName = `withmeta${uniqueId}`;
|
||||||
|
|
||||||
|
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
|
||||||
|
|
||||||
|
// Create folder WITH folder.meta.yaml
|
||||||
|
await mkdir(join(tempDir, "f", folderName), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "folder.meta.yaml"),
|
||||||
|
`summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "test_script.ts"),
|
||||||
|
'export async function main() { return "hello"; }',
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(0);
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
expect(output).not.toContain("Missing folder.meta.yaml");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIf(!process.env["EE_LICENSE_KEY"])("non-admin user gets error and exit code 1", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ backend, tempDir, workspaceId, runCLICommand, apiRequest }) => {
|
||||||
|
const nonAdminToken = await createNonAdminUser(backend, workspaceId);
|
||||||
|
|
||||||
|
const uniqueId = Date.now();
|
||||||
|
const folderName = `nometanonadmin${uniqueId}`;
|
||||||
|
|
||||||
|
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
|
||||||
|
|
||||||
|
// Create a script inside a folder WITHOUT folder.meta.yaml
|
||||||
|
// First create the folder on remote so the non-admin has somewhere to push
|
||||||
|
await apiRequest(
|
||||||
|
"/folders/create",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: folderName,
|
||||||
|
extra_perms: { "g/all": true },
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await mkdir(join(tempDir, "f", folderName), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(tempDir, "f", folderName, "test_script.ts"),
|
||||||
|
'export async function main() { return "hello"; }',
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
|
||||||
|
{ token: nonAdminToken }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Non-admin should get exit code 1
|
||||||
|
expect(result.code).toEqual(1);
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
expect(output).toContain("Missing folder.meta.yaml");
|
||||||
|
expect(output).toContain("wmill folder add-missing");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no warning for deleted changes without folder.meta.yaml", async () => {
|
||||||
|
await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
|
||||||
|
const uniqueId = Date.now();
|
||||||
|
const folderName = `delfolder${uniqueId}`;
|
||||||
|
|
||||||
|
// Create folder and script on remote via API
|
||||||
|
await apiRequest(
|
||||||
|
"/folders/create",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: folderName }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
|
||||||
|
|
||||||
|
// Pull to get remote state, then delete the folder locally
|
||||||
|
await runCLICommand(["sync", "pull", "--yes"]);
|
||||||
|
|
||||||
|
// Remove the folder locally to trigger a "deleted" change
|
||||||
|
await rm(join(tempDir, "f", folderName), { recursive: true, force: true });
|
||||||
|
|
||||||
|
const result = await runCLICommand(
|
||||||
|
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not warn about missing meta for deleted items
|
||||||
|
const output = result.stdout + result.stderr;
|
||||||
|
expect(output).not.toContain("Missing folder.meta.yaml");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,8 +40,8 @@ export interface TestBackend {
|
|||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
reset(): Promise<void>;
|
reset(): Promise<void>;
|
||||||
|
|
||||||
createCLICommand(args: string[], workingDir: string, workspaceName?: string): any;
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any;
|
||||||
runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{
|
runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
code: number;
|
code: number;
|
||||||
@@ -97,12 +97,12 @@ class CargoBackendAdapter implements TestBackend {
|
|||||||
await this.backend.reset();
|
await this.backend.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
createCLICommand(args: string[], workingDir: string, workspaceName?: string): any {
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any {
|
||||||
return this.backend.createCLICommand(args, workingDir, workspaceName);
|
return this.backend.createCLICommand(args, workingDir, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCLICommand(args: string[], workingDir: string, workspaceName?: string) {
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) {
|
||||||
return this.backend.runCLICommand(args, workingDir, workspaceName);
|
return this.backend.runCLICommand(args, workingDir, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async apiRequest(path: string, options?: RequestInit): Promise<Response> {
|
async apiRequest(path: string, options?: RequestInit): Promise<Response> {
|
||||||
@@ -369,12 +369,12 @@ class ContainerizedBackendAdapter implements TestBackend {
|
|||||||
await this.backend.reset();
|
await this.backend.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
createCLICommand(args: string[], workingDir: string, workspaceName?: string): any {
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any {
|
||||||
return this.backend.createCLICommand(args, workingDir, workspaceName);
|
return this.backend.createCLICommand(args, workingDir, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCLICommand(args: string[], workingDir: string, workspaceName?: string) {
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) {
|
||||||
return this.backend.runCLICommand(args, workingDir, workspaceName);
|
return this.backend.runCLICommand(args, workingDir, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async seedTestData(): Promise<void> {
|
async seedTestData(): Promise<void> {
|
||||||
@@ -507,6 +507,66 @@ function registerCleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a non-admin user, add them to the workspace, and return their token.
|
||||||
|
*/
|
||||||
|
export async function createNonAdminUser(
|
||||||
|
backend: TestBackend,
|
||||||
|
workspaceId: string = backend.workspace
|
||||||
|
): Promise<string> {
|
||||||
|
if (!backend.apiRequest) {
|
||||||
|
throw new Error("Backend does not support apiRequest");
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = `nonadmin_${Date.now()}@test.dev`;
|
||||||
|
const password = "testpass123";
|
||||||
|
|
||||||
|
// Create user globally (as admin)
|
||||||
|
const createResp = await backend.apiRequest("/api/users/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
super_admin: false,
|
||||||
|
name: "Non-Admin Test User",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!createResp.ok) {
|
||||||
|
throw new Error(`Failed to create user: ${await createResp.text()}`);
|
||||||
|
}
|
||||||
|
await createResp.text();
|
||||||
|
|
||||||
|
// Add user to workspace as non-admin
|
||||||
|
const addResp = await backend.apiRequest(
|
||||||
|
`/api/w/${workspaceId}/workspaces/add_user`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
is_admin: false,
|
||||||
|
operator: false,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!addResp.ok) {
|
||||||
|
throw new Error(`Failed to add user to workspace: ${await addResp.text()}`);
|
||||||
|
}
|
||||||
|
await addResp.text();
|
||||||
|
|
||||||
|
// Login as the non-admin user to get a token
|
||||||
|
const loginResp = await fetch(`${backend.baseUrl}/api/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
if (!loginResp.ok) {
|
||||||
|
throw new Error(`Failed to login as non-admin: ${await loginResp.text()}`);
|
||||||
|
}
|
||||||
|
return await loginResp.text();
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
export type { CargoBackendConfig } from "./cargo_backend.ts";
|
export type { CargoBackendConfig } from "./cargo_backend.ts";
|
||||||
export type { ContainerConfig } from "./containerized_backend.ts";
|
export type { ContainerConfig } from "./containerized_backend.ts";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.
|
The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.
|
||||||
|
|
||||||
Current version: 1.624.0
|
Current version: 1.642.0
|
||||||
|
|
||||||
## Global Options
|
## Global Options
|
||||||
|
|
||||||
@@ -19,8 +19,15 @@ Current version: 1.624.0
|
|||||||
|
|
||||||
app related commands
|
app related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `app list` - list all apps
|
||||||
|
- `--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
|
||||||
- `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement
|
- `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)
|
- `--port <port:number>` - Port to run the dev server on (will find next available port if occupied)
|
||||||
@@ -58,10 +65,16 @@ Launch a dev server that will spawn a webserver with HMR
|
|||||||
flow related commands
|
flow related commands
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--show-archived` - Enable archived scripts in output
|
- `--show-archived` - Enable archived flows in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `flow list` - list all flows
|
||||||
|
- `--show-archived` - Enable archived flows in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `flow get <path:string>` - get a flow's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
||||||
- `flow run <path:string>` - run a flow by path.
|
- `flow run <path:string>` - run a flow by path.
|
||||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
@@ -73,17 +86,31 @@ flow related commands
|
|||||||
- `--yes` - Skip confirmation prompt
|
- `--yes` - Skip confirmation prompt
|
||||||
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||||
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
|
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
|
||||||
- `flow bootstrap <flow_path:string>` - create a new empty flow
|
- `flow new <flow_path:string>` - create a new empty flow
|
||||||
- `--summary <summary:string>` - script summary
|
- `--summary <summary:string>` - flow summary
|
||||||
- `--description <description:string>` - script description
|
- `--description <description:string>` - flow description
|
||||||
|
- `flow bootstrap <flow_path:string>` - create a new empty flow (alias for new
|
||||||
|
- `--summary <summary:string>` - flow summary
|
||||||
|
- `--description <description:string>` - flow description
|
||||||
|
|
||||||
### folder
|
### folder
|
||||||
|
|
||||||
folder related commands
|
folder related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
- `folder push <file_path:string> <remote_path:string>` - push a local folder spec. This overrides any remote versions.
|
- `folder list` - list all folders
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `folder get <name:string>` - get a folder's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `folder new <name:string>` - create a new folder locally
|
||||||
|
- `--summary <summary:string>` - folder summary
|
||||||
|
- `folder push <name:string>` - push a local folder to the remote by name. This overrides any remote versions.
|
||||||
|
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
|
||||||
|
- `-y, --yes` - skip confirmation prompt
|
||||||
|
|
||||||
### gitsync-settings
|
### gitsync-settings
|
||||||
|
|
||||||
@@ -162,6 +189,9 @@ sync local with a remote instance or the opposite (push or pull)
|
|||||||
- `--prefix <prefix:string>` - Prefix of the local workspaces folders to push
|
- `--prefix <prefix:string>` - Prefix of the local workspaces folders to push
|
||||||
- `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
|
- `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
|
||||||
- `instance whoami` - Display information about the currently logged-in user
|
- `instance whoami` - Display information about the currently logged-in user
|
||||||
|
- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML
|
||||||
|
- `-o, --output-file <file:string>` - Write YAML to a file instead of stdout
|
||||||
|
- `--instance <instance:string>` - Name of the instance, override the active instance
|
||||||
|
|
||||||
### jobs
|
### jobs
|
||||||
|
|
||||||
@@ -179,6 +209,17 @@ Pull completed and queued jobs from workspace
|
|||||||
- `jobs pull`
|
- `jobs pull`
|
||||||
- `jobs push`
|
- `jobs push`
|
||||||
|
|
||||||
|
### lint
|
||||||
|
|
||||||
|
Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||||
|
|
||||||
|
**Arguments:** `[directory:string]`
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output results in JSON format
|
||||||
|
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
|
||||||
|
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||||
|
|
||||||
### queues
|
### queues
|
||||||
|
|
||||||
List all queues with their metrics
|
List all queues with their metrics
|
||||||
@@ -193,18 +234,33 @@ List all queues with their metrics
|
|||||||
|
|
||||||
resource related commands
|
resource related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `resource list` - list all resources
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource get <path:string>` - get a resource's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource new <path:string>` - create a new resource locally
|
||||||
- `resource push <file_path:string> <remote_path:string>` - push a local resource spec. This overrides any remote versions.
|
- `resource push <file_path:string> <remote_path:string>` - push a local resource spec. This overrides any remote versions.
|
||||||
|
|
||||||
### resource-type
|
### resource-type
|
||||||
|
|
||||||
resource type related commands
|
resource type related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
- `resource-type list` - list all resource types
|
- `resource-type list` - list all resource types
|
||||||
- `--schema` - Show schema in the output
|
- `--schema` - Show schema in the output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource-type get <path:string>` - get a resource type's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource-type new <name:string>` - create a new resource type locally
|
||||||
- `resource-type push <file_path:string> <name:string>` - push a local resource spec. This overrides any remote versions.
|
- `resource-type push <file_path:string> <name:string>` - push a local resource spec. This overrides any remote versions.
|
||||||
- `resource-type generate-namespace` - Create a TypeScript definition file with the RT namespace generated from the resource types
|
- `resource-type generate-namespace` - Create a TypeScript definition file with the RT namespace generated from the resource types
|
||||||
|
|
||||||
@@ -212,8 +268,16 @@ resource type related commands
|
|||||||
|
|
||||||
schedule related commands
|
schedule related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `schedule list` - list all schedules
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `schedule get <path:string>` - get a schedule's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `schedule new <path:string>` - create a new schedule locally
|
||||||
- `schedule push <file_path:string> <remote_path:string>` - push a local schedule spec. This overrides any remote versions.
|
- `schedule push <file_path:string> <remote_path:string>` - push a local schedule spec. This overrides any remote versions.
|
||||||
|
|
||||||
### script
|
### script
|
||||||
@@ -222,18 +286,27 @@ script related commands
|
|||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--show-archived` - Enable archived scripts in output
|
- `--show-archived` - Enable archived scripts in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `script list` - list all scripts
|
||||||
|
- `--show-archived` - Enable archived scripts in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||||
- `script show <path:file>` - show a scripts content
|
- `script get <path:file>` - get a script's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `script show <path:file>` - show a script's content (alias for get
|
||||||
- `script run <path:file>` - run a script by path
|
- `script run <path:file>` - run a script by path
|
||||||
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||||
- `script preview <path:file>` - preview a local script without deploying it. Supports both regular and codebase scripts.
|
- `script preview <path:file>` - preview a local script without deploying it. Supports both regular and codebase scripts.
|
||||||
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
- `-s --silent` - Do not output anything other than the final output. Useful for scripting.
|
- `-s --silent` - Do not output anything other than the final output. Useful for scripting.
|
||||||
- `script bootstrap <path:file> <language:string>` - create a new script
|
- `script new <path:file> <language:string>` - create a new script
|
||||||
|
- `--summary <summary:string>` - script summary
|
||||||
|
- `--description <description:string>` - script description
|
||||||
|
- `script bootstrap <path:file> <language:string>` - create a new script (alias for new
|
||||||
- `--summary <summary:string>` - script summary
|
- `--summary <summary:string>` - script summary
|
||||||
- `--description <description:string>` - script description
|
- `--description <description:string>` - script description
|
||||||
- `script generate-metadata [script:file]` - re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`
|
- `script generate-metadata [script:file]` - re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`
|
||||||
@@ -309,13 +382,25 @@ sync local with a remote workspaces or the opposite (push or pull)
|
|||||||
- `--parallel <number>` - Number of changes to process in parallel
|
- `--parallel <number>` - Number of changes to process in parallel
|
||||||
- `--repository <repo:string>` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
|
- `--repository <repo:string>` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
|
||||||
- `--branch <branch:string>` - Override the current git branch (works even outside a git repository)
|
- `--branch <branch:string>` - Override the current git branch (works even outside a git repository)
|
||||||
|
- `--lint` - Run lint validation before pushing
|
||||||
|
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||||
|
|
||||||
### trigger
|
### trigger
|
||||||
|
|
||||||
trigger related commands
|
trigger related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `trigger list` - list all triggers
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `trigger get <path:string>` - get a trigger's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `--kind <kind:string>` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup
|
||||||
|
- `trigger new <path:string>` - create a new trigger locally
|
||||||
|
- `--kind <kind:string>` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)
|
||||||
- `trigger push <file_path:string> <remote_path:string>` - push a local trigger spec. This overrides any remote versions.
|
- `trigger push <file_path:string> <remote_path:string>` - push a local trigger spec. This overrides any remote versions.
|
||||||
|
|
||||||
### user
|
### user
|
||||||
@@ -337,8 +422,16 @@ user related commands
|
|||||||
|
|
||||||
variable related commands
|
variable related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `variable list` - list all variables
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `variable get <path:string>` - get a variable's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `variable new <path:string>` - create a new variable locally
|
||||||
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
|
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
|
||||||
- `--plain-secrets` - Push secrets as plain text
|
- `--plain-secrets` - Push secrets as plain text
|
||||||
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
|
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
|
||||||
@@ -387,7 +480,8 @@ workspace related commands
|
|||||||
- `--create-username <username:string>` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.
|
- `--create-username <username:string>` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.
|
||||||
- `workspace remove <workspace_name:string>` - Remove a workspace
|
- `workspace remove <workspace_name:string>` - Remove a workspace
|
||||||
- `workspace whoami` - Show the currently active user
|
- `workspace whoami` - Show the currently active user
|
||||||
- `workspace list` - List workspaces on the remote server that you have access to
|
- `workspace list` - List local workspace profiles
|
||||||
|
- `workspace list-remote` - List workspaces on the remote server that you have access to
|
||||||
- `workspace bind` - Bind the current Git branch to the active workspace
|
- `workspace bind` - Bind the current Git branch to the active workspace
|
||||||
- `--branch <branch:string>` - Specify branch (defaults to current)
|
- `--branch <branch:string>` - Specify branch (defaults to current)
|
||||||
- `workspace unbind` - Remove workspace binding from the current Git branch
|
- `workspace unbind` - Remove workspace binding from the current Git branch
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: MUST use when using the CLI.
|
|||||||
|
|
||||||
The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.
|
The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.
|
||||||
|
|
||||||
Current version: 1.624.0
|
Current version: 1.642.0
|
||||||
|
|
||||||
## Global Options
|
## Global Options
|
||||||
|
|
||||||
@@ -24,8 +24,15 @@ Current version: 1.624.0
|
|||||||
|
|
||||||
app related commands
|
app related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `app list` - list all apps
|
||||||
|
- `--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
|
||||||
- `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement
|
- `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)
|
- `--port <port:number>` - Port to run the dev server on (will find next available port if occupied)
|
||||||
@@ -63,10 +70,16 @@ Launch a dev server that will spawn a webserver with HMR
|
|||||||
flow related commands
|
flow related commands
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--show-archived` - Enable archived scripts in output
|
- `--show-archived` - Enable archived flows in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `flow list` - list all flows
|
||||||
|
- `--show-archived` - Enable archived flows in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `flow get <path:string>` - get a flow's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
||||||
- `flow run <path:string>` - run a flow by path.
|
- `flow run <path:string>` - run a flow by path.
|
||||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
@@ -78,17 +91,31 @@ flow related commands
|
|||||||
- `--yes` - Skip confirmation prompt
|
- `--yes` - Skip confirmation prompt
|
||||||
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||||
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
|
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
|
||||||
- `flow bootstrap <flow_path:string>` - create a new empty flow
|
- `flow new <flow_path:string>` - create a new empty flow
|
||||||
- `--summary <summary:string>` - script summary
|
- `--summary <summary:string>` - flow summary
|
||||||
- `--description <description:string>` - script description
|
- `--description <description:string>` - flow description
|
||||||
|
- `flow bootstrap <flow_path:string>` - create a new empty flow (alias for new
|
||||||
|
- `--summary <summary:string>` - flow summary
|
||||||
|
- `--description <description:string>` - flow description
|
||||||
|
|
||||||
### folder
|
### folder
|
||||||
|
|
||||||
folder related commands
|
folder related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
- `folder push <file_path:string> <remote_path:string>` - push a local folder spec. This overrides any remote versions.
|
- `folder list` - list all folders
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `folder get <name:string>` - get a folder's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `folder new <name:string>` - create a new folder locally
|
||||||
|
- `--summary <summary:string>` - folder summary
|
||||||
|
- `folder push <name:string>` - push a local folder to the remote by name. This overrides any remote versions.
|
||||||
|
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
|
||||||
|
- `-y, --yes` - skip confirmation prompt
|
||||||
|
|
||||||
### gitsync-settings
|
### gitsync-settings
|
||||||
|
|
||||||
@@ -167,6 +194,9 @@ sync local with a remote instance or the opposite (push or pull)
|
|||||||
- `--prefix <prefix:string>` - Prefix of the local workspaces folders to push
|
- `--prefix <prefix:string>` - Prefix of the local workspaces folders to push
|
||||||
- `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
|
- `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
|
||||||
- `instance whoami` - Display information about the currently logged-in user
|
- `instance whoami` - Display information about the currently logged-in user
|
||||||
|
- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML
|
||||||
|
- `-o, --output-file <file:string>` - Write YAML to a file instead of stdout
|
||||||
|
- `--instance <instance:string>` - Name of the instance, override the active instance
|
||||||
|
|
||||||
### jobs
|
### jobs
|
||||||
|
|
||||||
@@ -184,6 +214,17 @@ Pull completed and queued jobs from workspace
|
|||||||
- `jobs pull`
|
- `jobs pull`
|
||||||
- `jobs push`
|
- `jobs push`
|
||||||
|
|
||||||
|
### lint
|
||||||
|
|
||||||
|
Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||||
|
|
||||||
|
**Arguments:** `[directory:string]`
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output results in JSON format
|
||||||
|
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
|
||||||
|
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||||
|
|
||||||
### queues
|
### queues
|
||||||
|
|
||||||
List all queues with their metrics
|
List all queues with their metrics
|
||||||
@@ -198,18 +239,33 @@ List all queues with their metrics
|
|||||||
|
|
||||||
resource related commands
|
resource related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `resource list` - list all resources
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource get <path:string>` - get a resource's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource new <path:string>` - create a new resource locally
|
||||||
- `resource push <file_path:string> <remote_path:string>` - push a local resource spec. This overrides any remote versions.
|
- `resource push <file_path:string> <remote_path:string>` - push a local resource spec. This overrides any remote versions.
|
||||||
|
|
||||||
### resource-type
|
### resource-type
|
||||||
|
|
||||||
resource type related commands
|
resource type related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
- `resource-type list` - list all resource types
|
- `resource-type list` - list all resource types
|
||||||
- `--schema` - Show schema in the output
|
- `--schema` - Show schema in the output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource-type get <path:string>` - get a resource type's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `resource-type new <name:string>` - create a new resource type locally
|
||||||
- `resource-type push <file_path:string> <name:string>` - push a local resource spec. This overrides any remote versions.
|
- `resource-type push <file_path:string> <name:string>` - push a local resource spec. This overrides any remote versions.
|
||||||
- `resource-type generate-namespace` - Create a TypeScript definition file with the RT namespace generated from the resource types
|
- `resource-type generate-namespace` - Create a TypeScript definition file with the RT namespace generated from the resource types
|
||||||
|
|
||||||
@@ -217,8 +273,16 @@ resource type related commands
|
|||||||
|
|
||||||
schedule related commands
|
schedule related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `schedule list` - list all schedules
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `schedule get <path:string>` - get a schedule's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `schedule new <path:string>` - create a new schedule locally
|
||||||
- `schedule push <file_path:string> <remote_path:string>` - push a local schedule spec. This overrides any remote versions.
|
- `schedule push <file_path:string> <remote_path:string>` - push a local schedule spec. This overrides any remote versions.
|
||||||
|
|
||||||
### script
|
### script
|
||||||
@@ -227,18 +291,27 @@ script related commands
|
|||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--show-archived` - Enable archived scripts in output
|
- `--show-archived` - Enable archived scripts in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `script list` - list all scripts
|
||||||
|
- `--show-archived` - Enable archived scripts in output
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||||
- `script show <path:file>` - show a scripts content
|
- `script get <path:file>` - get a script's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `script show <path:file>` - show a script's content (alias for get
|
||||||
- `script run <path:file>` - run a script by path
|
- `script run <path:file>` - run a script by path
|
||||||
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||||
- `script preview <path:file>` - preview a local script without deploying it. Supports both regular and codebase scripts.
|
- `script preview <path:file>` - preview a local script without deploying it. Supports both regular and codebase scripts.
|
||||||
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
- `-d --data <data:file>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||||
- `-s --silent` - Do not output anything other than the final output. Useful for scripting.
|
- `-s --silent` - Do not output anything other than the final output. Useful for scripting.
|
||||||
- `script bootstrap <path:file> <language:string>` - create a new script
|
- `script new <path:file> <language:string>` - create a new script
|
||||||
|
- `--summary <summary:string>` - script summary
|
||||||
|
- `--description <description:string>` - script description
|
||||||
|
- `script bootstrap <path:file> <language:string>` - create a new script (alias for new
|
||||||
- `--summary <summary:string>` - script summary
|
- `--summary <summary:string>` - script summary
|
||||||
- `--description <description:string>` - script description
|
- `--description <description:string>` - script description
|
||||||
- `script generate-metadata [script:file]` - re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`
|
- `script generate-metadata [script:file]` - re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`
|
||||||
@@ -314,13 +387,25 @@ sync local with a remote workspaces or the opposite (push or pull)
|
|||||||
- `--parallel <number>` - Number of changes to process in parallel
|
- `--parallel <number>` - Number of changes to process in parallel
|
||||||
- `--repository <repo:string>` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
|
- `--repository <repo:string>` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
|
||||||
- `--branch <branch:string>` - Override the current git branch (works even outside a git repository)
|
- `--branch <branch:string>` - Override the current git branch (works even outside a git repository)
|
||||||
|
- `--lint` - Run lint validation before pushing
|
||||||
|
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||||
|
|
||||||
### trigger
|
### trigger
|
||||||
|
|
||||||
trigger related commands
|
trigger related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `trigger list` - list all triggers
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `trigger get <path:string>` - get a trigger's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `--kind <kind:string>` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup
|
||||||
|
- `trigger new <path:string>` - create a new trigger locally
|
||||||
|
- `--kind <kind:string>` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)
|
||||||
- `trigger push <file_path:string> <remote_path:string>` - push a local trigger spec. This overrides any remote versions.
|
- `trigger push <file_path:string> <remote_path:string>` - push a local trigger spec. This overrides any remote versions.
|
||||||
|
|
||||||
### user
|
### user
|
||||||
@@ -342,8 +427,16 @@ user related commands
|
|||||||
|
|
||||||
variable related commands
|
variable related commands
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
|
||||||
**Subcommands:**
|
**Subcommands:**
|
||||||
|
|
||||||
|
- `variable list` - list all variables
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `variable get <path:string>` - get a variable's details
|
||||||
|
- `--json` - Output as JSON (for piping to jq)
|
||||||
|
- `variable new <path:string>` - create a new variable locally
|
||||||
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
|
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
|
||||||
- `--plain-secrets` - Push secrets as plain text
|
- `--plain-secrets` - Push secrets as plain text
|
||||||
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
|
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
|
||||||
@@ -392,7 +485,8 @@ workspace related commands
|
|||||||
- `--create-username <username:string>` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.
|
- `--create-username <username:string>` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.
|
||||||
- `workspace remove <workspace_name:string>` - Remove a workspace
|
- `workspace remove <workspace_name:string>` - Remove a workspace
|
||||||
- `workspace whoami` - Show the currently active user
|
- `workspace whoami` - Show the currently active user
|
||||||
- `workspace list` - List workspaces on the remote server that you have access to
|
- `workspace list` - List local workspace profiles
|
||||||
|
- `workspace list-remote` - List workspaces on the remote server that you have access to
|
||||||
- `workspace bind` - Bind the current Git branch to the active workspace
|
- `workspace bind` - Bind the current Git branch to the active workspace
|
||||||
- `--branch <branch:string>` - Specify branch (defaults to current)
|
- `--branch <branch:string>` - Specify branch (defaults to current)
|
||||||
- `workspace unbind` - Remove workspace binding from the current Git branch
|
- `workspace unbind` - Remove workspace binding from the current Git branch
|
||||||
|
|||||||
@@ -188,40 +188,16 @@ npm test:watch
|
|||||||
|
|
||||||
### Testing locally with the CLI
|
### Testing locally with the CLI
|
||||||
|
|
||||||
The Windmill CLI (`cli/`) is Deno-based and imports this package via `npm:windmill-yaml-validator@1.1.0`. Since Deno's `npm:` specifier always resolves from the npm registry, local testing requires a compatibility script that makes the TypeScript sources directly importable by Deno.
|
To test local changes before publishing, use `npm link`:
|
||||||
|
|
||||||
The `deno-compat.sh` script handles two Deno requirements:
|
|
||||||
- Adding `.ts` extensions to relative imports
|
|
||||||
- Adding `with { type: "json" }` assertions to JSON imports
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
|
|
||||||
1. Apply Deno compatibility:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deno-compat.sh
|
# In windmill-yaml-validator/
|
||||||
```
|
npm run build
|
||||||
|
npm link
|
||||||
|
|
||||||
2. Add the following entries to `cli/deno.json` imports:
|
# In cli/
|
||||||
|
npm link windmill-yaml-validator
|
||||||
```json
|
bun run src/main.ts lint
|
||||||
"npm:windmill-yaml-validator@1.1.0": "../windmill-yaml-validator/src/index.ts",
|
|
||||||
"ajv": "npm:ajv@^8.17.1",
|
|
||||||
"@stoplight/yaml": "npm:@stoplight/yaml@^4.3.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the CLI directly with Deno:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ../cli
|
|
||||||
deno run -A src/main.ts lint
|
|
||||||
```
|
|
||||||
|
|
||||||
4. When done, restore everything:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deno-compat.sh -r # restore original imports
|
|
||||||
# Remove the 3 import map lines from cli/deno.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Schema Generation
|
### Schema Generation
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Makes windmill-yaml-validator source files Deno-compatible by:
|
|
||||||
# 1. Adding .ts extensions to relative imports
|
|
||||||
# 2. Adding `with { type: "json" }` to JSON imports
|
|
||||||
# Use -r to restore (undo changes).
|
|
||||||
|
|
||||||
set -e
|
|
||||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
RESTORE_MODE=false
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
-r)
|
|
||||||
RESTORE_MODE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
echo "Usage: $0 [-r]"
|
|
||||||
echo " -r Restore original imports"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
SED=gsed
|
|
||||||
if ! command -v gsed &> /dev/null; then
|
|
||||||
echo "Error: gsed not found. Run: brew install gnu-sed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
SED=sed
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$RESTORE_MODE" == true ]]; then
|
|
||||||
echo "Restoring original imports..."
|
|
||||||
find "$script_dirpath"/src -name "*.ts" -type f ! -path '*__tests__*' | while read -r file; do
|
|
||||||
# Remove .ts from relative imports: from "./foo.ts" -> from "./foo"
|
|
||||||
$SED -E -i 's|(from "\.\.?/[^"]*)\.ts(")|\1\2|g' "$file"
|
|
||||||
# Remove ` with { type: "json" }` from JSON imports
|
|
||||||
$SED -E -i 's/ with \{ type: "json" \}//' "$file"
|
|
||||||
done
|
|
||||||
echo "✓ Restored original imports"
|
|
||||||
else
|
|
||||||
echo "Making sources Deno-compatible..."
|
|
||||||
find "$script_dirpath"/src -name "*.ts" -type f ! -path '*__tests__*' | while read -r file; do
|
|
||||||
# Add .ts to relative imports that don't already end in .ts or .json
|
|
||||||
$SED -E -i '/\.json"/! { /\.ts"/! s|(from "(\.\.?/[^"]*[^/]))"(;?)$|\1.ts"\3|; }' "$file"
|
|
||||||
# Add `with { type: "json" }` to .json imports that don't already have it
|
|
||||||
$SED -E -i '/with \{ type: "json" \}/! s/(from "[^"]*\.json")(;?)$/\1 with { type: "json" }\2/' "$file"
|
|
||||||
done
|
|
||||||
echo "✓ Sources are now Deno-compatible"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user