Files
windmill/cli/src/commands/gitsync-settings/utils.ts
T
Ruben Fiszel 4fedfdfd11 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>
2026-02-22 07:53:28 +00:00

190 lines
5.6 KiB
TypeScript

import { colors } from "@cliffy/ansi/colors";
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";
import { GitSyncSettingsConverter } from "./converter.ts";
// Helper for consistent output handling between JSON and regular modes
export function outputResult(opts: { jsonOutput?: boolean }, result: {
success: boolean;
message?: string;
error?: string;
[key: string]: any;
}): void {
if (opts.jsonOutput) {
console.log(JSON.stringify(result));
} else if (result.success && result.message) {
log.info(colors.green(result.message));
} else if (!result.success && result.error) {
log.error(colors.red(result.error));
}
}
// Helper to normalize repository path by removing $res: prefix
export function normalizeRepoPath(path: string): string {
return path.replace(/^\$res:/, "");
}
// Helper to get or create branch configuration
export function getOrCreateBranchConfig(config: SyncOptions, branchName: string): {
config: SyncOptions;
branchKey: string;
} {
if (!config.gitBranches) {
config.gitBranches = {};
}
if (!config.gitBranches[branchName]) {
config.gitBranches[branchName] = {};
}
return {
config,
branchKey: branchName
};
}
// Helper to apply backend settings to branch configuration
export function applyBackendSettingsToBranch(
config: SyncOptions,
branchName: string,
backendSettings: SyncOptions
): SyncOptions {
const { config: updatedConfig } = getOrCreateBranchConfig(config, branchName);
// Get the base settings (top-level + defaults) to compare against
const { gitBranches, ...topLevelSettings } = config;
const baseSettings: Partial<SyncOptions> = { ...DEFAULT_SYNC_OPTIONS, ...topLevelSettings };
// Only store fields that differ from the base settings
Object.keys(backendSettings).forEach(key => {
if (key !== 'gitBranches' && backendSettings[key as keyof SyncOptions] !== undefined) {
const backendValue = backendSettings[key as keyof SyncOptions];
const baseValue = baseSettings[key as keyof SyncOptions];
// Only store if different from base
const isDifferent = GitSyncSettingsConverter.isDifferent(backendValue, baseValue);
if (isDifferent) {
if (!updatedConfig.gitBranches![branchName].overrides) {
updatedConfig.gitBranches![branchName].overrides = {};
}
(updatedConfig.gitBranches![branchName].overrides as any)[key] = backendValue;
}
}
});
return updatedConfig;
}
// Select repository interactively if multiple exist
export async function selectAndLogRepository(
repositories: GitSyncRepository[],
repository?: string,
suppressLogs?: boolean,
): Promise<GitSyncRepository> {
let selectedRepo: GitSyncRepository;
if (repository) {
const found = repositories.find(
(r: GitSyncRepository) =>
r.git_repo_resource_path === repository ||
r.git_repo_resource_path === `$res:${repository}`,
);
if (!found) {
throw new Error(`Repository ${repository} not found`);
}
selectedRepo = found;
const repoPath = selectedRepo.git_repo_resource_path.replace(/^\$res:/, "");
if (!suppressLogs) {
log.info(colors.cyan(`Using repository: ${colors.bold(repoPath)}`));
}
} else {
selectedRepo = await selectRepository(repositories);
}
return selectedRepo;
}
// Generate structured diff showing field changes
export function generateStructuredDiff(
current: any,
backend: any,
): { [key: string]: { from: any; to: any } } {
const diff: { [key: string]: { from: any; to: any } } = {};
// Get all unique keys from both objects
const allKeys = new Set([...Object.keys(current), ...Object.keys(backend)]);
for (const key of allKeys) {
const currentValue = current[key];
const backendValue = backend[key];
if (!deepEqual(currentValue, backendValue)) {
diff[key] = {
from: currentValue,
to: backendValue,
};
}
}
return diff;
}
// Helper to generate changes between two SyncOptions objects (normalizes automatically)
export function generateChanges(
current: SyncOptions,
new_: SyncOptions,
): { [key: string]: { from: any; to: any } } {
const changes: { [key: string]: { from: any; to: any } } = {};
// Normalize both inputs for consistent comparison
const normalizedCurrent = GitSyncSettingsConverter.normalize(current);
const normalizedNew = GitSyncSettingsConverter.normalize(new_);
for (const field of GIT_SYNC_FIELDS) {
const currentValue = (normalizedCurrent as any)[field];
const newValue = (normalizedNew as any)[field];
if (!deepEqual(currentValue, newValue)) {
changes[field] = {
from: currentValue,
to: newValue,
};
}
}
return changes;
}
// Helper to display changes in human-readable format
export function displayChanges(
changes: { [key: string]: { from: any; to: any } },
): void {
for (const [field, change] of Object.entries(changes)) {
if (
Array.isArray(change.from) ||
Array.isArray(change.to)
) {
console.log(colors.yellow(` ${field}:`));
console.log(
colors.red(
` - ${JSON.stringify(change.from)}`,
),
);
console.log(
colors.green(
` + ${JSON.stringify(change.to)}`,
),
);
} else {
console.log(
colors.yellow(` ${field}: `) +
colors.red(`${change.from}`) +
" → " +
colors.green(`${change.to}`),
);
}
}
}