feat(cli): add consistent get/list/new subcommands for all item types (#8047)
* feat(cli): add consistent get/list/new subcommands for all item types Make the CLI consistent so every item type (script, flow, app, resource, resource-type, variable, schedule, folder, trigger) supports get/list/new subcommands, enabling the CLI to be used as a full API client in bash scripts with jq piping. - Add --json flag to all list commands for machine-readable output - Register explicit "list" subcommand alongside default action - Add "get <path> [--json]" subcommand to fetch single items from API - Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias - Add "new" subcommand for resource, resource-type, variable, schedule, folder, and trigger to create local template YAML files - Update cli-commands skill documentation for wmill init - Add integration tests for all new commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * feat: install wmill CLI in Docker images and use it for bash variable/resource access - Install windmill-cli via bun in all Dockerfiles that include bun - DockerfileCli: switch from node:slim to oven/bun:slim - CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars as last-resort fallback when no workspace is configured - Frontend: replace curl-based bash snippets with wmill variable/resource get - Add backend integration tests for wmill CLI in bash scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install windmill-cli in backend test workflow Ensures wmill is available on PATH for bash integration tests that use `wmill variable get` and `wmill resource get`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(cli): replace @std/* Deno dependencies with Node.js equivalents Replace @std/log with a lightweight custom logger (core/log.ts), @std/path with node:path, and @std/yaml with the yaml npm package. Also fix process hang on exit, add --node option to install_dev.sh, and add missing hasRequiredPermissions to NpmProvider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * all * refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation, replace @std/encoding with Node.js Buffer.toString("hex"), and fix @windmill-labs/shared-utils to use direct npm instead of JSR mirror. Also resolve merge conflicts in sync.ts and fix pre-existing type errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull The yaml library defaults to double quotes, but the codebase (and tests) expect single-quoted strings. Add singleQuote: true to yamlOptions and pass yamlOptions to gitsync-settings pull writeFile calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * fix(cli): address code review feedback - Install CLI from source in backend tests instead of npm - Fix script bootstrap catch block to re-throw "File already exists" - Add type-safe local variable after trigger kind validation - Use created_by instead of policy.on_behalf_of for app get output - Note --kind is recommended for faster trigger lookup in help text - Document node symlink purpose in Dockerfiles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install wmill to ~/.local/bin to avoid permission issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci(backend): switch to Blacksmith runner and add cargo caching - Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds - Add stickydisk for cargo target directory (persistent NVMe cache across runs) - Add cache for cargo registry and git dependencies - Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1 - Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix ci --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
.github/DockerfileBackendTests
vendored
4
.github/DockerfileBackendTests
vendored
@@ -44,6 +44,10 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
|
||||
# Bun
|
||||
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# Install windmill CLI
|
||||
RUN bun install -g windmill-cli \
|
||||
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Deno
|
||||
|
||||
29
.github/workflows/backend-test.yml
vendored
29
.github/workflows/backend-test.yml
vendored
@@ -19,7 +19,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
cargo_test:
|
||||
runs-on: ubicloud-standard-16
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
@@ -70,6 +70,16 @@ jobs:
|
||||
with:
|
||||
ruby-version: "3.3"
|
||||
bundler-cache: false
|
||||
- name: Install windmill CLI from source
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/cli
|
||||
bash gen_wm_client.sh
|
||||
bun install
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
|
||||
chmod +x "$HOME/.local/bin/wmill"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
working-directory: /
|
||||
- name: Install PowerShell, mold and clang
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev
|
||||
@@ -78,6 +88,20 @@ jobs:
|
||||
with:
|
||||
cache: false
|
||||
toolchain: 1.93.0
|
||||
- name: Cache cargo target directory
|
||||
uses: useblacksmith/stickydisk@v1
|
||||
with:
|
||||
key: cargo-target
|
||||
path: ./backend/target
|
||||
- name: Cache cargo registry
|
||||
uses: useblacksmith/cache@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: cargo-registry-${{ hashFiles('backend/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-registry-
|
||||
- name: Read EE repo commit hash
|
||||
run: |
|
||||
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
|
||||
@@ -205,7 +229,7 @@ jobs:
|
||||
fi
|
||||
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
|
||||
- name: Cache DuckDB FFI module build
|
||||
uses: actions/cache@v3
|
||||
uses: useblacksmith/cache@v1
|
||||
with:
|
||||
path: ./backend/windmill-duckdb-ffi-internal/target
|
||||
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
|
||||
@@ -221,6 +245,7 @@ jobs:
|
||||
RUST_LOG_STYLE: never
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
CARGO_INCREMENTAL: 1
|
||||
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
|
||||
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
|
||||
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
|
||||
|
||||
@@ -258,6 +258,10 @@ COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
|
||||
|
||||
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# Install windmill CLI
|
||||
RUN bun install -g windmill-cli \
|
||||
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
|
||||
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
10
backend/tests/fixtures/wmill_cli_test.sql
vendored
Normal file
10
backend/tests/fixtures/wmill_cli_test.sql
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Fixture for testing wmill CLI variable/resource get from bash scripts
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/test_var', 'hello from variable', false, 'A test variable', '{"u/test-user": true}');
|
||||
|
||||
INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
|
||||
VALUES ('test-workspace', 'test_object', '{}', 'Test object type', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/test_res', '{"host": "localhost", "port": 5432}', 'A test resource', 'test_object', '{"u/test-user": true}', 'test-user');
|
||||
@@ -993,6 +993,80 @@ echo "hello $msg"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "wmill_cli_test"))]
|
||||
async fn test_bash_wmill_variable_get(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// The bash script uses wmill CLI to get the variable value.
|
||||
// The worker sets WM_TOKEN, WM_WORKSPACE, and BASE_INTERNAL_URL as env vars,
|
||||
// and the CLI auto-configures from them when no workspace is explicitly set.
|
||||
// We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes.
|
||||
let content = r#"
|
||||
export WMILL_CONFIG_DIR=$(mktemp -d)
|
||||
result=$(wmill variable get "u/test-user/test_var" --json | jq -r .value)
|
||||
echo "$result"
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Bash,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
assert_eq!(job.json_result(), Some(json!("hello from variable")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "wmill_cli_test"))]
|
||||
async fn test_bash_wmill_resource_get(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// The bash script uses wmill CLI to get the resource value.
|
||||
// We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes.
|
||||
let content = r#"
|
||||
export WMILL_CONFIG_DIR=$(mktemp -d)
|
||||
result=$(wmill resource get "u/test-user/test_res" --json | jq -c .value)
|
||||
echo "$result"
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Bash,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
// Bash echo outputs are returned as strings, so the JSON is a string value
|
||||
assert_eq!(
|
||||
job.json_result(),
|
||||
Some(json!("{\"host\":\"localhost\",\"port\":5432}"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "nu")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nu_job(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
|
||||
47
cli/bun.lock
47
cli/bun.lock
@@ -5,16 +5,11 @@
|
||||
"": {
|
||||
"name": "windmill-cli-dev",
|
||||
"dependencies": {
|
||||
"@ayonli/jsext": "^1.9.0",
|
||||
"@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0",
|
||||
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
|
||||
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
|
||||
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
|
||||
"@std/encoding": "npm:@jsr/std__encoding@1.0.10",
|
||||
"@std/log": "npm:@jsr/std__log@0.224.14",
|
||||
"@std/path": "npm:@jsr/std__path@1.1.4",
|
||||
"@std/yaml": "npm:@jsr/std__yaml@1.0.10",
|
||||
"@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12",
|
||||
"@windmill-labs/shared-utils": "^1.0.12",
|
||||
"diff": "^5.2.0",
|
||||
"esbuild": "0.24.2",
|
||||
"get-port": "7.1.0",
|
||||
@@ -22,6 +17,7 @@
|
||||
"minimatch": "^10.0.0",
|
||||
"open": "^10.0.0",
|
||||
"svelte": "^5.45.2",
|
||||
"tar-stream": "^3.1.7",
|
||||
"windmill-parser-wasm-csharp": "*",
|
||||
"windmill-parser-wasm-go": "*",
|
||||
"windmill-parser-wasm-java": "*",
|
||||
@@ -40,14 +36,13 @@
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.2.3",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/tar-stream": "^3.1.4",
|
||||
"@types/ws": "^8.5.0",
|
||||
"typescript": "^5.7.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ayonli/jsext": ["@ayonli/jsext@1.9.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sudo-prompt": "^9.2.1", "ws": "^8.17.0", "zod": "^3.23.8" } }, "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g=="],
|
||||
|
||||
"@cliffy/ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="],
|
||||
|
||||
"@cliffy/command": ["@jsr/cliffy__command@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__command/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__flags": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__table": "1.0.0", "@jsr/std__fmt": "^1.0.9", "@jsr/std__semver": "^1.0.8", "@jsr/std__text": "^1.0.17" } }, "sha512-oObplVtu1tvpkhgpuPDHZidx9g3axVOfRMQGmw7ZSGxp0+vZIJGiEtpcSvlN0XfuEhOG8neqfVBSSE9txrKanw=="],
|
||||
@@ -134,8 +129,6 @@
|
||||
|
||||
"@jsr/std__fmt": ["@jsr/std__fmt@1.0.9", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz", {}, "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw=="],
|
||||
|
||||
"@jsr/std__fs": ["@jsr/std__fs@1.0.21", "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12", "@jsr/std__path": "^1.1.4" } }, "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg=="],
|
||||
|
||||
"@jsr/std__internal": ["@jsr/std__internal@1.0.12", "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz", {}, "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA=="],
|
||||
|
||||
"@jsr/std__io": ["@jsr/std__io@0.225.3", "https://npm.jsr.io/~/11/@jsr/std__io/0.225.3.tgz", { "dependencies": { "@jsr/std__bytes": "^1.0.6" } }, "sha512-IDXY253ipW6FV34CJVxO+3ubfvSEEzw9N2W303KnLe9K/Y9+v/ID1dQYf9VsCCOFMpFtCmOLqzIZsRqv6yQnWw=="],
|
||||
@@ -148,14 +141,6 @@
|
||||
|
||||
"@jsr/std__text": ["@jsr/std__text@1.0.17", "https://npm.jsr.io/~/11/@jsr/std__text/1.0.17.tgz", { "dependencies": { "@jsr/std__regexp": "^1.0.1" } }, "sha512-oZsihl1bcTy1Ixzven8rin8kjChj1zDJWqgpS0oSMGCJDzyB365gtIfAvcMmji+M+FcIWo3goDXfHcFYt+k/kg=="],
|
||||
|
||||
"@std/encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="],
|
||||
|
||||
"@std/log": ["@jsr/std__log@0.224.14", "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.5", "@jsr/std__fs": "^1.0.11", "@jsr/std__io": "^0.225.2" } }, "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ=="],
|
||||
|
||||
"@std/path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="],
|
||||
|
||||
"@std/yaml": ["@jsr/std__yaml@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz", {}, "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA=="],
|
||||
|
||||
"@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="],
|
||||
|
||||
"@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
|
||||
@@ -174,11 +159,13 @@
|
||||
|
||||
"@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
|
||||
|
||||
"@types/tar-stream": ["@types/tar-stream@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@windmill-labs/shared-utils": ["@jsr/windmill-labs__shared-utils@1.0.12", "https://npm.jsr.io/~/11/@jsr/windmill-labs__shared-utils/1.0.12.tgz", {}, "sha512-bJOacyfxxNPwNTzA4AxCB5iGFop0h3mCgs+E9j3ZaJYDo1soblY16CebnQ56EPy/M3V344X/QoOFBORyRo1Mnw=="],
|
||||
"@windmill-labs/shared-utils": ["@windmill-labs/shared-utils@1.0.12", "", {}, "sha512-n68uEYv2B5q2Pp8J9syMS3qPZbppFEfeM7HIBEUfU5lGqi3hwnv4mPvgRUyb6K9im3frXC4gzdIdZdlrDpudXQ=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
@@ -188,8 +175,12 @@
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
|
||||
|
||||
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
@@ -214,14 +205,16 @@
|
||||
|
||||
"esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
|
||||
|
||||
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
@@ -262,16 +255,18 @@
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="],
|
||||
|
||||
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="],
|
||||
|
||||
"svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
@@ -313,7 +308,5 @@
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
# Parse options
|
||||
USE_NODE=false
|
||||
name=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--node|-node|---node) USE_NODE=true ;;
|
||||
-*) echo "Unknown option: $arg"; echo "Usage: $0 [name] [--node]"; exit 1 ;;
|
||||
*) [ -z "$name" ] && name="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
name="wmill-dev"
|
||||
else
|
||||
name="$1"
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -18,10 +27,25 @@ bun install
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
cat > "$INSTALL_DIR/$name" <<EOF
|
||||
if [ "$USE_NODE" = true ]; then
|
||||
echo "Building npm bundle..."
|
||||
bun run build-npm.ts
|
||||
|
||||
NPM_DIR="$SCRIPT_DIR/npm"
|
||||
cd "$NPM_DIR" && npm install
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
cat > "$INSTALL_DIR/$name" <<EOF
|
||||
#!/bin/sh
|
||||
exec node "$NPM_DIR/esm/main.js" "\$@"
|
||||
EOF
|
||||
else
|
||||
cat > "$INSTALL_DIR/$name" <<EOF
|
||||
#!/bin/sh
|
||||
exec bun run "$SCRIPT_DIR/src/main.ts" "\$@"
|
||||
EOF
|
||||
fi
|
||||
|
||||
chmod +x "$INSTALL_DIR/$name"
|
||||
|
||||
echo "Installed dev cli as '$name' at $INSTALL_DIR/$name"
|
||||
@@ -29,4 +53,4 @@ echo "Installed dev cli as '$name' at $INSTALL_DIR/$name"
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
||||
echo "Warning: $INSTALL_DIR is not in your PATH. Add it with:"
|
||||
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
fi
|
||||
fi
|
||||
|
||||
1498
cli/package-lock.json
generated
Normal file
1498
cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,23 +13,19 @@
|
||||
"gen-client": "./gen_wm_client.sh && ./windmill-utils-internal/gen_wm_client.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ayonli/jsext": "^1.9.0",
|
||||
"@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0",
|
||||
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
|
||||
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
|
||||
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
|
||||
"@std/encoding": "npm:@jsr/std__encoding@1.0.10",
|
||||
"@std/log": "npm:@jsr/std__log@0.224.14",
|
||||
"@std/path": "npm:@jsr/std__path@1.1.4",
|
||||
"@std/yaml": "npm:@jsr/std__yaml@1.0.10",
|
||||
"@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12",
|
||||
"@windmill-labs/shared-utils": "^1.0.12",
|
||||
"diff": "^5.2.0",
|
||||
"esbuild": "0.24.2",
|
||||
"svelte": "^5.45.2",
|
||||
"get-port": "7.1.0",
|
||||
"jszip": "3.8.0",
|
||||
"minimatch": "^10.0.0",
|
||||
"open": "^10.0.0",
|
||||
"svelte": "^5.45.2",
|
||||
"tar-stream": "^3.1.7",
|
||||
"windmill-parser-wasm-csharp": "*",
|
||||
"windmill-parser-wasm-go": "*",
|
||||
"windmill-parser-wasm-java": "*",
|
||||
@@ -47,8 +43,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.2.3",
|
||||
"@types/ws": "^8.5.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/tar-stream": "^3.1.4",
|
||||
"@types/ws": "^8.5.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import * as windmillUtils from "@windmill-labs/shared-utils";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
@@ -185,7 +185,7 @@ export async function generatingPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) {
|
||||
async function list(opts: GlobalOptions & { includeDraftOnly?: boolean; json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
@@ -206,12 +206,32 @@ async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) {
|
||||
}
|
||||
}
|
||||
|
||||
new Table()
|
||||
.header(["path", "summary"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(total));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["path", "summary"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const a = await wmill.getAppByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(a));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + a.path);
|
||||
console.log(colors.bold("Summary:") + " " + (a.summary ?? ""));
|
||||
console.log(colors.bold("Created by:") + " " + (a.created_by ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
@@ -227,7 +247,15 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
|
||||
const command = new Command()
|
||||
.description("app related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all apps")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get an app's details")
|
||||
.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>")
|
||||
.action(push as any)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { readFile, mkdir } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import {
|
||||
checkifMetadataUptodate,
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import process from "node:process";
|
||||
import { spawn } from "node:child_process";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as windmillUtils from "@windmill-labs/shared-utils";
|
||||
export interface BundleOptions {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import * as windmillUtils from "@windmill-labs/shared-utils";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as getPort from "get-port";
|
||||
|
||||
@@ -5,7 +5,7 @@ import process from "node:process";
|
||||
|
||||
import { Command } from "@cliffy/command";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
||||
import process from "node:process";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { createBundle } from "./bundle.ts";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
import * as log from "@std/log";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { generateAgentsDocumentation, generateDatatablesDocumentation, yamlOptions } from "../sync/sync.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import * as windmillUtils from "@windmill-labs/shared-utils";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { Policy } from "../../../gen/types.gen.ts";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import fs from "node:fs";
|
||||
import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { readFile } from "node:fs/promises";
|
||||
@@ -113,7 +113,7 @@ async function push(opts: Options, filePath: string, remotePath: string) {
|
||||
}
|
||||
|
||||
async function list(
|
||||
opts: GlobalOptions & { showArchived?: boolean; includeDraftOnly?: boolean }
|
||||
opts: GlobalOptions & { showArchived?: boolean; includeDraftOnly?: boolean; json?: boolean }
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -136,13 +136,35 @@ async function list(
|
||||
}
|
||||
}
|
||||
|
||||
new Table()
|
||||
.header(["path", "summary", "edited by"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary, x.edited_by]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(total));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["path", "summary", "edited by"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary, x.edited_by]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const f = await wmill.getFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(f));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + f.path);
|
||||
console.log(colors.bold("Summary:") + " " + (f.summary ?? ""));
|
||||
console.log(colors.bold("Description:") + " " + (f.description ?? ""));
|
||||
console.log(colors.bold("Edited by:") + " " + (f.edited_by ?? ""));
|
||||
console.log(colors.bold("Edited at:") + " " + (f.edited_at ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
async function run(
|
||||
opts: GlobalOptions & {
|
||||
data?: string;
|
||||
@@ -375,8 +397,17 @@ export function bootstrap(
|
||||
|
||||
const command = new Command()
|
||||
.description("flow related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
.option("--show-archived", "Enable archived flows in output")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all flows")
|
||||
.option("--show-archived", "Enable archived flows in output")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a flow's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local flow spec. This overrides any remote versions."
|
||||
@@ -423,10 +454,15 @@ const command = new Command()
|
||||
"Comma separated patterns to specify which file to NOT take into account."
|
||||
)
|
||||
.action(generateLocks as any)
|
||||
.command("bootstrap", "create a new empty flow")
|
||||
.command("new", "create a new empty flow")
|
||||
.arguments("<flow_path:string>")
|
||||
.option("--summary <summary:string>", "script summary")
|
||||
.option("--description <description:string>", "script description")
|
||||
.option("--summary <summary:string>", "flow summary")
|
||||
.option("--description <description:string>", "flow description")
|
||||
.action(bootstrap as any)
|
||||
.command("bootstrap", "create a new empty flow (alias for new)")
|
||||
.arguments("<flow_path:string>")
|
||||
.option("--summary <summary:string>", "flow summary")
|
||||
.option("--description <description:string>", "flow description")
|
||||
.action(bootstrap as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile, mkdir } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
@@ -18,7 +19,7 @@ export interface FolderFile {
|
||||
display_name: string | undefined;
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
@@ -26,18 +27,60 @@ async function list(opts: GlobalOptions) {
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
new Table()
|
||||
.header(["Name", "Owners", "Extra Perms"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
folders.map((x) => [
|
||||
x.name,
|
||||
x.owners?.join(",") ?? "-",
|
||||
JSON.stringify(x.extra_perms ?? {}),
|
||||
])
|
||||
)
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(folders));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Name", "Owners", "Extra Perms"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
folders.map((x) => [
|
||||
x.name,
|
||||
x.owners?.join(",") ?? "-",
|
||||
JSON.stringify(x.extra_perms ?? {}),
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function newFolder(opts: GlobalOptions, name: string) {
|
||||
const dirPath = `f${SEP}${name}`;
|
||||
const filePath = `${dirPath}${SEP}folder.meta.yaml`;
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template: Omit<FolderFile, "display_name"> = {
|
||||
owners: [],
|
||||
extra_perms: {},
|
||||
};
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, name: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const f = await wmill.getFolder({
|
||||
workspace: workspace.workspaceId,
|
||||
name,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(f));
|
||||
} else {
|
||||
console.log(colors.bold("Name:") + " " + f.name);
|
||||
console.log(colors.bold("Summary:") + " " + (f.summary ?? ""));
|
||||
console.log(colors.bold("Owners:") + " " + (f.owners?.join(", ") ?? "-"));
|
||||
console.log(colors.bold("Extra Perms:") + " " + JSON.stringify(f.extra_perms ?? {}));
|
||||
}
|
||||
}
|
||||
|
||||
export async function pushFolder(
|
||||
@@ -126,7 +169,18 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
|
||||
const command = new Command()
|
||||
.description("folder related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all folders")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a folder's details")
|
||||
.arguments("<name:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("new", "create a new folder locally")
|
||||
.arguments("<name:string>")
|
||||
.action(newFolder as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local folder spec. This overrides any remote versions."
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { SyncOptions, readConfigFile, getEffectiveSettings, DEFAULT_SYNC_OPTIONS, getWmillYamlPath } from "../../core/conf.ts";
|
||||
import { yamlOptions } from "../sync/sync.ts";
|
||||
import { deepEqual } from "../../utils/utils.ts";
|
||||
import { getCurrentGitBranch, isGitRepository } from "../../utils/git.ts";
|
||||
|
||||
@@ -176,7 +177,7 @@ export async function pullGitSyncSettings(
|
||||
}
|
||||
|
||||
// Write the new configuration
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8");
|
||||
|
||||
if (opts.jsonOutput) {
|
||||
console.log(
|
||||
@@ -372,7 +373,7 @@ export async function pullGitSyncSettings(
|
||||
}
|
||||
|
||||
// Write updated configuration
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8");
|
||||
|
||||
if (opts.jsonOutput) {
|
||||
console.log(
|
||||
@@ -449,7 +450,7 @@ export async function pullGitSyncSettings(
|
||||
}
|
||||
|
||||
// Write updated configuration
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
|
||||
await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8");
|
||||
|
||||
if (opts.jsonOutput) {
|
||||
console.log(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import process from "node:process";
|
||||
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { deepEqual, selectRepository } from "../../utils/utils.ts";
|
||||
import { SyncOptions, getEffectiveSettings, DEFAULT_SYNC_OPTIONS } from "../../core/conf.ts";
|
||||
import { GitSyncRepository, GIT_SYNC_FIELDS } from "./types.ts";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { stat, writeFile, rm, mkdir } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { readLockfile } from "../../utils/metadata.ts";
|
||||
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts";
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import * as path from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as path from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { setClient } from "../../core/client.ts";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
@@ -3,9 +3,9 @@ import process from "node:process";
|
||||
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { pickInstance } from "../instance/instance.ts";
|
||||
|
||||
@@ -124,7 +124,7 @@ async function displayQueues(opts: GlobalOptions, workspace?: string) {
|
||||
table.body(body).render();
|
||||
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch queue metrics:", error);
|
||||
log.error(`Failed to fetch queue metrics: ${error}`);
|
||||
}
|
||||
} else {
|
||||
log.info("No active instance found");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import {
|
||||
GlobalOptions,
|
||||
@@ -14,7 +15,7 @@ import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { ResourceType } from "../../../gen/types.gen.ts";
|
||||
import { compileResourceTypeToTsType } from "../../utils/resource_types.ts";
|
||||
@@ -85,14 +86,16 @@ async function push(opts: PushOptions, filePath: string, name: string) {
|
||||
log.info(colors.bold.underline.green("Resource pushed"));
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions & { schema?: boolean }) {
|
||||
async function list(opts: GlobalOptions & { schema?: boolean; json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const res = await wmill.listResourceType({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (opts.schema) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(res));
|
||||
} else if (opts.schema) {
|
||||
new Table()
|
||||
.header(["Workspace", "Name", "Schema"])
|
||||
.padding(2)
|
||||
@@ -115,6 +118,44 @@ async function list(opts: GlobalOptions & { schema?: boolean }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function newResourceType(opts: GlobalOptions, name: string) {
|
||||
const filePath = name + ".resource-type.yaml";
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template: ResourceTypeFile = {
|
||||
schema: {},
|
||||
description: "",
|
||||
};
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const rt = await wmill.getResourceType({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(rt));
|
||||
} else {
|
||||
console.log(colors.bold("Name:") + " " + rt.name);
|
||||
console.log(colors.bold("Description:") + " " + (rt.description ?? ""));
|
||||
console.log(colors.bold("Workspace:") + " " + (rt.workspace_id ?? "Global"));
|
||||
if (rt.schema) {
|
||||
console.log(colors.bold("Schema:") + " " + JSON.stringify(rt.schema, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateRTNamespace(opts: GlobalOptions) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -146,10 +187,19 @@ export async function generateRTNamespace(opts: GlobalOptions) {
|
||||
|
||||
const command = new Command()
|
||||
.description("resource type related commands")
|
||||
.action(() => log.info("2 actions available, list and push."))
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all resource types")
|
||||
.option("--schema", "Show schema in the output")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a resource type's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("new", "create a new resource type locally")
|
||||
.arguments("<name:string>")
|
||||
.action(newResourceType as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local resource spec. This overrides any remote versions."
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import {
|
||||
GlobalOptions,
|
||||
@@ -11,8 +12,8 @@ import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { Resource } from "../../../gen/types.gen.ts";
|
||||
import { readInlinePathSync } from "../../utils/utils.ts";
|
||||
@@ -131,7 +132,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
|
||||
log.info(colors.bold.underline.green(`Resource ${remotePath} pushed`));
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
let page = 0;
|
||||
@@ -150,17 +151,73 @@ async function list(opts: GlobalOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
new Table()
|
||||
.header(["Path", "Resource Type"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.resource_type]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(total));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Path", "Resource Type"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.resource_type]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function newResource(opts: GlobalOptions, path: string) {
|
||||
if (!validatePath(path)) {
|
||||
return;
|
||||
}
|
||||
const filePath = path + ".resource.yaml";
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
// file doesn't exist, proceed
|
||||
}
|
||||
const template: ResourceFile = {
|
||||
value: {},
|
||||
resource_type: "",
|
||||
description: "",
|
||||
};
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const r = await wmill.getResource({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(r));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + r.path);
|
||||
console.log(colors.bold("Resource Type:") + " " + (r.resource_type ?? ""));
|
||||
console.log(colors.bold("Description:") + " " + (r.description ?? ""));
|
||||
console.log(colors.bold("Value:") + " " + JSON.stringify(r.value, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("resource related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all resources")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a resource's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("new", "create a new resource locally")
|
||||
.arguments("<path:string>")
|
||||
.action(newResource as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local resource spec. This overrides any remote versions."
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
@@ -27,7 +28,7 @@ export interface ScheduleFile {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
@@ -35,12 +36,62 @@ async function list(opts: GlobalOptions) {
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
new Table()
|
||||
.header(["Path", "Schedule"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(schedules.map((x) => [x.path, x.schedule]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(schedules));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Path", "Schedule"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(schedules.map((x) => [x.path, x.schedule]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function newSchedule(opts: GlobalOptions, path: string) {
|
||||
if (!validatePath(path)) {
|
||||
return;
|
||||
}
|
||||
const filePath = path + ".schedule.yaml";
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template: ScheduleFile = {
|
||||
schedule: "0 */6 * * *",
|
||||
on_failure: "",
|
||||
script_path: "",
|
||||
args: {},
|
||||
timezone: "Etc/UTC",
|
||||
is_flow: false,
|
||||
enabled: false,
|
||||
};
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const s = await wmill.getSchedule({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(s));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + s.path);
|
||||
console.log(colors.bold("Schedule:") + " " + s.schedule);
|
||||
console.log(colors.bold("Timezone:") + " " + (s.timezone ?? ""));
|
||||
console.log(colors.bold("Script Path:") + " " + (s.script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + (s.is_flow ? "true" : "false"));
|
||||
console.log(colors.bold("Enabled:") + " " + (s.enabled ? "true" : "false"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function pushSchedule(
|
||||
@@ -137,7 +188,18 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
|
||||
const command = new Command()
|
||||
.description("schedule related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all schedules")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a schedule's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("new", "create a new schedule locally")
|
||||
.arguments("<path:string>")
|
||||
.action(newSchedule as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local schedule spec. This overrides any remote versions."
|
||||
|
||||
@@ -7,9 +7,9 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { deepEqual } from "../../utils/utils.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import * as specificItems from "../../core/specific_items.ts";
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from "../../core/conf.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts";
|
||||
import fs from "node:fs";
|
||||
import { type Tarball } from "@ayonli/jsext/archive";
|
||||
import { createTarBlob, type TarEntry } from "../../utils/tar.ts";
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { NewScript, Script } from "../../../gen/types.gen.ts";
|
||||
@@ -246,7 +246,7 @@ export async function handleFile(
|
||||
const codebase =
|
||||
language == "bun" ? findCodebase(path, codebases) : undefined;
|
||||
|
||||
let bundleContent: string | Tarball | undefined = undefined;
|
||||
let bundleContent: string | Blob | undefined = undefined;
|
||||
|
||||
let forceTar = false;
|
||||
if (codebase) {
|
||||
@@ -292,7 +292,6 @@ export async function handleFile(
|
||||
);
|
||||
}
|
||||
if (outputFiles.length > 1) {
|
||||
const archiveNpm = await import("@ayonli/jsext/archive");
|
||||
log.info(
|
||||
`Found multiple output files for ${path}, creating a tarball... ${outputFiles
|
||||
.map((file) => file.path)
|
||||
@@ -300,54 +299,49 @@ export async function handleFile(
|
||||
);
|
||||
forceTar = true;
|
||||
const startTime = performance.now();
|
||||
const tarball = new archiveNpm.Tarball();
|
||||
const mainPath = path.split(SEP).pop()?.split(".")[0] + ".js";
|
||||
const content =
|
||||
const mainContent =
|
||||
outputFiles.find((file) => file.path == "/" + mainPath)?.text ?? "";
|
||||
log.info(`Main content: ${content.length}chars`);
|
||||
tarball.append(new File([content], "main.js", { type: "text/plain" }));
|
||||
log.info(`Main content: ${mainContent.length}chars`);
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: mainContent },
|
||||
];
|
||||
for (const file of outputFiles) {
|
||||
if (file.path == "/" + mainPath) {
|
||||
continue;
|
||||
}
|
||||
log.info(`Adding file: ${file.path.substring(1)}`);
|
||||
|
||||
const fil = new File([file.contents as any], file.path.substring(1));
|
||||
tarball.append(fil);
|
||||
entries.push({ name: file.path.substring(1), content: file.contents });
|
||||
}
|
||||
bundleContent = await createTarBlob(entries);
|
||||
const endTime = performance.now();
|
||||
log.info(
|
||||
`Finished creating tarball for ${path}: ${(
|
||||
tarball.size / 1024
|
||||
bundleContent.size / 1024
|
||||
).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`
|
||||
);
|
||||
bundleContent = tarball;
|
||||
} else {
|
||||
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
||||
const archiveNpm = await import("@ayonli/jsext/archive");
|
||||
log.info(
|
||||
`Using the following asset configuration for ${path}: ${JSON.stringify(
|
||||
codebase.assets
|
||||
)}`
|
||||
);
|
||||
const startTime = performance.now();
|
||||
const tarball = new archiveNpm.Tarball();
|
||||
tarball.append(
|
||||
new File([bundleContent], "main.js", { type: "text/plain" })
|
||||
);
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: bundleContent },
|
||||
];
|
||||
for (const asset of codebase.assets) {
|
||||
const data = fs.readFileSync(asset.from);
|
||||
const blob = new Blob([data], { type: "text/plain" });
|
||||
const file = new File([blob], asset.to);
|
||||
tarball.append(file);
|
||||
entries.push({ name: asset.to, content: data });
|
||||
}
|
||||
bundleContent = await createTarBlob(entries);
|
||||
const endTime = performance.now();
|
||||
log.info(
|
||||
`Finished creating tarball for ${path}: ${(
|
||||
tarball.size / 1024
|
||||
bundleContent.size / 1024
|
||||
).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`
|
||||
);
|
||||
bundleContent = tarball;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,31 +506,8 @@ export async function handleFile(
|
||||
return false;
|
||||
}
|
||||
|
||||
async function streamToBlob(stream: ReadableStream<Uint8Array>): Promise<Blob> {
|
||||
// Create a reader from the stream
|
||||
const reader = stream.getReader();
|
||||
const chunks = [];
|
||||
|
||||
// Read the data from the stream
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
// If stream is finished, break the loop
|
||||
break;
|
||||
}
|
||||
|
||||
// Push the chunk to the array
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
|
||||
const blob = new Blob(chunks as any);
|
||||
return blob;
|
||||
}
|
||||
|
||||
async function createScript(
|
||||
bundleContent: string | Tarball | undefined,
|
||||
bundleContent: string | Blob | undefined,
|
||||
workspaceId: string,
|
||||
body: NewScript,
|
||||
workspace: Workspace
|
||||
@@ -563,7 +534,7 @@ async function createScript(
|
||||
"file",
|
||||
typeof bundleContent == "string"
|
||||
? bundleContent
|
||||
: await streamToBlob(bundleContent.stream())
|
||||
: bundleContent
|
||||
);
|
||||
|
||||
const url =
|
||||
@@ -726,6 +697,7 @@ async function list(
|
||||
showArchived?: boolean;
|
||||
includeWithoutMain?: boolean;
|
||||
includeDraftOnly?: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -750,12 +722,16 @@ async function list(
|
||||
}
|
||||
}
|
||||
|
||||
new Table()
|
||||
.header(["path", "summary", "language", "created by"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary, x.language, x.created_by]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(total));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["path", "summary", "language", "created by"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(total.map((x) => [x.path, x.summary, x.language, x.created_by]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolve(input: string): Promise<Record<string, any>> {
|
||||
@@ -916,6 +892,26 @@ async function show(opts: GlobalOptions, path: string) {
|
||||
log.info(s.content);
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const s = await wmill.getScriptByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(s));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + s.path);
|
||||
console.log(colors.bold("Summary:") + " " + (s.summary ?? ""));
|
||||
console.log(colors.bold("Description:") + " " + (s.description ?? ""));
|
||||
console.log(colors.bold("Language:") + " " + s.language);
|
||||
console.log(colors.bold("Kind:") + " " + (s.kind ?? "script"));
|
||||
console.log(colors.bold("Created by:") + " " + (s.created_by ?? ""));
|
||||
console.log(colors.bold("Created at:") + " " + (s.created_at ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrap(
|
||||
opts: GlobalOptions & { summary: string; description: string },
|
||||
scriptPath: string,
|
||||
@@ -941,10 +937,15 @@ async function bootstrap(
|
||||
|
||||
try {
|
||||
await stat(scriptCodeFileFullPath);
|
||||
throw new Error("File already exists: " + scriptCodeFileFullPath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
try {
|
||||
await stat(scriptMetadataFileFullPath);
|
||||
throw new Error("File already exists in repository");
|
||||
} catch {
|
||||
// file does not exist, we can continue
|
||||
throw new Error("File already exists: " + scriptMetadataFileFullPath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
|
||||
const scriptMetadata = defaultScriptMetadata();
|
||||
@@ -1155,38 +1156,34 @@ async function preview(
|
||||
|
||||
// Handle multiple output files (create tarball)
|
||||
if (out.outputFiles.length > 1) {
|
||||
const archiveNpm = await import("@ayonli/jsext/archive");
|
||||
if (!opts.silent) {
|
||||
log.info(`Creating tarball for multiple output files...`);
|
||||
}
|
||||
const tarball = new archiveNpm.Tarball();
|
||||
const mainPath = filePath.split(SEP).pop()?.split(".")[0] + ".js";
|
||||
const mainContent =
|
||||
out.outputFiles.find((file: OutputFile) => file.path == "/" + mainPath)?.text ?? "";
|
||||
tarball.append(new File([mainContent], "main.js", { type: "text/plain" }));
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: mainContent },
|
||||
];
|
||||
for (const file of out.outputFiles) {
|
||||
if (file.path == "/" + mainPath) continue;
|
||||
|
||||
const fil = new File([file.contents as any], file.path.substring(1));
|
||||
tarball.append(fil);
|
||||
entries.push({ name: file.path.substring(1), content: file.contents });
|
||||
}
|
||||
bundledContent = await streamToBlob(tarball.stream());
|
||||
bundledContent = await createTarBlob(entries);
|
||||
isTar = true;
|
||||
} else if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
||||
// Handle assets
|
||||
const archiveNpm = await import("@ayonli/jsext/archive");
|
||||
if (!opts.silent) {
|
||||
log.info(`Adding assets to tarball...`);
|
||||
}
|
||||
const tarball = new archiveNpm.Tarball();
|
||||
tarball.append(new File([bundledContent], "main.js", { type: "text/plain" }));
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: bundledContent },
|
||||
];
|
||||
for (const asset of codebase.assets) {
|
||||
const data = fs.readFileSync(asset.from);
|
||||
const blob = new Blob([data], { type: "text/plain" });
|
||||
const file = new File([blob], asset.to);
|
||||
tarball.append(file);
|
||||
entries.push({ name: asset.to, content: data });
|
||||
}
|
||||
bundledContent = await streamToBlob(tarball.stream());
|
||||
bundledContent = await createTarBlob(entries);
|
||||
isTar = true;
|
||||
}
|
||||
|
||||
@@ -1290,6 +1287,11 @@ async function preview(
|
||||
const command = new Command()
|
||||
.description("script related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all scripts")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command(
|
||||
"push",
|
||||
@@ -1297,7 +1299,11 @@ const command = new Command()
|
||||
)
|
||||
.arguments("<path:file>")
|
||||
.action(push as any)
|
||||
.command("show", "show a scripts content")
|
||||
.command("get", "get a script's details")
|
||||
.arguments("<path:file>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("show", "show a script's content (alias for get)")
|
||||
.arguments("<path:file>")
|
||||
.action(show as any)
|
||||
.command("run", "run a script by path")
|
||||
@@ -1325,7 +1331,12 @@ const command = new Command()
|
||||
"Do not output anything other than the final output. Useful for scripting."
|
||||
)
|
||||
.action(preview as any)
|
||||
.command("bootstrap", "create a new script")
|
||||
.command("new", "create a new script")
|
||||
.arguments("<path:file> <language:string>")
|
||||
.option("--summary <summary:string>", "script summary")
|
||||
.option("--description <description:string>", "script description")
|
||||
.action(bootstrap as any)
|
||||
.command("bootstrap", "create a new script (alias for new)")
|
||||
.arguments("<path:file> <language:string>")
|
||||
.option("--summary <summary:string>", "script summary")
|
||||
.option("--description <description:string>", "script description")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
|
||||
let GLOBAL_VERSIONS: {
|
||||
remoteMajor: number | undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import JSZip from "jszip";
|
||||
import { Workspace } from "../workspace/workspace.ts";
|
||||
import { getHeaders } from "../../utils/utils.ts";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
|
||||
function stub(_opts: GlobalOptions, _dir?: string) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { readFile, writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify, type DocumentOptions, type SchemaOptions, type CreateNodeOptions, type ToStringOptions } from "yaml";
|
||||
import JSZip from "jszip";
|
||||
import { minimatch } from "minimatch";
|
||||
import { yamlParseContent } from "../../utils/yaml.ts";
|
||||
@@ -276,13 +276,12 @@ function prioritizeName(name: string): string {
|
||||
return name;
|
||||
}
|
||||
|
||||
export const yamlOptions = {
|
||||
sortKeys: (a: any, b: any) => {
|
||||
return prioritizeName(a).localeCompare(prioritizeName(b));
|
||||
export const yamlOptions: DocumentOptions & SchemaOptions & CreateNodeOptions & ToStringOptions = {
|
||||
sortMapEntries: (a, b) => {
|
||||
return prioritizeName(String(a.key)).localeCompare(prioritizeName(String(b.key)));
|
||||
},
|
||||
noCompatMode: true,
|
||||
noRefs: true,
|
||||
skipInvalid: true,
|
||||
aliasDuplicateObjects: false,
|
||||
singleQuote: true,
|
||||
};
|
||||
|
||||
export interface InlineScript {
|
||||
@@ -1338,17 +1337,19 @@ async function compareDynFSElement(
|
||||
continue;
|
||||
}
|
||||
if (!ignoreCodebaseChanges) {
|
||||
const beforeCodebase = before?.codebase;
|
||||
const afterCodebase = after?.codebase;
|
||||
if (before?.codebase != undefined) {
|
||||
delete before.codebase;
|
||||
m2[k] = yamlStringify(before, yamlOptions);
|
||||
}
|
||||
if (after?.codebase != undefined) {
|
||||
if (before.codebase != after.codebase) {
|
||||
codebaseChanges[k] = after.codebase;
|
||||
}
|
||||
delete after.codebase;
|
||||
v = yamlStringify(after, yamlOptions);
|
||||
}
|
||||
if (beforeCodebase != afterCodebase) {
|
||||
codebaseChanges[k] = afterCodebase ?? beforeCodebase ?? "";
|
||||
}
|
||||
}
|
||||
if (skipMetadata) {
|
||||
continue;
|
||||
@@ -2214,7 +2215,7 @@ export async function push(
|
||||
`\nPush aborted: ${lockIssues.length} script(s) missing locks.`,
|
||||
),
|
||||
);
|
||||
Deno.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
log.info(colors.green("All scripts have valid locks."));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import {
|
||||
@@ -18,8 +19,8 @@ import {
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import {
|
||||
GlobalOptions,
|
||||
isSuperset,
|
||||
@@ -295,37 +296,192 @@ export async function pushNativeTrigger(
|
||||
}
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
const triggerTemplates: Record<TriggerType, Record<string, any>> = {
|
||||
http: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
route_path: "",
|
||||
http_method: "get",
|
||||
is_async: false,
|
||||
requires_auth: true,
|
||||
},
|
||||
websocket: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
url: "",
|
||||
enabled: false,
|
||||
},
|
||||
kafka: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
kafka_resource_path: "",
|
||||
group_id: "",
|
||||
topics: [],
|
||||
enabled: false,
|
||||
},
|
||||
nats: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
nats_resource_path: "",
|
||||
subjects: [],
|
||||
enabled: false,
|
||||
},
|
||||
postgres: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
postgres_resource_path: "",
|
||||
publication_name: "",
|
||||
replication_slot_name: "",
|
||||
enabled: false,
|
||||
},
|
||||
mqtt: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
mqtt_resource_path: "",
|
||||
topics: [],
|
||||
subscribe_qos: 0,
|
||||
enabled: false,
|
||||
},
|
||||
sqs: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
sqs_resource_path: "",
|
||||
queue_url: "",
|
||||
enabled: false,
|
||||
},
|
||||
gcp: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
gcp_resource_path: "",
|
||||
subscription_id: "",
|
||||
topic_id: "",
|
||||
enabled: false,
|
||||
},
|
||||
email: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
async function newTrigger(opts: GlobalOptions & { kind: string }, path: string) {
|
||||
if (!validatePath(path)) {
|
||||
return;
|
||||
}
|
||||
if (!opts.kind) {
|
||||
throw new Error("--kind is required. Valid kinds: " + TRIGGER_TYPES.join(", "));
|
||||
}
|
||||
if (!checkIfValidTrigger(opts.kind)) {
|
||||
throw new Error("Invalid trigger kind: " + opts.kind + ". Valid kinds: " + TRIGGER_TYPES.join(", "));
|
||||
}
|
||||
const kind: TriggerType = opts.kind;
|
||||
const filePath = `${path}.${kind}_trigger.yaml`;
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template = triggerTemplates[kind];
|
||||
await writeFile(filePath, yamlStringify(template), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const httpTriggers = await wmill.listHttpTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const websocketTriggers = await wmill.listWebsocketTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const kafkaTriggers = await wmill.listKafkaTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const natsTriggers = await wmill.listNatsTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const postgresTriggers = await wmill.listPostgresTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const mqttTriggers = await wmill.listMqttTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const sqsTriggers = await wmill.listSqsTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const gcpTriggers = await wmill.listGcpTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
const emailTriggers = await wmill.listEmailTriggers({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
if (opts.kind) {
|
||||
if (!checkIfValidTrigger(opts.kind)) {
|
||||
throw new Error("Invalid trigger kind: " + opts.kind + ". Valid kinds: " + TRIGGER_TYPES.join(", "));
|
||||
}
|
||||
const trigger = await getTrigger(opts.kind, workspace.workspaceId, path);
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(trigger));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + (trigger as any).path);
|
||||
console.log(colors.bold("Kind:") + " " + opts.kind);
|
||||
console.log(colors.bold("Enabled:") + " " + ((trigger as any).enabled ?? "-"));
|
||||
console.log(colors.bold("Script Path:") + " " + ((trigger as any).script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + ((trigger as any).is_flow ? "true" : "false"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Try all trigger types and collect matches
|
||||
const matches: { kind: string; trigger: any }[] = [];
|
||||
for (const kind of TRIGGER_TYPES) {
|
||||
try {
|
||||
const trigger = await getTrigger(kind, workspace.workspaceId, path);
|
||||
matches.push({ kind, trigger });
|
||||
} catch {
|
||||
// not found for this kind
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
throw new Error("No trigger found at path: " + path);
|
||||
}
|
||||
|
||||
if (matches.length === 1) {
|
||||
const { kind, trigger } = matches[0];
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(trigger));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + trigger.path);
|
||||
console.log(colors.bold("Kind:") + " " + kind);
|
||||
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? "-"));
|
||||
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
|
||||
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple matches — ask user to specify --kind
|
||||
console.log("Multiple triggers found at path " + path + ":");
|
||||
for (const m of matches) {
|
||||
console.log(" - " + m.kind);
|
||||
}
|
||||
console.log("Please specify --kind <type> to select one.");
|
||||
}
|
||||
|
||||
async function listOrEmpty<T>(fn: () => Promise<T[]>): Promise<T[]> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const ws = workspace.workspaceId;
|
||||
const [
|
||||
httpTriggers,
|
||||
websocketTriggers,
|
||||
kafkaTriggers,
|
||||
natsTriggers,
|
||||
postgresTriggers,
|
||||
mqttTriggers,
|
||||
sqsTriggers,
|
||||
gcpTriggers,
|
||||
emailTriggers,
|
||||
] = await Promise.all([
|
||||
listOrEmpty(() => wmill.listHttpTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listWebsocketTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listKafkaTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listNatsTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listPostgresTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listMqttTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listSqsTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listGcpTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listEmailTriggers({ workspace: ws })),
|
||||
]);
|
||||
const triggers = [
|
||||
...httpTriggers.map((x) => ({ path: x.path, kind: "http" })),
|
||||
...websocketTriggers.map((x) => ({ path: x.path, kind: "websocket" })),
|
||||
@@ -338,12 +494,16 @@ async function list(opts: GlobalOptions) {
|
||||
...emailTriggers.map((x) => ({ path: x.path, kind: "email" })),
|
||||
];
|
||||
|
||||
new Table()
|
||||
.header(["Path", "Kind"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(triggers.map((x) => [x.path, x.kind]))
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(triggers));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Path", "Kind"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(triggers.map((x) => [x.path, x.kind]))
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfValidTrigger(kind: string | undefined): kind is TriggerType {
|
||||
@@ -401,7 +561,20 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
|
||||
const command = new Command()
|
||||
.description("trigger related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all triggers")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a trigger's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--kind <kind:string>", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup")
|
||||
.action(get as any)
|
||||
.command("new", "create a new trigger locally")
|
||||
.arguments("<path:string>")
|
||||
.option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)")
|
||||
.action(newTrigger as any)
|
||||
.command(
|
||||
"push",
|
||||
"push a local trigger spec. This overrides any remote versions."
|
||||
|
||||
@@ -11,8 +11,8 @@ import { compareInstanceObjects, InstanceSyncOptions } from "../instance/instanc
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
@@ -12,13 +13,13 @@ import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { ListableVariable } from "../../../gen/types.gen.ts";
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
@@ -26,19 +27,64 @@ async function list(opts: GlobalOptions) {
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
new Table()
|
||||
.header(["Path", "Is Secret", "Account", "Value"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
variables.map((x) => [
|
||||
x.path,
|
||||
x.is_secret ? "true" : "false",
|
||||
x.account ?? "-",
|
||||
x.value ?? "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(variables));
|
||||
} else {
|
||||
new Table()
|
||||
.header(["Path", "Is Secret", "Account", "Value"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
variables.map((x) => [
|
||||
x.path,
|
||||
x.is_secret ? "true" : "false",
|
||||
x.account ?? "-",
|
||||
x.value ?? "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function newVariable(opts: GlobalOptions, path: string) {
|
||||
if (!validatePath(path)) {
|
||||
return;
|
||||
}
|
||||
const filePath = path + ".variable.yaml";
|
||||
try {
|
||||
await stat(filePath);
|
||||
throw new Error("File already exists: " + filePath);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template: VariableFile = {
|
||||
value: "",
|
||||
is_secret: false,
|
||||
description: "",
|
||||
};
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
log.info(colors.green(`Created ${filePath}`));
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const v = await wmill.getVariable({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(v));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + v.path);
|
||||
console.log(colors.bold("Value:") + " " + (v.value ?? "-"));
|
||||
console.log(colors.bold("Is Secret:") + " " + (v.is_secret ? "true" : "false"));
|
||||
console.log(colors.bold("Description:") + " " + (v.description ?? ""));
|
||||
console.log(colors.bold("Account:") + " " + (v.account ?? "-"));
|
||||
}
|
||||
}
|
||||
|
||||
export interface VariableFile {
|
||||
@@ -178,7 +224,18 @@ async function add(
|
||||
|
||||
const command = new Command()
|
||||
.description("variable related commands")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "list all variables")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "get a variable's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("new", "create a new variable locally")
|
||||
.arguments("<path:string>")
|
||||
.action(newVariable as any)
|
||||
.command(
|
||||
"push",
|
||||
"Push a local variable spec. This overrides any remote versions."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { setClient } from "../../core/client.ts";
|
||||
import { allInstances, getActiveInstance, InstanceSyncOptions, pickInstance } from "../instance/instance.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { pickInstance } from "../instance/instance.ts";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { setClient } from "../../core/client.ts";
|
||||
import { allWorkspaces, list, removeWorkspace } from "./workspace.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { setClient } from "../../core/client.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { createWorkspaceFork, deleteWorkspaceFork } from "./fork.ts";
|
||||
@@ -518,7 +518,7 @@ async function bind(
|
||||
}
|
||||
|
||||
// Write back the updated config
|
||||
const { stringify: yamlStringify } = await import("@std/yaml");
|
||||
const { stringify: yamlStringify } = await import("yaml");
|
||||
try {
|
||||
await writeFile("wmill.yaml", yamlStringify(config), "utf-8");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import { setClient } from "./client.ts";
|
||||
import * as wmill from "../../gen/services.gen.ts";
|
||||
import { GlobalUserInfo } from "../../gen/types.gen.ts";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { getStore } from "./store.ts";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import { yamlParseFile } from "../utils/yaml.ts";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import {
|
||||
getCurrentGitBranch,
|
||||
getOriginalBranchForWorkspaceForks,
|
||||
@@ -196,7 +196,7 @@ export async function readConfigFile(): Promise<SyncOptions> {
|
||||
|
||||
if (!wmillYamlPath) {
|
||||
log.warn(
|
||||
"No wmill.yaml found. Use 'wmill init' to bootstrap it. Using 'bun' as default typescript runtime."
|
||||
"No wmill.yaml found. Use 'wmill init' to bootstrap it."
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
@@ -459,11 +459,12 @@ export async function resolveWorkspace(
|
||||
// forked workspace, that we detect through the branch name (only when not using branchOverride)
|
||||
const res = await tryResolveWorkspace(opts);
|
||||
if (!res.isError) {
|
||||
const workspace = (res as { isError: false; value: Workspace }).value;
|
||||
if (branchOverride || !branch || !branch.startsWith(WM_FORK_PREFIX)) {
|
||||
return res.value;
|
||||
return workspace;
|
||||
} else {
|
||||
log.info(
|
||||
`Found an active workspace \`${res.value.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\``
|
||||
`Found an active workspace \`${workspace.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\``
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -486,13 +487,41 @@ export async function resolveWorkspace(
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to active workspace (lowest priority)
|
||||
// Fall back to active workspace
|
||||
const activeWorkspace = await getActiveWorkspace(opts);
|
||||
if (activeWorkspace) {
|
||||
(opts as any).__secret_workspace = activeWorkspace;
|
||||
return activeWorkspace;
|
||||
}
|
||||
|
||||
// Last resort: auto-configure from Windmill environment variables
|
||||
// (set by the worker for bash/script execution)
|
||||
const envWorkspace = process.env["WM_WORKSPACE"];
|
||||
const envToken = process.env["WM_TOKEN"];
|
||||
const envBaseUrl =
|
||||
process.env["BASE_INTERNAL_URL"] ?? process.env["BASE_URL"];
|
||||
|
||||
if (envWorkspace && envToken && envBaseUrl) {
|
||||
let normalizedBaseUrl: string;
|
||||
try {
|
||||
normalizedBaseUrl = new URL(envBaseUrl).toString();
|
||||
} catch {
|
||||
log.info(colors.red(`Invalid BASE_INTERNAL_URL: ${envBaseUrl}`));
|
||||
return process.exit(-1);
|
||||
}
|
||||
log.debug(
|
||||
`Using workspace from environment variables: ${envWorkspace} on ${normalizedBaseUrl}`
|
||||
);
|
||||
const ws: Workspace = {
|
||||
name: envWorkspace,
|
||||
workspaceId: envWorkspace,
|
||||
remote: normalizedBaseUrl,
|
||||
token: envToken,
|
||||
};
|
||||
(opts as any).__secret_workspace = ws;
|
||||
return ws;
|
||||
}
|
||||
|
||||
// If everything failed, show error
|
||||
log.info(colors.red.bold("No workspace given and no default set."));
|
||||
return process.exit(-1);
|
||||
@@ -532,7 +561,8 @@ export async function tryResolveVersion(
|
||||
|
||||
const workspaceRes = await tryResolveWorkspace(opts);
|
||||
if (workspaceRes.isError) return undefined;
|
||||
const version = await fetchVersion(workspaceRes.value.remote);
|
||||
const workspace = (workspaceRes as { isError: false; value: Workspace }).value;
|
||||
const version = await fetchVersion(workspace.remote);
|
||||
|
||||
try {
|
||||
return Number.parseInt(
|
||||
|
||||
24
cli/src/core/log.ts
Normal file
24
cli/src/core/log.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
let logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" = "INFO";
|
||||
|
||||
const levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||
|
||||
export function setup(level: "DEBUG" | "INFO" | "WARN" | "ERROR") {
|
||||
logLevel = level;
|
||||
}
|
||||
|
||||
export function debug(msg: unknown) {
|
||||
if (levels[logLevel] <= levels.DEBUG)
|
||||
console.log(`\x1b[90m${String(msg)}\x1b[39m`);
|
||||
}
|
||||
|
||||
export function info(msg: unknown) {
|
||||
console.log(`\x1b[34m${String(msg)}\x1b[39m`);
|
||||
}
|
||||
|
||||
export function warn(msg: unknown) {
|
||||
console.log(`\x1b[33m${String(msg)}\x1b[39m`);
|
||||
}
|
||||
|
||||
export function error(msg: unknown) {
|
||||
console.log(`\x1b[31m${String(msg)}\x1b[39m`);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GlobalOptions } from "../types.ts";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as getPort from "get-port";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import * as open from "open";
|
||||
import { Secret } from "@cliffy/prompt/secret";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
|
||||
@@ -2,9 +2,9 @@ import process from "node:process";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./log.ts";
|
||||
import { yamlParseFile } from "../utils/yaml.ts";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import * as wmill from "../../gen/services.gen.ts";
|
||||
import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
|
||||
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";
|
||||
|
||||
@@ -4557,9 +4557,16 @@ Current version: 1.624.0
|
||||
|
||||
app related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`app push <file_path:string> <remote_path:string>\` - push a local app
|
||||
- \`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 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
|
||||
@@ -4596,10 +4603,16 @@ Launch a dev server that will spawn a webserver with HMR
|
||||
flow related commands
|
||||
|
||||
**Options:**
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--show-archived\` - Enable archived flows in output
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**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 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 @-.
|
||||
@@ -4611,16 +4624,27 @@ flow related commands
|
||||
- \`--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)
|
||||
- \`-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
|
||||
- \`--summary <summary:string>\` - script summary
|
||||
- \`--description <description:string>\` - script description
|
||||
- \`flow new <flow_path:string>\` - create a new empty flow
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--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 related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`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
|
||||
- \`folder push <file_path:string> <remote_path:string>\` - push a local folder spec. This overrides any remote versions.
|
||||
|
||||
### gitsync-settings
|
||||
@@ -4731,18 +4755,33 @@ List all queues with their metrics
|
||||
|
||||
resource related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**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-type
|
||||
|
||||
resource type related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`resource-type list\` - list all resource types
|
||||
- \`--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 generate-namespace\` - Create a TypeScript definition file with the RT namespace generated from the resource types
|
||||
|
||||
@@ -4750,8 +4789,16 @@ resource type related commands
|
||||
|
||||
schedule related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**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.
|
||||
|
||||
### script
|
||||
@@ -4760,21 +4807,30 @@ script related commands
|
||||
|
||||
**Options:**
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`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 list\` - list all scripts
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`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 push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)
|
||||
- \`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 @-.
|
||||
- \`-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.
|
||||
- \`-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.
|
||||
- \`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 generate-metadata [script:file]\` - re-generate the metadata file updating the lock and the script schema (for flows, use \`wmill flow generate-locks\`
|
||||
- \`script bootstrap <path:file> <language:string>\` - create a new script (alias for new)
|
||||
- \`--summary <summary:string>\` - script summary
|
||||
- \`--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\`)
|
||||
- \`--yes\` - Skip confirmation prompt
|
||||
- \`--dry-run\` - Perform a dry run without making changes
|
||||
- \`--lock-only\` - re-generate only the lock
|
||||
@@ -4852,8 +4908,18 @@ sync local with a remote workspaces or the opposite (push or pull)
|
||||
|
||||
trigger related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**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)
|
||||
- \`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.
|
||||
|
||||
### user
|
||||
@@ -4875,8 +4941,16 @@ user related commands
|
||||
|
||||
variable related commands
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**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.
|
||||
- \`--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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Command } from "@cliffy/command";
|
||||
import { CompletionsCommand } from "@cliffy/command/completions";
|
||||
import { UpgradeCommand } from "@cliffy/command/upgrade";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "./core/log.ts";
|
||||
|
||||
import { realpathSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -28,7 +28,7 @@ import lint from "./commands/lint/lint.ts";
|
||||
import dev from "./commands/dev/dev.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { OpenAPI } from "../gen/index.ts";
|
||||
import { getHeaders, getIsWin } from "./utils/utils.ts";
|
||||
import { getHeaders } from "./utils/utils.ts";
|
||||
import { setShowDiffs } from "./core/conf.ts";
|
||||
import { NpmProvider } from "./utils/upgrade.ts";
|
||||
import { pull as hubPull } from "./commands/hub/hub.ts";
|
||||
@@ -187,21 +187,7 @@ async function main() {
|
||||
// const NO_COLORS = args.includes("--no-colors");
|
||||
setShowDiffs(args.includes("--show-diffs"));
|
||||
|
||||
const isWin = await getIsWin();
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => msg,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.setup(LOG_LEVEL);
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
@@ -235,7 +221,10 @@ function isMain() {
|
||||
}
|
||||
}
|
||||
if (isMain()) {
|
||||
main();
|
||||
main().then(() => {
|
||||
// Destroy stdin so interactive prompts (Cliffy) don't keep the event loop alive
|
||||
process.stdin.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as Diff from "diff";
|
||||
import * as log from "@std/log";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "./core/log.ts";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseContent } from "./utils/yaml.ts";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { pushApp } from "./commands/app/app.ts";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Codebase, SyncOptions } from "../core/conf.ts";
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../core/log.ts";
|
||||
import { digestDir } from "./utils.ts";
|
||||
|
||||
export type SyncCodebase = Codebase & {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as log from "@std/log";
|
||||
import * as log from "../core/log.ts";
|
||||
import { execSync } from "node:child_process";
|
||||
import { WM_FORK_PREFIX } from "../core/constants.ts";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { GlobalOptions } from "../types.ts";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "@std/log";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import * as log from "../core/log.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "./yaml.ts";
|
||||
import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app).
|
||||
*/
|
||||
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { yamlParseFile } from "./yaml.ts";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
22
cli/src/utils/tar.ts
Normal file
22
cli/src/utils/tar.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { pack } from "tar-stream";
|
||||
|
||||
export interface TarEntry {
|
||||
name: string;
|
||||
content: Buffer | Uint8Array | string;
|
||||
}
|
||||
|
||||
export function createTarBlob(entries: TarEntry[]): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = pack();
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
p.on("data", (chunk: Buffer) => chunks.push(new Uint8Array(chunk)));
|
||||
p.on("end", () => resolve(new Blob(chunks as BlobPart[])));
|
||||
p.on("error", reject);
|
||||
|
||||
for (const entry of entries) {
|
||||
p.entry({ name: entry.name }, Buffer.from(entry.content));
|
||||
}
|
||||
p.finalize();
|
||||
});
|
||||
}
|
||||
@@ -53,6 +53,10 @@ export class NpmProvider extends Provider {
|
||||
getRegistryUrl(name: string, version: string): string {
|
||||
return `npm:${this.packageName ?? name}@${version}`;
|
||||
}
|
||||
|
||||
async hasRequiredPermissions(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
type NpmApiPackageMetadata = {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
// @ts-nocheck This file is copied from a JS project, so it's not type-safe.
|
||||
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { encodeHex } from "@std/encoding";
|
||||
import * as log from "@std/log";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as log from "../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
@@ -128,7 +127,7 @@ export async function generateHashFromBuffer(
|
||||
content: BufferSource
|
||||
): Promise<string> {
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
|
||||
return encodeHex(hashBuffer);
|
||||
return Buffer.from(hashBuffer).toString("hex");
|
||||
}
|
||||
|
||||
export function readInlinePathSync(path: string): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parse as yamlParse, type ParseOptions } from "@std/yaml";
|
||||
import { parse as yamlParse, type ParseOptions } from "yaml";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
export async function yamlParseFile(path: string, options: ParseOptions = {}) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
formatValidationError,
|
||||
runLint,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { checkMissingLocks, runLint } from "../src/commands/lint/lint.ts";
|
||||
|
||||
async function withTempDir(
|
||||
|
||||
639
cli/test/list_get_new_commands.test.ts
Normal file
639
cli/test/list_get_new_commands.test.ts
Normal file
@@ -0,0 +1,639 @@
|
||||
/**
|
||||
* Integration tests for the new list/get/new CLI commands.
|
||||
*
|
||||
* Tests:
|
||||
* - `list --json` for all item types
|
||||
* - `get <path>` and `get <path> --json` for all item types
|
||||
* - `new` (bootstrap) for script, flow, resource, resource-type, variable, schedule, folder, trigger
|
||||
* - `bootstrap` alias for script and flow
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { writeFile, mkdir, stat, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { withTestBackend, type TestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
async function setupWorkspaceProfile(backend: TestBackend): Promise<void> {
|
||||
await addWorkspace(
|
||||
{
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "localhost_test",
|
||||
token: backend.token!,
|
||||
},
|
||||
{ force: true, configDir: backend.testConfigDir }
|
||||
);
|
||||
}
|
||||
|
||||
async function createRemoteScript(
|
||||
backend: TestBackend,
|
||||
scriptPath: string,
|
||||
content: string = 'export async function main() { return "hello"; }'
|
||||
): Promise<void> {
|
||||
const resp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content,
|
||||
language: "bun",
|
||||
summary: "Test script summary",
|
||||
description: "Test script description",
|
||||
schema: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
expect(resp.status).toBeLessThan(300);
|
||||
await resp.text();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// list --json
|
||||
// =============================================================================
|
||||
|
||||
describe("list --json flag", () => {
|
||||
test("script list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/list_json_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.some((s: any) => s.path === scriptPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
// seedTestData creates f/test/my_resource
|
||||
expect(parsed.some((r: any) => r.path === "f/test/my_resource")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("variable list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("folder list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.some((f: any) => f.name === "test")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("schedule list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["schedule", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource-type list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource-type", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("app list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["app", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("default action with --json works (e.g. wmill script --json)", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// get <path> and get <path> --json
|
||||
// =============================================================================
|
||||
|
||||
describe("get command", () => {
|
||||
test("script get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/get_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "get", scriptPath],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Path:");
|
||||
expect(output).toContain(scriptPath);
|
||||
expect(output).toContain("Summary:");
|
||||
expect(output).toContain("Language:");
|
||||
expect(output).toContain("bun");
|
||||
});
|
||||
});
|
||||
|
||||
test("script get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/get_json_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "get", scriptPath, "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe(scriptPath);
|
||||
expect(parsed.language).toBe("bun");
|
||||
expect(parsed.summary).toBe("Test script summary");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "get", "f/test/my_resource", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe("f/test/my_resource");
|
||||
expect(parsed.resource_type).toBe("any");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "get", "f/test/my_resource"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Path:");
|
||||
expect(output).toContain("f/test/my_resource");
|
||||
expect(output).toContain("Resource Type:");
|
||||
});
|
||||
});
|
||||
|
||||
test("variable get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "get", "f/test/my_variable", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe("f/test/my_variable");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "get", "test", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.name).toBe("test");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "get", "test"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Name:");
|
||||
expect(output).toContain("test");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// new command
|
||||
// =============================================================================
|
||||
|
||||
describe("new command", () => {
|
||||
test("script new creates files (same as bootstrap)", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await writeFile(
|
||||
join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
|
||||
"utf-8"
|
||||
);
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "new", "f/test/new_cmd_script", "bun", "--summary", "Test new"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const codeStat = await stat(join(tempDir, "f/test/new_cmd_script.ts"));
|
||||
expect(codeStat.isFile()).toBe(true);
|
||||
|
||||
const metaStat = await stat(
|
||||
join(tempDir, "f/test/new_cmd_script.script.yaml")
|
||||
);
|
||||
expect(metaStat.isFile()).toBe(true);
|
||||
|
||||
const metaContent = await readFile(
|
||||
join(tempDir, "f/test/new_cmd_script.script.yaml"),
|
||||
"utf-8"
|
||||
);
|
||||
expect(metaContent).toContain("Test new");
|
||||
});
|
||||
});
|
||||
|
||||
test("script bootstrap still works as alias", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await writeFile(
|
||||
join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
|
||||
"utf-8"
|
||||
);
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "bootstrap", "f/test/alias_script", "bun"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const codeStat = await stat(join(tempDir, "f/test/alias_script.ts"));
|
||||
expect(codeStat.isFile()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow new creates flow directory and flow.yaml", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "new", "f/test/new_flow", "--summary", "My flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const flowYamlStat = await stat(
|
||||
join(tempDir, "f/test/new_flow.flow/flow.yaml")
|
||||
);
|
||||
expect(flowYamlStat.isFile()).toBe(true);
|
||||
|
||||
const flowContent = await readFile(
|
||||
join(tempDir, "f/test/new_flow.flow/flow.yaml"),
|
||||
"utf-8"
|
||||
);
|
||||
expect(flowContent).toContain("My flow");
|
||||
});
|
||||
});
|
||||
|
||||
test("flow bootstrap still works as alias", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "bootstrap", "f/test/alias_flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const flowYamlStat = await stat(
|
||||
join(tempDir, "f/test/alias_flow.flow/flow.yaml")
|
||||
);
|
||||
expect(flowYamlStat.isFile()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource new creates resource yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "new", "f/test/new_resource"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_resource.resource.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("resource_type");
|
||||
expect(content).toContain("value");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource-type new creates resource-type yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource-type", "new", "my_custom_type"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "my_custom_type.resource-type.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("schema");
|
||||
expect(content).toContain("description");
|
||||
});
|
||||
});
|
||||
|
||||
test("variable new creates variable yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "new", "f/test/new_var"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_var.variable.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("is_secret");
|
||||
expect(content).toContain("value");
|
||||
});
|
||||
});
|
||||
|
||||
test("schedule new creates schedule yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["schedule", "new", "f/test/new_sched"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_sched.schedule.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("schedule");
|
||||
expect(content).toContain("script_path");
|
||||
expect(content).toContain("timezone");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder new creates folder.meta.yaml in f/<name>/", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "new", "new_folder"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/new_folder/folder.meta.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("owners");
|
||||
expect(content).toContain("extra_perms");
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new --kind http creates http trigger yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/new_trigger", "--kind", "http"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(
|
||||
tempDir,
|
||||
"f/test/new_trigger.http_trigger.yaml"
|
||||
);
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("script_path");
|
||||
expect(content).toContain("route_path");
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new without --kind fails with error", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/fail_trigger"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).not.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new --kind kafka creates kafka trigger yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/kafka_trigger", "--kind", "kafka"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(
|
||||
tempDir,
|
||||
"f/test/kafka_trigger.kafka_trigger.yaml"
|
||||
);
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("kafka_resource_path");
|
||||
expect(content).toContain("topics");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import { encodeHex } from "@std/encoding";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mirrors extractWorkspaceDepsAnnotation + computeLockCacheKey from
|
||||
@@ -110,7 +110,7 @@ async function computeLockCacheKey(
|
||||
.join(";");
|
||||
const content = `${language}|${annotationStr}|${depsStr}`;
|
||||
const buf = new TextEncoder().encode(content);
|
||||
return encodeHex(await crypto.subtle.digest("SHA-256", buf));
|
||||
return Buffer.from(await crypto.subtle.digest("SHA-256", buf)).toString("hex");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, stat } from "node:fs/promises";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, stat, rm, mkdir } from "node:fs/promises";
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { writeFile, readFile, readdir, rm, mkdir, mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
140
cli/test/tar_creation.test.ts
Normal file
140
cli/test/tar_creation.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Unit tests for the tar creation utility.
|
||||
* These tests require no backend — they test standalone tar logic.
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { createTarBlob, type TarEntry } from "../src/utils/tar.ts";
|
||||
import { extract, type Headers } from "tar-stream";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
/** Extract all entries from a tarball Blob into a map of name -> content string */
|
||||
async function extractTar(
|
||||
blob: Blob
|
||||
): Promise<Map<string, { content: string; header: Headers }>> {
|
||||
const result = new Map<string, { content: string; header: Headers }>();
|
||||
const ex = extract();
|
||||
const buffer = Buffer.from(await blob.arrayBuffer());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ex.on("entry", (header, stream, next) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("end", () => {
|
||||
result.set(header.name, {
|
||||
content: Buffer.concat(chunks).toString("utf-8"),
|
||||
header,
|
||||
});
|
||||
next();
|
||||
});
|
||||
stream.on("error", reject);
|
||||
stream.resume();
|
||||
});
|
||||
ex.on("finish", () => resolve(result));
|
||||
ex.on("error", reject);
|
||||
|
||||
Readable.from(buffer).pipe(ex);
|
||||
});
|
||||
}
|
||||
|
||||
describe("createTarBlob", () => {
|
||||
test("single file tarball", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: 'console.log("hello");' },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(1);
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.get("main.js")!.content).toBe('console.log("hello");');
|
||||
});
|
||||
|
||||
test("multiple output files", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: 'import "./chunk-abc.js";' },
|
||||
{ name: "chunk-abc.js", content: "export const x = 42;" },
|
||||
{ name: "chunk-def.js", content: "export const y = 99;" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(3);
|
||||
expect(extracted.get("main.js")!.content).toBe(
|
||||
'import "./chunk-abc.js";'
|
||||
);
|
||||
expect(extracted.get("chunk-abc.js")!.content).toBe(
|
||||
"export const x = 42;"
|
||||
);
|
||||
expect(extracted.get("chunk-def.js")!.content).toBe(
|
||||
"export const y = 99;"
|
||||
);
|
||||
});
|
||||
|
||||
test("single file with assets", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "const data = require('./data.json');" },
|
||||
{ name: "data.json", content: '{"key":"value"}' },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(2);
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.has("data.json")).toBe(true);
|
||||
expect(extracted.get("data.json")!.content).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
test("produces a valid Blob", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "module.exports = {};" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
|
||||
expect(blob).toBeInstanceOf(Blob);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
// Tar blocks are 512-byte aligned
|
||||
expect(blob.size % 512).toBe(0);
|
||||
});
|
||||
|
||||
test("file naming — entries have exact names given", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "entry point" },
|
||||
{ name: "lib/utils.js", content: "utils" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
// Names should be exactly as provided (no leading slash)
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.has("lib/utils.js")).toBe(true);
|
||||
});
|
||||
|
||||
test("handles Buffer content", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: Buffer.from("buffer content") },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.get("main.js")!.content).toBe("buffer content");
|
||||
});
|
||||
|
||||
test("handles Uint8Array content", async () => {
|
||||
const content = new TextEncoder().encode("uint8 content");
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.get("main.js")!.content).toBe("uint8 content");
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
clearGlobalLock,
|
||||
} from "../src/utils/metadata.ts";
|
||||
import { generateHash } from "../src/utils/utils.ts";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../src/utils/yaml.ts";
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ import { expect, test } from "bun:test";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { writeFile, mkdir } from "node:fs/promises";
|
||||
import { stringify as stringifyYaml } from "@std/yaml";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
|
||||
// Import hash generation utilities from CLI
|
||||
import { generateHash } from "../src/utils/utils.ts";
|
||||
|
||||
@@ -228,7 +228,7 @@ export function argSigToJsonSchemaType(
|
||||
if (oldS.items && typeof oldS.items === "object") {
|
||||
ITEMS_PRESERVED_FIELDS.forEach((field) => {
|
||||
if (oldS.items && oldS.items[field] !== undefined) {
|
||||
newS.items![field] = oldS.items[field];
|
||||
(newS.items as any)[field] = oldS.items[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -241,7 +241,7 @@ export function argSigToJsonSchemaType(
|
||||
if (oldS.items && typeof oldS.items === "object") {
|
||||
ITEMS_PRESERVED_FIELDS.forEach((field) => {
|
||||
if (oldS.items && oldS.items[field] !== undefined) {
|
||||
newS.items![field] = oldS.items[field];
|
||||
(newS.items as any)[field] = oldS.items[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
FROM node:slim
|
||||
FROM oven/bun:slim
|
||||
|
||||
RUN npm install -g windmill-cli
|
||||
RUN bun install -g windmill-cli
|
||||
|
||||
ENTRYPOINT [ "wmill" ]
|
||||
RUN ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
ENTRYPOINT [ "wmill" ]
|
||||
|
||||
@@ -56,6 +56,11 @@ RUN mkdir -p /tmp/windmill/cache && \
|
||||
|
||||
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# Install windmill CLI (node symlink needed for bun install)
|
||||
RUN ln -s /usr/bin/bun /usr/bin/node \
|
||||
&& bun install -g windmill-cli \
|
||||
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ RUN mkdir -p /tmp/windmill/cache && \
|
||||
|
||||
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# Install windmill CLI (node symlink needed for bun install)
|
||||
RUN ln -s /usr/bin/bun /usr/bin/node \
|
||||
&& bun install -g windmill-cli \
|
||||
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
|
||||
46
frontend/package-lock.json
generated
46
frontend/package-lock.json
generated
@@ -835,7 +835,6 @@
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -847,7 +846,6 @@
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -858,7 +856,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1348,7 +1345,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz",
|
||||
"integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1503,7 +1499,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1520,7 +1515,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1537,7 +1531,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1554,7 +1547,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1571,7 +1563,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1588,7 +1579,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1605,7 +1595,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1622,7 +1611,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1639,7 +1627,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1656,7 +1643,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1673,7 +1659,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1690,7 +1675,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1707,7 +1691,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2313,7 +2296,6 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -7193,7 +7175,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7692,7 +7674,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7713,7 +7694,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7734,7 +7714,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7755,7 +7734,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7776,7 +7754,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7797,7 +7774,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7818,7 +7794,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7839,7 +7814,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7860,7 +7834,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7881,7 +7854,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7902,7 +7874,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12529,21 +12500,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
|
||||
@@ -543,8 +543,7 @@
|
||||
}
|
||||
editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
editor.insertAtCursor(`curl -s -H "Authorization: Bearer $WM_TOKEN" \\
|
||||
"$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}" | jq -r .`)
|
||||
editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`)
|
||||
} else if (lang == 'powershell') {
|
||||
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
|
||||
editor.arrowDown()
|
||||
@@ -620,8 +619,7 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
|
||||
}
|
||||
editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
editor.insertAtCursor(`curl -s -H "Authorization: Bearer $WM_TOKEN" \\
|
||||
"$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}" | jq`)
|
||||
editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`)
|
||||
} else if (lang == 'powershell') {
|
||||
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
|
||||
editor.arrowDown()
|
||||
|
||||
Reference in New Issue
Block a user