mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
220cd35cf7
* feat: support $f/ and $u/ import path aliases for scripts $f/ and $u/ are local-friendly aliases for the absolute workspace import paths /f/ and /u/. Unlike the /-prefixed form (which local tools treat as a filesystem-root path), the $-prefixed form is a bare specifier that can be remapped via tsconfig paths / Deno import maps, so the same import resolves on the Windmill worker and in a local editor. - worker: recognize $f//$u/ in the Deno import map and both Bun loaders - dep-map/parser: normalize $f/->f/, $u/->u/ for lockgen + dep tracking - cli: emit $f/$u path aliases in generated tsconfig.json / deno.json - frontend: ATA + Monaco paths resolve $f//$u/ type hints in the editor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): split generated tsconfig into managed + user file with refresh command Mirror the AGENTS.cli.md/AGENTS.md prompts model for the IDE tsconfig so the recommended settings can evolve without ever clobbering user customizations: - tsconfig.wmill.json: wmill-managed, always refreshed, holds recommended compilerOptions incl. the $f/$u path aliases (Deno: import_map.wmill.json) - tsconfig.json: user-owned, created once, just extends the managed file; warn (never auto-edit) when an existing one doesn't reference it - add 'wmill refresh tsconfig'; init generates it unconditionally (no longer gated behind resource-type namespace / a bound workspace) - regenerate CLI guidance docs for the new subcommand Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): address PR review on $f/ tsconfig generation - handle existing deno.jsonc so we don't shadow it with a new deno.json (P1 identified by cubic) - fix the bun-types hint that pointed users at the managed do-not-edit tsconfig.wmill.json; tell them to install + re-run 'wmill refresh tsconfig' - document the .ts-extension-only local-resolution limitation (cross-flavor .bun.ts/.deno.ts/.fetch.ts scripts won't resolve in a local editor) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): warn when a project's tsconfig isn't wired to tsconfig.wmill.json Mirror the prompts freshness check for the managed tsconfig so users with an existing setup actually discover they're missing $f//$u/ resolution: - embed a version hash in tsconfig.wmill.json (excludes the env-dependent bun-types 'types' entry so it doesn't false-positive) - add warnIfTsconfigStale to the main.ts freshness hook, gated identically to the prompts check (skips init/refresh/help/version). When a tsconfig.json exists it warns one line (stderr) if the managed file is missing, not referenced via extends, or out of date; silent for non-TS projects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): make tsconfig setup equivalent to prompts (auto-wire + stale-only) Unify the two managed-file systems so they behave identically: - auto-wire an existing unlinked tsconfig.json/deno.json on init/refresh (add extends / importMap; merge into an array extends), instead of only warning. Parses JSON and falls back to a warning when it can't round-trip (JSONC comments, or a conflicting deno imports/importMap) — never corrupts. - narrow warnIfTsconfigStale to stale-only, gated on the managed file existing, exactly like warnIfPromptsStale: it no longer nags about a missing or unlinked tsconfig.json, so a deliberately-custom/unlinked setup stays silent and a not-yet-initialized project isn't bothered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): place tsconfig.wmill.json first in extends to preserve user base config Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): migrate legacy tsconfig and require consent for custom configs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): align prompts wiring to the same consent model Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(cli): bump windmill-parser-wasm-ts to 1.714.0 for $f/ $u/ aliases Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(worker): resolve $f/ and $u/ in deno lock generation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: narrow relative-imports lock-gen guard to deno import-map failure Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(cli): sync bun.lock with windmill-parser-wasm-ts 1.714.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): warn when a custom tsconfig's paths would shadow $f/ $u/ aliases Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import { stat, writeFile, rm } from "node:fs/promises";
|
|
import { colors } from "@cliffy/ansi/colors";
|
|
import { Command } from "@cliffy/command";
|
|
import { Confirm } from "@cliffy/prompt/confirm";
|
|
import { Select } from "@cliffy/prompt/select";
|
|
import { Input } from "@cliffy/prompt/input";
|
|
import * as log from "../../core/log.ts";
|
|
import { type WorkspaceBinding } from "./template.ts";
|
|
import { GlobalOptions } from "../../types.ts";
|
|
import { readLockfile } from "../../utils/metadata.ts";
|
|
import {
|
|
getActiveWorkspaceOrFallback,
|
|
getActiveWorkspace,
|
|
allWorkspaces,
|
|
add as addWorkspaceProfile,
|
|
type Workspace,
|
|
} from "../workspace/workspace.ts";
|
|
import { generateRTNamespace } from "../resource-type/resource-type.ts";
|
|
import { generateCommentedTemplate } from "./template.ts";
|
|
import { refreshPrompts } from "../refresh/prompts.ts";
|
|
import { refreshTsconfig } from "../refresh/tsconfig.ts";
|
|
|
|
export interface InitOptions {
|
|
useDefault?: boolean;
|
|
useBackend?: boolean;
|
|
repository?: string;
|
|
workspace?: string;
|
|
debug?: unknown;
|
|
showDiffs?: boolean;
|
|
token?: string;
|
|
baseUrl?: string;
|
|
configDir?: string;
|
|
bindProfile?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Bootstrap a windmill project with a wmill.yaml file
|
|
*/
|
|
async function initAction(opts: InitOptions) {
|
|
let didBindWorkspace = false;
|
|
let boundProfile: Workspace | undefined;
|
|
|
|
if (await stat("wmill.yaml").catch(() => null)) {
|
|
log.info("wmill.yaml already exists, skipping config generation");
|
|
} else {
|
|
// Detect current git branch for template
|
|
const { isGitRepository, getCurrentGitBranch } = await import(
|
|
"../../utils/git.ts"
|
|
);
|
|
let branchName: string | undefined;
|
|
let wsBindings: WorkspaceBinding[] | undefined;
|
|
// boundProfile is set during bind (hoisted to function scope)
|
|
const inGitRepo = isGitRepository();
|
|
if (inGitRepo) {
|
|
branchName = getCurrentGitBranch() ?? undefined;
|
|
}
|
|
|
|
const isInteractive = !!process.stdin.isTTY && !opts.useDefault;
|
|
|
|
if (isInteractive && opts.bindProfile !== false) {
|
|
const shouldBind = opts.bindProfile === true || await Confirm.prompt({
|
|
message: "Bind a workspace?",
|
|
default: true,
|
|
});
|
|
|
|
if (shouldBind) {
|
|
// Step 1: Pick workspace profile (same as wmill workspace bind)
|
|
let profiles = await allWorkspaces(opts.configDir);
|
|
let selectedProfile: Workspace | undefined;
|
|
|
|
if (profiles.length === 0) {
|
|
log.info(colors.yellow("No workspace profiles found. Let's create one."));
|
|
await addWorkspaceProfile(opts as any, undefined, undefined, undefined);
|
|
profiles = await allWorkspaces(opts.configDir);
|
|
selectedProfile = profiles.length > 0
|
|
? await getActiveWorkspace(opts as GlobalOptions)
|
|
: undefined;
|
|
} else {
|
|
const activeProfile = await getActiveWorkspace(opts as GlobalOptions);
|
|
const orderedProfiles = activeProfile
|
|
? [
|
|
...profiles.filter((p) => p.name === activeProfile.name),
|
|
...profiles.filter((p) => p.name !== activeProfile.name),
|
|
]
|
|
: profiles;
|
|
const selectedName = await Select.prompt({
|
|
message: "Select workspace profile",
|
|
options: orderedProfiles.map((p) => ({
|
|
name: `${p.name} (${p.workspaceId} on ${p.remote})${
|
|
activeProfile?.name === p.name ? " — active" : ""
|
|
}`,
|
|
value: p.name,
|
|
})),
|
|
});
|
|
selectedProfile = profiles.find((p) => p.name === selectedName);
|
|
}
|
|
|
|
if (selectedProfile) {
|
|
// Step 2: Pick workspace name
|
|
const wsName = await Input.prompt({
|
|
message: "Workspace name (key in wmill.yaml)",
|
|
default: selectedProfile.workspaceId,
|
|
});
|
|
|
|
// Step 3: Pick git branch (only in git repos)
|
|
let gitBranch: string | undefined;
|
|
if (inGitRepo) {
|
|
const branchInput = await Input.prompt({
|
|
message: "Git branch to associate",
|
|
default: branchName ?? wsName,
|
|
});
|
|
if (branchInput !== wsName) {
|
|
gitBranch = branchInput;
|
|
}
|
|
}
|
|
|
|
wsBindings = [{
|
|
name: wsName,
|
|
baseUrl: selectedProfile.remote,
|
|
workspaceId: selectedProfile.workspaceId !== wsName ? selectedProfile.workspaceId : wsName,
|
|
gitBranch,
|
|
}];
|
|
boundProfile = selectedProfile;
|
|
}
|
|
}
|
|
} else if (opts.bindProfile === true) {
|
|
// Non-interactive bind: create a single workspace entry from active profile
|
|
const activeWorkspace = await getActiveWorkspaceOrFallback(
|
|
opts as GlobalOptions
|
|
);
|
|
if (activeWorkspace) {
|
|
const wsName = branchName ?? activeWorkspace.workspaceId;
|
|
wsBindings = [{
|
|
name: wsName,
|
|
baseUrl: activeWorkspace.remote,
|
|
workspaceId: activeWorkspace.workspaceId !== wsName ? activeWorkspace.workspaceId : wsName,
|
|
}];
|
|
boundProfile = activeWorkspace;
|
|
}
|
|
}
|
|
|
|
await writeFile("wmill.yaml", generateCommentedTemplate(branchName, undefined, wsBindings), "utf-8");
|
|
log.info(colors.green("wmill.yaml created with default settings"));
|
|
if (wsBindings && wsBindings.length > 0) {
|
|
didBindWorkspace = true;
|
|
log.info(
|
|
colors.green(
|
|
`✓ Bound workspace '${wsBindings[0].name}' → ${wsBindings[0].workspaceId} on ${wsBindings[0].baseUrl}`
|
|
)
|
|
);
|
|
log.info(
|
|
colors.gray("To bind additional workspaces, run: wmill workspace bind")
|
|
);
|
|
} else {
|
|
log.warn(
|
|
"⚠️ No workspace bound. Sync commands will not work without a workspace.\n" +
|
|
" Run 'wmill workspace bind' to bind a workspace to this project."
|
|
);
|
|
}
|
|
|
|
// Create lock file
|
|
await readLockfile();
|
|
|
|
// Check for backend git-sync settings — only if a workspace was bound and not --use-default
|
|
if (!opts.useDefault && didBindWorkspace && boundProfile) {
|
|
try {
|
|
const { setClient } = await import("../../core/client.ts");
|
|
|
|
// Use the bound profile directly — skip requireLogin which would resolve to the active profile
|
|
setClient(boundProfile.token, boundProfile.remote.replace(/\/$/, ""));
|
|
|
|
const wmill = await import("../../../gen/services.gen.ts");
|
|
const settings = await wmill.getSettings({
|
|
workspace: boundProfile.workspaceId,
|
|
});
|
|
|
|
if (
|
|
settings.git_sync?.repositories &&
|
|
settings.git_sync.repositories.length > 0
|
|
) {
|
|
let useBackendSettings = opts.useBackend;
|
|
|
|
// If repository is specified, implicitly use backend settings
|
|
if (opts.repository && !opts.useDefault) {
|
|
useBackendSettings = true;
|
|
}
|
|
|
|
if (useBackendSettings === undefined) {
|
|
const choice = await Select.prompt({
|
|
message:
|
|
"Git-sync settings found on backend. What would you like to do?",
|
|
options: [
|
|
{ name: "Use backend git-sync settings", value: "backend" },
|
|
{ name: "Use default settings", value: "default" },
|
|
{ name: "Cancel", value: "cancel" },
|
|
],
|
|
});
|
|
|
|
if (choice === "cancel") {
|
|
try {
|
|
await rm("wmill.yaml");
|
|
await rm("wmill-lock.yaml");
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
log.info("Init cancelled");
|
|
process.exit(0);
|
|
}
|
|
|
|
useBackendSettings = choice === "backend";
|
|
}
|
|
|
|
if (useBackendSettings) {
|
|
log.info("Applying git-sync settings from backend...");
|
|
const { pullGitSyncSettings } = await import(
|
|
"../gitsync-settings/gitsync-settings.ts"
|
|
);
|
|
const gsOpts = {
|
|
...(opts as GlobalOptions),
|
|
workspace: boundProfile.name,
|
|
repository: opts.repository,
|
|
jsonOutput: false,
|
|
diff: false,
|
|
replace: true,
|
|
};
|
|
(gsOpts as any).__secret_workspace = boundProfile;
|
|
await pullGitSyncSettings(gsOpts);
|
|
log.info(colors.green("Git-sync settings applied from backend"));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.warn(
|
|
`Could not check backend for git-sync settings: ${(error as Error).message}`
|
|
);
|
|
log.info("Continuing with default settings");
|
|
}
|
|
}
|
|
}
|
|
|
|
await refreshPrompts({ yes: opts.useDefault === true });
|
|
|
|
// Generate the IDE tsconfig (managed tsconfig.wmill.json + user tsconfig.json
|
|
// that extends it). Independent of any workspace binding — it's purely local.
|
|
try {
|
|
await refreshTsconfig({ yes: opts.useDefault === true });
|
|
} catch (error) {
|
|
log.warn(
|
|
`Could not generate tsconfig: ${
|
|
error instanceof Error ? error.message : error
|
|
}`
|
|
);
|
|
}
|
|
|
|
// Generate resource type namespace (only if a workspace was bound)
|
|
if (didBindWorkspace && boundProfile) {
|
|
try {
|
|
// Cache the bound profile so resolveWorkspace doesn't re-resolve and prompt again
|
|
const rtOpts = { ...opts } as GlobalOptions;
|
|
(rtOpts as any).__secret_workspace = boundProfile;
|
|
await generateRTNamespace(rtOpts);
|
|
} catch (error) {
|
|
log.warn(
|
|
`Could not pull resource types and generate TypeScript namespace: ${
|
|
error instanceof Error ? error.message : error
|
|
}`
|
|
);
|
|
}
|
|
} else {
|
|
log.info(
|
|
colors.gray("Skipped resource type namespace generation (no workspace bound). Run 'wmill workspace bind' then 'wmill init' to generate it.")
|
|
);
|
|
}
|
|
}
|
|
|
|
const command = new Command()
|
|
.description("Bootstrap a windmill project with a wmill.yaml file")
|
|
.option("--use-default", "Use default settings without checking backend")
|
|
.option("--use-backend", "Use backend git-sync settings if available")
|
|
.option(
|
|
"--repository <repo:string>",
|
|
"Specify repository path (e.g., u/user/repo) when using backend settings"
|
|
)
|
|
.option(
|
|
"--bind-profile",
|
|
"Automatically bind active workspace profile to current Git branch"
|
|
)
|
|
.option("--no-bind-profile", "Skip workspace profile binding prompt")
|
|
.action(initAction as any);
|
|
|
|
export default command;
|