fix: fix circular dependancy breaking bundling of cli (#6219)

* fix: fix circular dependancy breaking bundling of cli

* remove hubpaths
This commit is contained in:
Alexander Petric
2025-07-17 18:13:55 -04:00
committed by GitHub
parent af1e30c21e
commit ceca360e70
18 changed files with 112 additions and 80 deletions

View File

@@ -1,5 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import { colors, Command, log, SEP, Table, yamlParseFile } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { ListableApp, Policy } from "./gen/types.gen.ts";

61
cli/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
// deno-lint-ignore-file no-explicit-any
import { colors, log, setClient } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { GlobalUserInfo } from "./gen/types.gen.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { GlobalOptions } from "./types.ts";
import {
addWorkspace,
removeWorkspace,
Workspace
} from "./workspace.ts";
/**
* Main authentication function - moved from context.ts to break circular dependencies
* This function maintains the original API signature from context.ts
*/
export async function requireLogin(
opts: GlobalOptions
): Promise<GlobalUserInfo> {
// Import resolveWorkspace to avoid circular dependency at module level
const { resolveWorkspace } = await import("./context.ts");
const workspace = await resolveWorkspace(opts);
let token = await tryGetLoginInfo(opts);
if (!token) {
token = workspace.token;
}
setClient(token, workspace.remote.substring(0, workspace.remote.length - 1));
try {
return await wmill.globalWhoami();
} catch (error) {
// Check for network errors and provide clearer messages
const errorMsg = error instanceof Error ? error.message : String(error);
if (errorMsg.includes('fetch') || errorMsg.includes('connection') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('refused')) {
throw new Error(`Network error: Could not connect to Windmill server at ${workspace.remote}`);
}
log.info(
"! Could not reach API given existing credentials. Attempting to reauth..."
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
throw new Error("Unauthorized: Could not authenticate with the provided credentials");
}
// Update workspace token
removeWorkspace(workspace.name, false, opts);
workspace.token = newToken;
addWorkspace(workspace, opts);
setClient(
newToken,
workspace.remote.substring(0, workspace.remote.length - 1)
);
return await wmill.globalWhoami();
}
}

View File

@@ -58,12 +58,12 @@ export async function resolveWorkspace(
if (opts.baseUrl) {
if (opts.workspace && opts.token) {
const normalizedBaseUrl = new URL(opts.baseUrl).toString(); // add trailing slash if not present
// Try to find existing workspace profile by name, then by workspaceId + remote
if (opts.workspace) {
// Try by workspace name first
let existingWorkspace = await getWorkspaceByName(opts.workspace, opts.configDir);
// If not found by name, try to find by workspaceId + remote match
if (!existingWorkspace) {
const { allWorkspaces } = await import("./workspace.ts");
@@ -71,13 +71,13 @@ export async function resolveWorkspace(
const matchingWorkspaces = workspaces.filter(
w => w.workspaceId === opts.workspace && w.remote === normalizedBaseUrl
);
// Due to uniqueness constraint, there can only be 0 or 1 match
if (matchingWorkspaces.length === 1) {
existingWorkspace = matchingWorkspaces[0];
}
}
if (existingWorkspace) {
// Validate that the base URL matches the profile's remote
if (existingWorkspace.remote !== normalizedBaseUrl) {
@@ -95,7 +95,7 @@ export async function resolveWorkspace(
};
}
}
// No existing profile found, create temporary workspace
return {
remote: normalizedBaseUrl,
@@ -121,45 +121,6 @@ export async function resolveWorkspace(
}
}
export async function requireLogin(
opts: GlobalOptions
): Promise<GlobalUserInfo> {
const workspace = await resolveWorkspace(opts);
let token = await tryGetLoginInfo(opts);
if (!token) {
token = workspace.token;
}
setClient(token, workspace.remote.substring(0, workspace.remote.length - 1));
try {
return await wmill.globalWhoami();
} catch (error) {
// Check for network errors and provide clearer messages
const errorMsg = error instanceof Error ? error.message : String(error);
if (errorMsg.includes('fetch') || errorMsg.includes('connection') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('refused')) {
throw new Error(`Network error: Could not connect to Windmill server at ${workspace.remote}`);
}
log.info(
"! Could not reach API given existing credentials. Attempting to reauth..."
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
throw new Error("Unauthorized: Could not authenticate with the provided credentials");
}
removeWorkspace(workspace.name, false, opts);
workspace.token = newToken;
addWorkspace(workspace, opts);
setClient(
newToken,
workspace.remote.substring(0, workspace.remote.length - 1)
);
return await wmill.globalWhoami();
}
}
export async function fetchVersion(baseUrl: string): Promise<string> {
const requestHeaders = new Headers();
@@ -176,13 +137,13 @@ export async function fetchVersion(baseUrl: string): Promise<string> {
new URL(new URL(baseUrl).origin + "/api/version"),
{ headers: requestHeaders, method: "GET" }
);
if (!response.ok) {
// Consume response body even on error to avoid resource leak
await response.text();
throw new Error(`Failed to fetch version: ${response.status} ${response.statusText}`);
}
return await response.text();
}
export async function tryResolveVersion(

View File

@@ -12,7 +12,8 @@ import {
} from "./deps.ts";
import { getTypeStrFromPath, GlobalOptions } from "./types.ts";
import { ignoreF } from "./sync.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace } from "./context.ts";
import {
SyncOptions,
mergeConfigWithConfigFile,

View File

@@ -4,7 +4,8 @@ import { Confirm, SEP, log, yamlStringify } from "./deps.ts";
import { colors, Command, Table, yamlParseFile } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import { resolve, track_job } from "./script.ts";
import { defaultFlowDefinition } from "./bootstrap/flow_bootstrap.ts";
import { blueColor, generateFlowLockInternal } from "./metadata.ts";

View File

@@ -2,7 +2,8 @@
import { colors, Command, log, SEP, Table } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import { GlobalOptions, isSuperset, parseFromFile } from "./types.ts";
import { Folder } from "./gen/types.gen.ts";

View File

@@ -1,6 +1,7 @@
import { colors, Command, log, yamlStringify } from "./deps.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace } from "./context.ts";
import * as wmill from "./gen/services.gen.ts";
import {
DEFAULT_SYNC_OPTIONS,

View File

@@ -2,7 +2,8 @@
import { Command, log } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace } from "./context.ts";
import { pushResourceType } from "./resource-type.ts";
import { GlobalOptions } from "./types.ts";
import { deepEqual } from "./utils.ts";

View File

@@ -159,9 +159,8 @@ const command = new Command()
// Check for backend git-sync settings unless --use-default is specified
if (!opts.useDefault) {
try {
const { requireLogin, resolveWorkspace } = await import(
"./context.ts"
);
const { requireLogin } = await import("./auth.ts");
const { resolveWorkspace } = await import("./context.ts");
// Check if user has workspace configured
const { getActiveWorkspace } = await import(

View File

@@ -5,7 +5,8 @@ import {
parseFromFile,
removeType,
} from "./types.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace } from "./context.ts";
import { colors, Command, log, Table } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { ResourceType } from "./gen/types.gen.ts";

View File

@@ -5,7 +5,8 @@ import {
parseFromFile,
removeType,
} from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import { colors, Command, log, SEP, Table } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { Resource } from "./gen/types.gen.ts";

View File

@@ -1,6 +1,7 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command, log, SEP, Table } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import * as wmill from "./gen/services.gen.ts";
import {

View File

@@ -1,6 +1,7 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
Command,

View File

@@ -1,4 +1,5 @@
import { fetchVersion, requireLogin, resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { fetchVersion, resolveWorkspace } from "./context.ts";
import {
colors,
Command,

View File

@@ -16,9 +16,8 @@ import {
parseFromFile,
removeType,
} from "./types.ts";
import { requireLogin } from "./context.ts";
import { validatePath } from "./context.ts";
import { resolveWorkspace } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { validatePath, resolveWorkspace } from "./context.ts";
type Trigger = {
http: HttpTrigger;

View File

@@ -1,5 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin } from "./context.ts";
import { requireLogin } from "./auth.ts";
import {
GlobalOptions,
isSuperset,

View File

@@ -1,5 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { requireLogin } from "./auth.ts";
import { resolveWorkspace, validatePath } from "./context.ts";
import {
GlobalOptions,
isSuperset,

View File

@@ -3,9 +3,9 @@ import { GlobalOptions } from "./types.ts";
import { getRootStore } from "./store.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { colors, Command, Confirm, Input, log, setClient, Table } from "./deps.ts";
import { requireLogin } from "./auth.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin } from "./context.ts";
export interface Workspace {
remote: string;
@@ -258,11 +258,11 @@ export async function add(
export async function addWorkspace(workspace: Workspace, opts: any) {
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
// Check for conflicts before adding
const existingWorkspaces = await allWorkspaces(opts.configDir);
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal() && !opts.force;
// Check 1: Workspace name already exists
const nameConflict = existingWorkspaces.find(w => w.name === workspace.name);
if (nameConflict) {
@@ -274,7 +274,7 @@ export async function addWorkspace(workspace: Workspace, opts: any) {
log.info(colors.red.bold(`❌ Workspace name "${workspace.name}" already exists!`));
log.info(` Existing: ${nameConflict.workspaceId} on ${nameConflict.remote}`);
log.info(` New: ${workspace.workspaceId} on ${workspace.remote}`);
if (!isInteractive) {
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
if (opts.force) {
@@ -287,7 +287,7 @@ export async function addWorkspace(workspace: Workspace, opts: any) {
message: "Do you want to overwrite the existing workspace?",
default: false,
});
if (!overwrite) {
log.info(colors.yellow("Operation cancelled."));
return;
@@ -295,20 +295,20 @@ export async function addWorkspace(workspace: Workspace, opts: any) {
}
}
}
// Check 2: Same (remote, workspaceId) tuple already exists under different name
const tupleConflict = existingWorkspaces.find(w =>
w.remote === workspace.remote &&
w.workspaceId === workspace.workspaceId &&
const tupleConflict = existingWorkspaces.find(w =>
w.remote === workspace.remote &&
w.workspaceId === workspace.workspaceId &&
w.name !== workspace.name
);
if (tupleConflict) {
log.info(colors.red.bold(`❌ Workspace ${workspace.workspaceId} on ${workspace.remote} already exists!`));
log.info(` Existing name: "${tupleConflict.name}"`);
log.info(` New name: "${workspace.name}"`);
log.info(colors.yellow(`\nNote: Backend constraint prevents duplicate (remote, workspaceId) combinations.`));
if (!isInteractive) {
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
if (opts.force) {
@@ -321,20 +321,20 @@ export async function addWorkspace(workspace: Workspace, opts: any) {
message: `Do you want to overwrite the existing workspace "${tupleConflict.name}"?`,
default: false,
});
if (!overwrite) {
log.info(colors.yellow("Operation cancelled."));
return;
}
}
// Remove the conflicting workspace
await removeWorkspace(tupleConflict.name, true, opts);
}
// Remove existing workspace with same name (if updating)
await removeWorkspace(workspace.name, true, opts);
// Add the new workspace
const file = await Deno.open((await getRootStore(opts.configDir)) + "remotes.ndjson", {
append: true,