mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
* fix(cli): prevent duplicate 'Using non-dotted paths' log message Add a flag to track whether the message has already been logged, so it only prints once even if setNonDottedPaths is called multiple times. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): add --branch option to sync pull/push commands Add a --branch argument that allows overriding the current git branch for sync operations. This enables: - Using branch-specific settings even when not in a git repository - Overriding the detected git branch when needed The branch override is applied to: - getEffectiveSettings() for branch-specific config overrides - getSpecificItemsForCurrentBranch() for branch-specific items Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): correct log message for workspace fork branches Use rawGitBranch instead of currentBranch in the log message when showing the origin of a workspace fork branch. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): extend --branch support to specificItems functionality Update getBranchSpecificPath and isCurrentBranchFile to accept optional branchOverride parameter. This ensures that branch-specific file handling (for variables, resources, triggers) works correctly with --branch flag. Updated functions: - getBranchSpecificPath(): now accepts branchOverride - isCurrentBranchFile(): now accepts branchOverride - elementsToMap(): now accepts branchOverride - compareDynFSElement(): now accepts branchOverride - prettyChanges(): now accepts branchOverride All call sites updated to pass opts.branch through the call chain. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): resolve TypeScript type errors - Fix Timeout type in dev.ts using ReturnType<typeof setTimeout> - Add proper type casts for unknown error types - Cast FlowModule to any to resolve generated type mismatch - Cast Uint8Array to BlobPart for Blob constructor compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(cli): add unit tests for branch detection and --branch override - Add specific_items.test.ts with 35 tests covering: - toBranchSpecificPath and fromBranchSpecificPath conversions - isSpecificItem pattern matching - isBranchSpecificFile detection - Round-trip path conversions - branchOverride parameter functionality - Add conf_branch_override.test.ts with 6 tests covering: - getEffectiveSettings with branchOverride parameter - Branch-specific overrides application - promotionOverrides precedence - Fallback to top-level settings - Fix containerized_backend.ts to use dynamic paths instead of hardcoded user home directories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(cli): add conf.ts barrel file for test imports Re-exports from src/core/conf.ts to support existing test imports. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all * fix(cli): pass --branch override to workspace resolution Previously, the --branch flag was only used for config resolution but not for workspace resolution. This caused confusing log messages that showed the git branch (e.g., master) before showing the override branch. Changes: - Add branchOverride parameter to tryResolveBranchWorkspace() - Add branchOverride parameter to resolveWorkspace() - Pass opts.branch from sync pull/push to resolveWorkspace() - Log "Using branch override" early in workspace resolution - Remove duplicate log from getEffectiveSettings() Now when using --branch foobar, the logs show: Using branch override: foobar Applied settings for Git branch: foobar ... Instead of the confusing previous output that mentioned both master and foobar. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(cli): remove redundant branch detection in elementsToMap isCurrentBranchFile() already validates that a branch exists (via branchOverride or git detection) before returning true. No need to pre-compute currentBranch before calling it. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
422 lines
13 KiB
TypeScript
422 lines
13 KiB
TypeScript
/**
|
|
* Utility module for handling resource folder naming conventions.
|
|
*
|
|
* This module centralizes the logic for detecting and manipulating paths
|
|
* that contain resource folders (.flow, .app, .raw_app).
|
|
*
|
|
* The folder suffixes can be configured to use either dot-prefixed names
|
|
* (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app).
|
|
*/
|
|
|
|
import { log, SEP } from "../../deps.ts";
|
|
|
|
// Resource types that use folder-based storage
|
|
export type FolderResourceType = "flow" | "app" | "raw_app";
|
|
|
|
// Configuration for folder suffixes - can be switched between dot and dunder prefixes
|
|
// The default uses dot-prefixed names (.flow, .app, .raw_app)
|
|
const DOTTED_SUFFIXES = {
|
|
flow: ".flow",
|
|
app: ".app",
|
|
raw_app: ".raw_app",
|
|
} as const;
|
|
|
|
// Alternative dunder-prefixed names (__flow, __app, __raw_app)
|
|
const NON_DOTTED_SUFFIXES = {
|
|
flow: "__flow",
|
|
app: "__app",
|
|
raw_app: "__raw_app",
|
|
} as const;
|
|
|
|
export type FolderSuffixes =
|
|
| typeof DOTTED_SUFFIXES
|
|
| typeof NON_DOTTED_SUFFIXES;
|
|
|
|
// Global state for nonDottedPaths configuration
|
|
let _nonDottedPaths = false;
|
|
let _nonDottedPathsLogged = false;
|
|
|
|
/**
|
|
* Set whether to use non-dotted paths (__flow, __app, __raw_app)
|
|
* instead of dotted paths (.flow, .app, .raw_app).
|
|
* This should be called once at startup based on wmill.yaml configuration.
|
|
*/
|
|
export function setNonDottedPaths(value: boolean): void {
|
|
if (value && !_nonDottedPathsLogged) {
|
|
log.info("Using non-dotted paths (__flow, __app, __raw_app)");
|
|
_nonDottedPathsLogged = true;
|
|
}
|
|
_nonDottedPaths = value;
|
|
}
|
|
|
|
/**
|
|
* Get the current nonDottedPaths setting.
|
|
*/
|
|
export function getNonDottedPaths(): boolean {
|
|
return _nonDottedPaths;
|
|
}
|
|
|
|
/**
|
|
* Get the folder suffixes based on the global configuration.
|
|
*/
|
|
export function getFolderSuffixes(): FolderSuffixes {
|
|
return _nonDottedPaths ? NON_DOTTED_SUFFIXES : DOTTED_SUFFIXES;
|
|
}
|
|
|
|
// Metadata file names inside each folder type
|
|
const METADATA_FILES = {
|
|
flow: { yaml: "flow.yaml", json: "flow.json" },
|
|
app: { yaml: "app.yaml", json: "app.json" },
|
|
raw_app: { yaml: "raw_app.yaml", json: "raw_app.json" },
|
|
} as const;
|
|
|
|
/**
|
|
* Get the folder suffix for a resource type (e.g., ".flow", ".app", ".raw_app" or "__flow", "__app", "__raw_app")
|
|
*/
|
|
export function getFolderSuffix(type: FolderResourceType): string {
|
|
return getFolderSuffixes()[type];
|
|
}
|
|
|
|
/**
|
|
* Get the folder suffix with path separator (e.g., ".flow/", ".app/", ".raw_app/")
|
|
*/
|
|
export function getFolderSuffixWithSep(type: FolderResourceType): string {
|
|
return getFolderSuffixes()[type] + SEP;
|
|
}
|
|
|
|
/**
|
|
* Get the metadata file name for a resource type
|
|
*/
|
|
export function getMetadataFileName(
|
|
type: FolderResourceType,
|
|
format: "yaml" | "json"
|
|
): string {
|
|
return METADATA_FILES[type][format];
|
|
}
|
|
|
|
/**
|
|
* Get the full metadata file path suffix (e.g., ".flow/flow.yaml" or "__flow/flow.yaml")
|
|
*/
|
|
export function getMetadataPathSuffix(
|
|
type: FolderResourceType,
|
|
format: "yaml" | "json"
|
|
): string {
|
|
return getFolderSuffixes()[type] + "/" + METADATA_FILES[type][format];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Path Detection Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if a path is inside a flow folder
|
|
*/
|
|
export function isFlowPath(p: string): boolean {
|
|
return p.includes(getFolderSuffixes().flow + SEP);
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside an app folder
|
|
*/
|
|
export function isAppPath(p: string): boolean {
|
|
return p.includes(getFolderSuffixes().app + SEP);
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside a raw_app folder
|
|
*/
|
|
export function isRawAppPath(p: string): boolean {
|
|
return p.includes(getFolderSuffixes().raw_app + SEP);
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside any folder-based resource (flow, app, or raw_app)
|
|
*/
|
|
export function isFolderResourcePath(p: string): boolean {
|
|
return isFlowPath(p) || isAppPath(p) || isRawAppPath(p);
|
|
}
|
|
|
|
/**
|
|
* Detect the resource type from a path, if any
|
|
*/
|
|
export function detectFolderResourceType(p: string): FolderResourceType | null {
|
|
if (isFlowPath(p)) return "flow";
|
|
if (isAppPath(p)) return "app";
|
|
if (isRawAppPath(p)) return "raw_app";
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside a raw app backend folder.
|
|
* Matches patterns like: .../myApp.raw_app/backend/... or .../myApp__raw_app/backend/...
|
|
*/
|
|
export function isRawAppBackendPath(filePath: string): boolean {
|
|
const suffixes = getFolderSuffixes();
|
|
// Normalize path separators for consistent matching
|
|
const normalizedPath = filePath.replaceAll(SEP, "/");
|
|
// Check if path contains pattern: *.[suffix]/backend/
|
|
const escapedSuffix = suffixes.raw_app.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const pattern = new RegExp(`${escapedSuffix}/backend/`);
|
|
return pattern.test(normalizedPath);
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside a normal app folder (inline script).
|
|
* Matches patterns like: .../myApp.app/... or .../myApp__app/...
|
|
* This is used to detect inline scripts that belong to normal apps.
|
|
*/
|
|
export function isAppInlineScriptPath(filePath: string): boolean {
|
|
const suffixes = getFolderSuffixes();
|
|
// Normalize path separators for consistent matching
|
|
const normalizedPath = filePath.replaceAll(SEP, "/");
|
|
// Check if path contains pattern: *.[suffix]/
|
|
const escapedSuffix = suffixes.app.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const pattern = new RegExp(`${escapedSuffix}/`);
|
|
return pattern.test(normalizedPath);
|
|
}
|
|
|
|
/**
|
|
* Check if a path is inside a flow folder (inline script).
|
|
* Matches patterns like: .../myFlow.flow/... or .../myFlow__flow/...
|
|
* This is used to detect inline scripts that belong to flows.
|
|
*/
|
|
export function isFlowInlineScriptPath(filePath: string): boolean {
|
|
const suffixes = getFolderSuffixes();
|
|
// Normalize path separators for consistent matching
|
|
const normalizedPath = filePath.replaceAll(SEP, "/");
|
|
// Check if path contains pattern: *.[suffix]/
|
|
const escapedSuffix = suffixes.flow.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const pattern = new RegExp(`${escapedSuffix}/`);
|
|
return pattern.test(normalizedPath);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Path Manipulation Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Extract the resource name from a path (the part before the folder suffix)
|
|
* e.g., "f/my_flow.flow/flow.yaml" -> "f/my_flow"
|
|
*/
|
|
export function extractResourceName(
|
|
p: string,
|
|
type: FolderResourceType
|
|
): string | null {
|
|
const suffix = getFolderSuffixes()[type] + SEP;
|
|
const index = p.indexOf(suffix);
|
|
if (index === -1) return null;
|
|
return p.substring(0, index);
|
|
}
|
|
|
|
/**
|
|
* Extract the folder path (resource name + folder suffix)
|
|
* e.g., "f/my_flow.flow/flow.yaml" -> "f/my_flow.flow/"
|
|
*/
|
|
export function extractFolderPath(
|
|
p: string,
|
|
type: FolderResourceType
|
|
): string | null {
|
|
const suffix = getFolderSuffixes()[type] + SEP;
|
|
const index = p.indexOf(suffix);
|
|
if (index === -1) return null;
|
|
return p.substring(0, index) + suffix;
|
|
}
|
|
|
|
/**
|
|
* Build a folder path from a resource name
|
|
* e.g., ("f/my_flow", "flow") -> "f/my_flow.flow"
|
|
*/
|
|
export function buildFolderPath(
|
|
resourceName: string,
|
|
type: FolderResourceType
|
|
): string {
|
|
return resourceName + getFolderSuffixes()[type];
|
|
}
|
|
|
|
/**
|
|
* Build a metadata file path from a resource name
|
|
* e.g., ("f/my_flow", "flow", "yaml") -> "f/my_flow.flow/flow.yaml"
|
|
*/
|
|
export function buildMetadataPath(
|
|
resourceName: string,
|
|
type: FolderResourceType,
|
|
format: "yaml" | "json"
|
|
): string {
|
|
return (
|
|
resourceName +
|
|
getFolderSuffixes()[type] +
|
|
SEP +
|
|
METADATA_FILES[type][format]
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Folder Validation Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if a directory name ends with a specific resource folder suffix
|
|
* e.g., "my_app.raw_app" ends with ".raw_app" or "my_app__raw_app" ends with "__raw_app"
|
|
*/
|
|
export function hasFolderSuffix(
|
|
dirName: string,
|
|
type: FolderResourceType
|
|
): boolean {
|
|
return dirName.endsWith(getFolderSuffixes()[type]);
|
|
}
|
|
|
|
/**
|
|
* Validate that a directory name has the expected folder suffix
|
|
* Returns an error message if invalid, null if valid
|
|
*/
|
|
export function validateFolderName(
|
|
dirName: string,
|
|
type: FolderResourceType
|
|
): string | null {
|
|
const suffixes = getFolderSuffixes();
|
|
if (!hasFolderSuffix(dirName, type)) {
|
|
return `'${dirName}' does not end with '${suffixes[type]}'`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Extract the resource name from a folder name by removing the suffix
|
|
* e.g., "my_app.raw_app" -> "my_app" or "my_app__raw_app" -> "my_app"
|
|
*/
|
|
export function extractNameFromFolder(
|
|
folderName: string,
|
|
type: FolderResourceType
|
|
): string {
|
|
const suffix = getFolderSuffixes()[type];
|
|
if (folderName.endsWith(suffix)) {
|
|
return folderName.substring(0, folderName.length - suffix.length);
|
|
}
|
|
return folderName;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Metadata File Detection Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if a path ends with a flow metadata file suffix.
|
|
* Detects BOTH API format (always dotted: .flow.json) and local format (user-configured).
|
|
* This is necessary because the API always returns dotted format, but local files
|
|
* may use non-dotted format if nonDottedPaths is configured.
|
|
*/
|
|
export function isFlowMetadataFile(p: string): boolean {
|
|
// Always check API format (dotted)
|
|
if (
|
|
p.endsWith(DOTTED_SUFFIXES.flow + ".json") ||
|
|
p.endsWith(DOTTED_SUFFIXES.flow + ".yaml")
|
|
) {
|
|
return true;
|
|
}
|
|
// Also check non-dotted format for local files
|
|
if (_nonDottedPaths) {
|
|
return (
|
|
p.endsWith(NON_DOTTED_SUFFIXES.flow + ".json") ||
|
|
p.endsWith(NON_DOTTED_SUFFIXES.flow + ".yaml")
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a path ends with an app metadata file suffix.
|
|
* Detects BOTH API format (always dotted: .app.json) and local format (user-configured).
|
|
*/
|
|
export function isAppMetadataFile(p: string): boolean {
|
|
// Always check API format (dotted)
|
|
if (
|
|
p.endsWith(DOTTED_SUFFIXES.app + ".json") ||
|
|
p.endsWith(DOTTED_SUFFIXES.app + ".yaml")
|
|
) {
|
|
return true;
|
|
}
|
|
// Also check non-dotted format for local files
|
|
if (_nonDottedPaths) {
|
|
return (
|
|
p.endsWith(NON_DOTTED_SUFFIXES.app + ".json") ||
|
|
p.endsWith(NON_DOTTED_SUFFIXES.app + ".yaml")
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a path ends with a raw_app metadata file suffix.
|
|
* Detects BOTH API format (always dotted: .raw_app.json) and local format (user-configured).
|
|
*/
|
|
export function isRawAppMetadataFile(p: string): boolean {
|
|
// Always check API format (dotted)
|
|
if (
|
|
p.endsWith(DOTTED_SUFFIXES.raw_app + ".json") ||
|
|
p.endsWith(DOTTED_SUFFIXES.raw_app + ".yaml")
|
|
) {
|
|
return true;
|
|
}
|
|
// Also check non-dotted format for local files
|
|
if (_nonDottedPaths) {
|
|
return (
|
|
p.endsWith(NON_DOTTED_SUFFIXES.raw_app + ".json") ||
|
|
p.endsWith(NON_DOTTED_SUFFIXES.raw_app + ".yaml")
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a path ends with a specific raw_app metadata file
|
|
* (inside the folder, e.g., ".raw_app/raw_app.yaml" or "__raw_app/raw_app.yaml")
|
|
*/
|
|
export function isRawAppFolderMetadataFile(p: string): boolean {
|
|
return (
|
|
p.endsWith(getMetadataPathSuffix("raw_app", "yaml")) ||
|
|
p.endsWith(getMetadataPathSuffix("raw_app", "json"))
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sync-related Path Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get the path suffix to remove when converting local path to API path
|
|
* for delete operations
|
|
*/
|
|
export function getDeleteSuffix(
|
|
type: FolderResourceType,
|
|
format: "yaml" | "json"
|
|
): string {
|
|
return getFolderSuffixes()[type] + "/" + METADATA_FILES[type][format];
|
|
}
|
|
|
|
/**
|
|
* Transform a JSON path from API format to local directory path for sync.
|
|
* The API always returns dotted format (.flow.json, .app.json, .raw_app.json).
|
|
* This function transforms to the user's configured format (dotted or non-dotted).
|
|
* e.g., with nonDottedPaths=true: "f/my_flow.flow.json" -> "f/my_flow__flow"
|
|
* e.g., with nonDottedPaths=false: "f/my_flow.flow.json" -> "f/my_flow.flow"
|
|
*/
|
|
export function transformJsonPathToDir(
|
|
p: string,
|
|
type: FolderResourceType
|
|
): string {
|
|
// API always returns dotted format
|
|
const apiSuffix = DOTTED_SUFFIXES[type] + ".json";
|
|
if (p.endsWith(apiSuffix)) {
|
|
// Remove the API suffix and add user's configured suffix
|
|
const basePath = p.substring(0, p.length - apiSuffix.length);
|
|
return basePath + getFolderSuffixes()[type];
|
|
}
|
|
// Also handle the case where path already has user's configured format
|
|
const userSuffix = getFolderSuffixes()[type] + ".json";
|
|
if (p.endsWith(userSuffix)) {
|
|
return p.substring(0, p.length - 5); // Remove ".json"
|
|
}
|
|
// Path doesn't match expected suffix pattern, return unchanged
|
|
return p;
|
|
}
|