feat: support $f/ and $u/ import path aliases for scripts (#9378)

* 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>
This commit is contained in:
hugocasa
2026-06-03 13:58:12 +02:00
committed by GitHub
parent b3027c35cb
commit 220cd35cf7
25 changed files with 720 additions and 135 deletions
+15 -5
View File
@@ -129,7 +129,10 @@ impl Visit for ImportsFinder {
/// See also: [`parse_relative_imports`] for resolved absolute paths.
pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.to_string());
let fm = cm.new_source_file(
FileName::Custom("main.d.ts".into()).into(),
code.to_string(),
);
let mut tss = TsSyntax::default();
tss.disallow_ambiguous_jsx_like;
tss.tsx = true;
@@ -194,8 +197,10 @@ pub fn parse_relative_imports(code: &str, path: &str) -> anyhow::Result<Vec<Stri
// Remove .ts extension if present
let imp = imp.strip_suffix(".ts").unwrap_or(&imp);
if imp.starts_with("/") {
// Absolute path (e.g., /f/folder/script) - remove leading slash
if imp.starts_with("/") || imp.starts_with("$f/") || imp.starts_with("$u/") {
// Absolute path (e.g., /f/folder/script) or its local-friendly alias
// (e.g., $f/folder/script) - strip the leading `/` or `$` to get the
// workspace-rooted path.
imp[1..].to_string()
} else {
// Relative path (e.g., ./script or ../folder/script)
@@ -210,9 +215,14 @@ pub fn parse_relative_imports(code: &str, path: &str) -> anyhow::Result<Vec<Stri
Ok(resolved)
}
/// Check if an import path is a relative import (starts with `./`, `../`, or `/`)
/// Check if an import path is a relative import (starts with `./`, `../`, `/`, or the
/// local-friendly workspace aliases `$f/`/`$u/`)
fn is_relative_import(import_path: &str) -> bool {
import_path.starts_with("./") || import_path.starts_with("../") || import_path.starts_with("/")
import_path.starts_with("./")
|| import_path.starts_with("../")
|| import_path.starts_with("/")
|| import_path.starts_with("$f/")
|| import_path.starts_with("$u/")
}
/// Normalize a path by resolving `.` and `..` components
@@ -891,18 +891,40 @@ mod tests {
assert_eq!(result, vec!["f/shared/utils"]);
}
#[test]
fn test_relative_imports_dollar_alias() {
// `$f/`/`$u/` are local-friendly aliases for the absolute workspace paths
// `/f/`/`/u/` and must resolve to the same workspace-rooted path.
let code = r#"
import { shared } from "$f/shared/utils.ts";
import { mine } from "$u/me/lib";
export async function main() { return shared() + mine(); }
"#;
let result = parse_relative_imports(code, "f/folder/script").unwrap();
assert_eq!(result, vec!["f/shared/utils", "u/me/lib"]);
}
#[test]
fn test_relative_imports_mixed() {
let code = r#"
import { helper } from "./helper";
import { utils } from "../utils";
import { shared } from "/f/shared/lib";
import { aliased } from "$f/shared/aliased.ts";
import lodash from "lodash";
export async function main() { return helper() + utils() + shared(); }
export async function main() { return helper() + utils() + shared() + aliased(); }
"#;
let result = parse_relative_imports(code, "f/folder/script").unwrap();
// Should only include relative imports, not external packages like lodash
assert_eq!(result, vec!["f/folder/helper", "f/shared/lib", "f/utils"]);
assert_eq!(
result,
vec![
"f/folder/helper",
"f/shared/aliased",
"f/shared/lib",
"f/utils"
]
);
}
#[test]
+43
View File
@@ -4763,6 +4763,28 @@ export async function main() {
Ok(())
}
#[sqlx::test(fixtures("base", "relative_bun"))]
async fn test_relative_imports_bun_dollar_alias(db: Pool<Postgres>) -> anyhow::Result<()> {
// `$f/` is a local-friendly alias for the absolute workspace path `/f/` and must
// resolve identically on the worker. Mix it with relative imports to confirm both
// paths coexist.
let content = r#"
import { main as test1 } from "$f/system/same_folder_script.ts";
import { main as test2 } from "./same_folder_script.ts";
import { main as test3 } from "$f/system_relative/different_folder_script.ts";
import { main as test4 } from "../system_relative/different_folder_script.ts";
export async function main() {
return [test1(), test2(), test3(), test4()];
}
"#
.to_string();
run_deployed_relative_imports(&db, content.clone(), ScriptLang::Bun).await?;
run_preview_relative_imports(&db, content, ScriptLang::Bun).await?;
Ok(())
}
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "relative_bun"))]
async fn test_nested_imports_bun(db: Pool<Postgres>) -> anyhow::Result<()> {
@@ -4799,6 +4821,27 @@ export async function main() {
Ok(())
}
#[sqlx::test(fixtures("base", "relative_deno"))]
async fn test_relative_imports_deno_dollar_alias(db: Pool<Postgres>) -> anyhow::Result<()> {
// `$f/` is a local-friendly alias for the absolute workspace path `/f/` and must
// resolve identically via the Deno import map. Mix it with relative imports.
let content = r#"
import { main as test1 } from "$f/system/same_folder_script.ts";
import { main as test2 } from "./same_folder_script.ts";
import { main as test3 } from "$f/system_relative/different_folder_script.ts";
import { main as test4 } from "../system_relative/different_folder_script.ts";
export async function main() {
return [test1(), test2(), test3(), test4()];
}
"#
.to_string();
run_deployed_relative_imports(&db, content.clone(), ScriptLang::Deno).await?;
run_preview_relative_imports(&db, content, ScriptLang::Deno).await?;
Ok(())
}
#[sqlx::test(fixtures("base", "relative_deno"))]
async fn test_nested_imports_deno(db: Pool<Postgres>) -> anyhow::Result<()> {
let content = r#"
+5
View File
@@ -50,6 +50,11 @@ fn parse_ts_relative_imports(
let import = import.trim_end_matches(".ts");
if import.starts_with("/") {
relative_imports.push(import.trim_start_matches("/").to_string());
} else if import.starts_with("$f/") || import.starts_with("$u/") {
// `$f/...`/`$u/...` are local-friendly aliases for the absolute workspace
// paths `/f/...`/`/u/...`. Stripping the leading `$` yields the same
// workspace-rooted path the rest of the dependency machinery expects.
relative_imports.push(import.trim_start_matches("$").to_string());
} else if import.starts_with(".") {
let normalized = try_normalize(std::path::Path::new(&format!(
"{}/../{}",
+20
View File
@@ -914,6 +914,26 @@ pub async fn run_deployed_relative_imports(
.await
.unwrap();
// Regression guard for the Deno lock-gen import map (generate_deno_lock):
// it must resolve the `$f/`/`$u/` aliases, otherwise `deno cache --lock`
// fails with "not a dependency and not in import map". We match that
// specific failure rather than asserting lock_error_logs is empty —
// the field also captures benign, non-fatal lock-job output (e.g. Bun's
// "empty dependencies, skipping install"). (Runtime query to avoid
// touching the sqlx offline cache.)
let lock_error: Option<String> =
sqlx::query_scalar("SELECT lock_error_logs FROM script WHERE path = $1")
.bind("f/system/test_import")
.fetch_one(&db2)
.await
.unwrap();
if let Some(err) = &lock_error {
assert!(
!err.contains("not in import map"),
"lock generation failed to resolve a workspace import (likely $f//$u/): {err}"
);
}
let job = RunJob::from(JobPayload::ScriptHash {
path: "f/system/test_import".to_string(),
hash: ScriptHash(script.hash),
+18 -8
View File
@@ -38,7 +38,9 @@ const p = {
if (
(imp.path.startsWith(".") ||
imp.path.startsWith("/u/") ||
imp.path.startsWith("/f/")) &&
imp.path.startsWith("/f/") ||
imp.path.startsWith("$u/") ||
imp.path.startsWith("$f/")) &&
!imp.path.endsWith(".ts")
) {
code = code.replaceAll(imp.path, imp.path + ".ts");
@@ -97,21 +99,29 @@ const p = {
? current_path
: args.importer.replace(cdir + "/", "");
const isRelative = !args.path.startsWith("/");
const endExt = args.path.endsWith(".ts") ? "" : ".ts";
const pathNoExt = args.path.replace(/\.ts$/, "");
// `$f/...`/`$u/...` are local-friendly aliases for the absolute workspace
// paths `/f/...`/`/u/...`; rewrite to the `/`-prefixed form so the absolute
// resolution branch below handles them identically.
const importPath =
args.path.startsWith("$f/") || args.path.startsWith("$u/")
? "/" + args.path.slice(1)
: args.path;
const isRelative = !importPath.startsWith("/");
const endExt = importPath.endsWith(".ts") ? "" : ".ts";
const pathNoExt = importPath.replace(/\.ts$/, "");
// Lookup temp script hash
const normalized = (isRelative ? join(dirname(file_path), pathNoExt) : pathNoExt.slice(1)).replace(/\\/g, "/");
const hash = TEMP_SCRIPT_REFS?.[normalized];
const url = (isRelative
? `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${file_path}/../${args.path}${endExt}`
: `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${args.path}${endExt}`
? `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${file_path}/../${importPath}${endExt}`
: `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${importPath}${endExt}`
) + (hash ? `?temp_script_hash=${hash}` : "");
const file = isRelative
? resolve("./" + file_path + "/../" + args.path + ".url")
: resolve("./" + args.path + ".url");
? resolve("./" + file_path + "/../" + importPath + ".url")
: resolve("./" + importPath + ".url");
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, url);
return {
+10 -2
View File
@@ -44,7 +44,9 @@ const p = {
if (
(imp.path.startsWith(".") ||
imp.path.startsWith("/u/") ||
imp.path.startsWith("/f/")) &&
imp.path.startsWith("/f/") ||
imp.path.startsWith("$u/") ||
imp.path.startsWith("$f/")) &&
!imp.path.endsWith(".ts")
) {
code = code.replaceAll(imp.path, imp.path + ".ts");
@@ -67,7 +69,13 @@ const p = {
// Resolve a windmill script import path relative to an importer path.
// Bun on Windows may prefix args with "windmill-url:" or strip leading "/".
function resolveWindmillImport(importerPath, importPath) {
const path = importPath.replace(/^windmill-url:/, "").replace(/^\//, "");
// Strip the "windmill-url:" namespace prefix, an optional leading "/"
// (absolute `/f/`,`/u/`), and the local-friendly "$" alias prefix
// (`$f/`,`$u/`) so absolute workspace imports normalize to `f/`,`u/`.
const path = importPath
.replace(/^windmill-url:/, "")
.replace(/^\//, "")
.replace(/^\$(?=[fu]\/)/, "");
const isAbsolute = path.startsWith("f/") || path.startsWith("u/");
const endExt = path.endsWith(".ts") ? "" : ".ts";
const rawScriptPath = isAbsolute
+8 -1
View File
@@ -170,10 +170,15 @@ pub async fn generate_deno_lock(
let _ = write_file(job_dir, "main.ts", code)?;
let import_map_path = format!("{job_dir}/import_map.json");
// `$f/`/`$u/` are local-friendly aliases for the absolute workspace paths `/f/`,`/u/`.
// The runtime import map (`build_import_map`) maps them too; without these keys here,
// `deno cache --lock` can't resolve `$f/...` imports and lock generation fails.
let import_map = format!(
r#"{{
"imports": {{
"/": "{base_internal_url}/api/scripts_u/empty_ts/"
"/": "{base_internal_url}/api/scripts_u/empty_ts/",
"$f/": "{base_internal_url}/api/scripts_u/empty_ts/f/",
"$u/": "{base_internal_url}/api/scripts_u/empty_ts/u/"
}}
}}"#,
);
@@ -581,6 +586,8 @@ pub(crate) async fn build_import_map(
"{base_internal_url}/api/w/{w_id}/scripts/raw/p/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/",
"{base_internal_url}": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/",
"/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/",
"$f/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/f/",
"$u/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/u/",
"./wrapper.ts": "./wrapper.ts",
"./main.ts": "./main.ts"{relative_mounts}
{extra_import_map}
+2 -2
View File
@@ -29,7 +29,7 @@
"windmill-parser-wasm-regex": "1.692.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.695.0",
"windmill-parser-wasm-ts": "1.714.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
@@ -308,7 +308,7 @@
"windmill-parser-wasm-rust": ["windmill-parser-wasm-rust@1.647.1", "", {}, "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="],
"windmill-parser-wasm-ts": ["windmill-parser-wasm-ts@1.695.0", "", {}, "sha512-9EFxeRZWmfb7EyhSlcG7dzTTKETPRYAvpRlxxLkhhtI5I219wFgI7kwrMpz4stXHJj/aqBknVv66NQHRstSJmw=="],
"windmill-parser-wasm-ts": ["windmill-parser-wasm-ts@1.714.0", "", {}, "sha512-IvkR+tMupzIviST6IahLyNt+feK1A6ZBFUGHWl0NwJQUT0FHdjSk1BmSULSVdwSXTvvp+xcd2WrkB9pBEM5Giw=="],
"windmill-parser-wasm-yaml": ["windmill-parser-wasm-yaml@1.593.0", "", {}, "sha512-Gyx4aR2jsJYuDrD3mCNTmz7LWOQQXPw5yKNCC1xRgUOPfjsD/tINAFfsBLwVOSmlQQcFZO+wHm4KtDtXOcnGVw=="],
+4 -4
View File
@@ -29,7 +29,7 @@
"windmill-parser-wasm-regex": "1.692.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.695.0",
"windmill-parser-wasm-ts": "1.714.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
@@ -1453,9 +1453,9 @@
"integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="
},
"node_modules/windmill-parser-wasm-ts": {
"version": "1.695.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.695.0.tgz",
"integrity": "sha512-9EFxeRZWmfb7EyhSlcG7dzTTKETPRYAvpRlxxLkhhtI5I219wFgI7kwrMpz4stXHJj/aqBknVv66NQHRstSJmw=="
"version": "1.714.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.714.0.tgz",
"integrity": "sha512-IvkR+tMupzIviST6IahLyNt+feK1A6ZBFUGHWl0NwJQUT0FHdjSk1BmSULSVdwSXTvvp+xcd2WrkB9pBEM5Giw=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.593.0",
+1 -1
View File
@@ -38,7 +38,7 @@
"windmill-parser-wasm-regex": "1.692.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.695.0",
"windmill-parser-wasm-ts": "1.714.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
+13
View File
@@ -18,6 +18,7 @@ import {
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;
@@ -238,6 +239,18 @@ async function initAction(opts: InitOptions) {
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 {
+10 -4
View File
@@ -32,7 +32,8 @@ export async function refreshPrompts(opts: {
// If config can't be read, use the conservative default above.
}
const interactive = process.stdin.isTTY && !opts.yes;
const assumeYes = opts.yes === true;
const interactive = process.stdin.isTTY && !assumeYes;
try {
const result = await writeAiGuidanceFiles({
@@ -42,8 +43,13 @@ export async function refreshPrompts(opts: {
agentsSourcePath: process.env[WMILL_INIT_AI_AGENTS_SOURCE_ENV],
claudeSourcePath: process.env[WMILL_INIT_AI_CLAUDE_SOURCE_ENV],
resolveAgentsMdMigration: async () => {
if (!interactive) return "append";
return await promptMigration();
// Consent model (matches `wmill refresh tsconfig`): we only touch an
// existing user-owned file that we don't recognize when the user opts
// in. `--yes` (and `wmill init --default`) appends without asking; an
// interactive run prompts; a plain non-interactive run leaves it alone.
if (assumeYes) return "append";
if (interactive) return await promptMigration();
return "skip";
},
});
@@ -175,7 +181,7 @@ const command = new Command()
.description("Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.")
.option(
"--yes",
"Non-interactive: skip the migration prompt for existing AGENTS.md / CLAUDE.md without the expected include; defaults to appending the include."
"Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched."
)
.action(promptsAction as any);
+6 -2
View File
@@ -1,8 +1,12 @@
import { Command } from "@cliffy/command";
import promptsCommand from "./prompts.ts";
import tsconfigCommand from "./tsconfig.ts";
const command = new Command()
.description("Refresh wmill-managed project files (AGENTS.cli.md and skills)")
.command("prompts", promptsCommand);
.description(
"Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)"
)
.command("prompts", promptsCommand)
.command("tsconfig", tsconfigCommand);
export default command;
+499
View File
@@ -0,0 +1,499 @@
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "../../core/log.ts";
import { readConfigFile } from "../../core/conf.ts";
/**
* Local-friendly aliases for the absolute workspace import paths `/f/...` and
* `/u/...`. Unlike the `/`-prefixed form (which every local tool treats as a
* filesystem-root path), `$f/`/`$u/` are bare specifiers that can be remapped to
* the on-disk `f/`/`u/` folders via tsconfig `paths` and Deno import maps, so the
* same import resolves both on the Windmill worker and in a local editor.
*/
const WORKSPACE_IMPORT_ALIASES: Record<string, string> = {
$f: "f",
$u: "u",
};
// wmill-managed files holding the recommended config. They are always
// (re)written so we can ship updated recommendations over time; users keep
// their own overrides in tsconfig.json / deno.json, which reference these
// managed files and are never overwritten. This mirrors how AGENTS.cli.md
// (managed) and AGENTS.md (user-owned) work for AI prompts.
const MANAGED_TSCONFIG = "tsconfig.wmill.json";
const MANAGED_IMPORT_MAP = "import_map.wmill.json";
const MANAGED_NOTICE =
"// Managed by wmill — regenerated by `wmill init` / `wmill refresh tsconfig`.\n" +
"// Do not edit; put your overrides in tsconfig.json (which extends this file).\n";
// Embedded in tsconfig.wmill.json so any command can detect a stale managed file
// (the recommended config changed) and nudge the user to `wmill refresh tsconfig`
// — mirroring the prompts freshness marker in AGENTS.cli.md.
const TSCONFIG_HASH_PREFIX = "// wmill-tsconfig-hash: ";
const TSCONFIG_HASH_REGEX = /^\/\/ wmill-tsconfig-hash: ([0-9a-f]{12})/m;
/**
* The recommended managed tsconfig, minus environment-dependent bits (`types`
* depends on whether bun-types is installed locally). This is both the source
* of the written file and the input to the freshness hash, so the hash only
* changes when wmill's *recommended* config changes — not when bun-types
* appears/disappears on a given machine.
*/
function buildManagedTsconfig(): {
compilerOptions: Record<string, unknown>;
include: string[];
} {
// Map "$f/*" -> ["./f/*"], "$u/*" -> ["./u/*"] so the editor resolves
// workspace imports against the local script folders.
//
// Known limitation: this resolves imports written with a plain `.ts` extension
// (the canonical form). Scripts stored with a flavor-specific extension —
// `.bun.ts`/`.deno.ts`/`.fetch.ts` for languages other than the project default
// (see filePathExtensionFromContentType) — won't resolve via these aliases in a
// local editor. The worker (extension-agnostic API) and the in-app editor (ATA
// normalizes to `.ts`) handle those fine; only local tsc / VS Code is affected.
const paths: Record<string, string[]> = {};
for (const [alias, dir] of Object.entries(WORKSPACE_IMPORT_ALIASES)) {
paths[`${alias}/*`] = [`./${dir}/*`];
}
return {
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "bundler",
// Workspace imports carry an explicit `.ts` extension (e.g. "$f/foo/bar.ts");
// allow it so the editor doesn't flag every cross-script import.
allowImportingTsExtensions: true,
noEmit: true,
strict: false,
// No `baseUrl`: with moduleResolution "bundler" the `paths` patterns resolve
// relative to this file, and `baseUrl` is deprecated in TypeScript 7+.
paths,
},
include: ["**/*.ts", "rt.d.ts"],
};
}
function currentTsconfigHash(): string {
return createHash("sha256")
.update(JSON.stringify(buildManagedTsconfig()))
.digest("hex")
.slice(0, 12);
}
// Exact `tsconfig.json` shapes the *previous* CLI generated (single-file, before
// the managed/user split — see the now-deleted resource-type/tsconfig.ts). When
// an existing tsconfig.json matches one of these verbatim, we know it's ours (not
// a user customization), so we can safely replace it wholesale with the thin stub
// that extends tsconfig.wmill.json. Anything else is treated as user-authored.
const LEGACY_GENERATED_TSCONFIGS: Record<string, unknown>[] = [
{
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "bundler",
noEmit: true,
strict: false,
},
include: ["**/*.ts", "rt.d.ts"],
},
{
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "bundler",
noEmit: true,
strict: false,
types: ["bun-types"],
},
include: ["**/*.ts", "rt.d.ts"],
},
];
// Order-sensitive deep equality (arrays compared positionally, objects by key
// set). Used only on small parsed JSON config objects.
function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (Array.isArray(a) || Array.isArray(b)) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
return false;
}
return a.every((x, i) => deepEqual(x, b[i]));
}
if (a && b && typeof a === "object" && typeof b === "object") {
const ka = Object.keys(a as object);
const kb = Object.keys(b as object);
if (ka.length !== kb.length) return false;
return ka.every((k) =>
deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])
);
}
return false;
}
// How to handle an existing, user-authored config that doesn't yet reference the
// managed file. `assumeYes` (from `--yes` / `wmill init --default`) wires it
// without asking; otherwise we only wire it after an interactive confirmation —
// a non-interactive run with neither leaves the file untouched.
type WireMode = { interactive: boolean; assumeYes: boolean };
/**
* (Re)generate the wmill-managed TypeScript/Deno IDE config so the editor
* resolves `$f/`/`$u/` workspace imports against the local script folders.
*
* Split into a managed base file (always refreshed) and a user-owned file that
* references it. We only ever touch files that are ours: the managed file is
* regenerated, a tsconfig.json still in the previously-generated shape is
* migrated to the new split, and a genuinely custom config is wired only with
* the user's consent (interactive prompt, or `--yes`).
*
* Programmatic entry point reused by `wmill init`; also exposed as
* `wmill refresh tsconfig`.
*/
export async function refreshTsconfig(opts?: { yes?: boolean }): Promise<void> {
let defaultTs: "bun" | "deno" = "bun";
try {
const conf = await readConfigFile({ warnIfMissing: false });
if (conf?.defaultTs === "deno") {
defaultTs = "deno";
}
} catch {
// fall back to bun if wmill.yaml is missing or unreadable
}
const assumeYes = opts?.yes === true;
const mode: WireMode = {
assumeYes,
interactive: !!process.stdin.isTTY && !assumeYes,
};
// tsconfig.json is useful for Bun and general TS tooling regardless of the
// default; the Deno import map is only relevant for Deno-default projects
// (the Deno LSP ignores tsconfig.json).
await refreshManagedTsconfig(defaultTs, mode);
if (defaultTs === "deno") {
await refreshManagedDenoImportMap(mode);
}
}
async function refreshManagedTsconfig(defaultTs: "bun" | "deno", mode: WireMode) {
const managed = buildManagedTsconfig();
// Only reference bun-types if it's actually available; otherwise the IDE
// would flag the missing type definitions. (Excluded from the freshness hash
// since it's environment-, not recommendation-, dependent.)
const bunTypesAvailable =
defaultTs === "bun" ? ensureBunTypesAvailable() : false;
if (bunTypesAvailable) {
managed.compilerOptions.types = ["bun-types"];
}
const header = MANAGED_NOTICE + TSCONFIG_HASH_PREFIX + currentTsconfigHash() + "\n";
writeFileSync(
path.join(process.cwd(), MANAGED_TSCONFIG),
header + JSON.stringify(managed, null, 2) + "\n"
);
log.info(colors.green(`Refreshed ${MANAGED_TSCONFIG}`));
await ensureUserReferencesManaged({
file: "tsconfig.json",
create: { extends: `./${MANAGED_TSCONFIG}` },
legacyFormats: LEGACY_GENERATED_TSCONFIGS,
mode,
wire: (parsed) => {
// TypeScript does NOT merge `compilerOptions.paths` across `extends` — the
// nearest config that defines `paths` wins wholesale. So a config with its
// own `paths` would shadow the managed `$f/`/`$u/` mappings and the aliases
// would silently fail to resolve. We don't touch the user's paths — warn
// and leave it for them to wire manually.
const co = parsed.compilerOptions;
const paths =
co && typeof co === "object" && !Array.isArray(co)
? (co as Record<string, unknown>).paths
: undefined;
if (
paths &&
typeof paths === "object" &&
!Array.isArray(paths) &&
Object.keys(paths).length > 0
) {
return (
"defines its own `compilerOptions.paths` (TS won't merge ours in via " +
'`extends`); add "$f/*": ["./f/*"] and "$u/*": ["./u/*"] to it'
);
}
const ext = parsed.extends;
const managed = `./${MANAGED_TSCONFIG}`;
// Insert the managed config FIRST in `extends` so the user's own base config
// keeps precedence on overlapping compilerOptions (strict/target/module)
// rather than being overridden by our defaults.
if (ext === undefined) {
parsed.extends = managed;
} else if (typeof ext === "string") {
parsed.extends = [managed, ext];
} else if (Array.isArray(ext)) {
ext.unshift(managed);
} else {
return "unexpected `extends` value";
}
return true;
},
token: MANAGED_TSCONFIG,
hint: `"extends": "./${MANAGED_TSCONFIG}"`,
});
}
async function refreshManagedDenoImportMap(mode: WireMode) {
// Import-map prefix keys must end with "/": "$f/" -> "./f/", "$u/" -> "./u/".
const imports: Record<string, string> = {};
for (const [alias, dir] of Object.entries(WORKSPACE_IMPORT_ALIASES)) {
imports[`${alias}/`] = `./${dir}/`;
}
// Deno import maps only allow `imports`/`scopes`, so no comment header here.
writeFileSync(
path.join(process.cwd(), MANAGED_IMPORT_MAP),
JSON.stringify({ imports }, null, 2) + "\n"
);
log.info(colors.green(`Refreshed ${MANAGED_IMPORT_MAP}`));
await ensureUserReferencesManaged({
file: "deno.json",
// Don't write deno.json if the project already uses deno.jsonc — a new
// deno.json would take precedence and shadow the existing config.
altFiles: ["deno.jsonc"],
create: { importMap: `./${MANAGED_IMPORT_MAP}` },
mode,
wire: (parsed) => {
// Deno rejects `imports` + `importMap` together, so we can't auto-wire a
// deno.json that already defines its own imports.
if (parsed.imports !== undefined) {
return "deno.json already defines `imports` (can't also use importMap)";
}
if (
parsed.importMap !== undefined &&
parsed.importMap !== `./${MANAGED_IMPORT_MAP}`
) {
return "deno.json already sets a different `importMap`";
}
parsed.importMap = `./${MANAGED_IMPORT_MAP}`;
return true;
},
token: MANAGED_IMPORT_MAP,
hint: `"importMap": "./${MANAGED_IMPORT_MAP}"`,
});
}
/**
* Ensure a user-owned config file references the wmill-managed file. Mirrors how
* `wmill refresh prompts` wires `@AGENTS.cli.md` into AGENTS.md:
* - missing → create the minimal file (already linked);
* - exists & linked → leave it alone;
* - exists & unlinked → auto-wire it (parse JSON, apply `wire`, write back).
* Falls back to a one-line warning when the file can't be auto-edited safely
* (JSONC comments fail JSON.parse, or the structure already conflicts) — we
* never corrupt a file we can't round-trip.
*/
async function ensureUserReferencesManaged(opts: {
file: string;
// Sibling configs that, if already present, must not be shadowed by writing
// `opts.file` next to them (e.g. an existing deno.jsonc vs a new deno.json).
altFiles?: string[];
create: Record<string, unknown>;
// Mutate the parsed user config to reference the managed file. Returns true
// when wired, or a short reason string when it can't be wired cleanly (→ warn).
wire: (parsed: Record<string, unknown>) => true | string;
// Verbatim shapes a previous CLI generated for this file. A match means the
// file is ours, so it's replaced wholesale (no prompt); anything else is
// treated as user-authored and only wired with consent.
legacyFormats?: Record<string, unknown>[];
mode: WireMode;
token: string;
hint: string;
}) {
// Prefer any existing config (including alternates) over creating a fresh one,
// so we never shadow a config the user already has.
const existing = [opts.file, ...(opts.altFiles ?? [])]
.map((f) => path.join(process.cwd(), f))
.find((p) => existsSync(p));
if (!existing) {
const userPath = path.join(process.cwd(), opts.file);
writeFileSync(userPath, JSON.stringify(opts.create, null, 2) + "\n");
log.info(colors.green(`Created ${opts.file} (references ${opts.token})`));
return;
}
const existingName = path.basename(existing);
let text = "";
try {
text = readFileSync(existing, "utf-8");
} catch {
return;
}
if (text.includes(opts.token)) {
log.info(
colors.gray(`${existingName} already references ${opts.token}, leaving it untouched`)
);
return;
}
// We only ever rewrite a config we can round-trip as JSON. Files with comments
// (JSONC) fail JSON.parse, so we warn instead of corrupting them.
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(text);
} catch {
log.warn(
`${existingName} couldn't be auto-edited (it may contain comments). Add ${opts.hint} ` +
`to pick up wmill's recommended settings (incl. $f//$u/ import aliases).`
);
return;
}
// The file is still exactly what a previous CLI generated → it's ours, so
// migrate it to the new split (replace with the thin stub that extends the
// managed file). No prompt: we're not touching user-authored content.
if (opts.legacyFormats?.some((fmt) => deepEqual(parsed, fmt))) {
writeFileSync(existing, JSON.stringify(opts.create, null, 2) + "\n");
log.info(
colors.green(
`Migrated previously-generated ${existingName} to reference ${opts.token}`
)
);
return;
}
// Custom config. Try wiring a clone so we can report un-wireable cases without
// mutating, then only persist with the user's consent.
const next = JSON.parse(JSON.stringify(parsed)) as Record<string, unknown>;
const wired = opts.wire(next);
if (wired !== true) {
log.warn(
`${existingName}: ${wired}. Add ${opts.hint} manually to pick up wmill's ` +
`recommended settings (incl. $f//$u/ import aliases).`
);
return;
}
const consent = opts.mode.assumeYes
? true
: opts.mode.interactive
? await Confirm.prompt({
message:
`${existingName} isn't linked to wmill's ${opts.token}. Add ${opts.hint}? ` +
`Your settings are preserved (it's inserted first, so your config wins).`,
default: true,
})
: false;
if (!consent) {
log.info(
colors.gray(
`Left ${existingName} unchanged — add ${opts.hint} when ready to enable ` +
`$f//$u/ import aliases (or re-run \`wmill refresh tsconfig\`).`
)
);
return;
}
writeFileSync(existing, JSON.stringify(next, null, 2) + "\n");
log.info(colors.green(`Linked ${existingName}${opts.token}`));
}
function ensureBunTypesAvailable(): boolean {
const cwd = process.cwd();
if (existsSync(path.join(cwd, "node_modules", "bun-types"))) {
return true;
}
try {
execSync("bun --version", { stdio: "ignore" });
} catch {
log.info(
"Install bun (https://bun.sh), run 'bun add -d bun-types', then re-run 'wmill refresh tsconfig' for Bun API autocompletion."
);
return false;
}
try {
log.info(
colors.yellow("Installing bun-types with 'bun add -d bun-types'...")
);
execSync("bun add -d bun-types", { stdio: "inherit" });
log.info(colors.green("Installed bun-types."));
return true;
} catch (e) {
log.warn(
`Failed to install bun-types automatically: ${
e instanceof Error ? e.message : e
}`
);
log.info(
"Run 'bun add -d bun-types' manually, then 'wmill refresh tsconfig', for Bun API autocompletion."
);
return false;
}
}
/**
* One-line, non-blocking warning (to stderr) when the managed config is out of
* date. Mirrors the prompts freshness check (`warnIfPromptsStale`) exactly:
* gated on the *managed* file existing (= the project opted in by running
* init/refresh), and only ever warns that it's **stale** — never about a
* missing or unlinked user tsconfig.json. So a deliberately-unlinked / custom
* setup is never nagged, and a not-yet-initialized project stays silent.
* Gated identically in main.ts so it never fires during `wmill init`/`refresh`.
*/
export async function warnIfTsconfigStale(opts?: { cwd?: string }): Promise<void> {
const cwd = opts?.cwd ?? process.cwd();
const managedPath = path.join(cwd, MANAGED_TSCONFIG);
if (!existsSync(managedPath)) return;
let managedText: string;
try {
managedText = readFileSync(managedPath, "utf-8");
} catch {
return;
}
const match = managedText.match(TSCONFIG_HASH_REGEX);
if (!match) {
emitTsconfigWarning(
`${MANAGED_TSCONFIG} predates versioning. Run \`wmill refresh tsconfig\` to refresh it.`
);
return;
}
if (match[1] !== currentTsconfigHash()) {
emitTsconfigWarning(
`${MANAGED_TSCONFIG} is out of date. Run \`wmill refresh tsconfig\` to refresh.`
);
}
}
// Send to stderr (not log.warn → stdout) so it never contaminates a piped
// command's output, matching the prompts freshness warning.
function emitTsconfigWarning(message: string): void {
process.stderr.write(`${colors.yellow(message)}\n`);
}
const command = new Command()
.description(
"Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)"
)
.option(
"--yes",
"Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically)."
)
.action((async (opts: { yes?: boolean }) => {
await refreshTsconfig({ yes: opts.yes === true });
}) as any);
export default command;
@@ -12,7 +12,6 @@ import {
} from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { generateTsconfigForIde } from "./tsconfig.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
@@ -190,8 +189,6 @@ export async function generateRTNamespace(opts: GlobalOptions) {
"Created rt.d.ts with resource types namespace (RT) for TypeScript."
)
);
await generateTsconfigForIde();
}
const command = new Command()
@@ -1,87 +0,0 @@
import { execSync } from "node:child_process";
import { existsSync, writeFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import * as log from "../../core/log.ts";
import { readConfigFile } from "../../core/conf.ts";
export async function generateTsconfigForIde() {
const tsconfigPath = path.join(process.cwd(), "tsconfig.json");
if (existsSync(tsconfigPath)) {
log.info(colors.gray("tsconfig.json already exists, skipping"));
return;
}
let defaultTs: "bun" | "deno" = "bun";
try {
const conf = await readConfigFile({ warnIfMissing: false });
if (conf?.defaultTs === "deno") {
defaultTs = "deno";
}
} catch {
// fall back to bun if wmill.yaml is missing or unreadable
}
// Only reference bun-types in tsconfig if it's actually available; otherwise
// the IDE will flag the missing type definitions.
const bunTypesAvailable =
defaultTs === "bun" ? ensureBunTypesAvailable() : false;
const tsconfig: {
compilerOptions: Record<string, unknown>;
include: string[];
} = {
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "bundler",
noEmit: true,
strict: false,
},
include: ["**/*.ts", "rt.d.ts"],
};
if (bunTypesAvailable) {
tsconfig.compilerOptions.types = ["bun-types"];
}
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
log.info(colors.green("Created tsconfig.json for IDE type support."));
}
function ensureBunTypesAvailable(): boolean {
const cwd = process.cwd();
if (existsSync(path.join(cwd, "node_modules", "bun-types"))) {
return true;
}
try {
execSync("bun --version", { stdio: "ignore" });
} catch {
log.info(
"Install bun (https://bun.sh) then run 'bun add -d bun-types' and add \"types\": [\"bun-types\"] to tsconfig.json for Bun API autocompletion."
);
return false;
}
try {
log.info(
colors.yellow("Installing bun-types with 'bun add -d bun-types'...")
);
execSync("bun add -d bun-types", { stdio: "inherit" });
log.info(colors.green("Installed bun-types."));
return true;
} catch (e) {
log.warn(
`Failed to install bun-types automatically: ${
e instanceof Error ? e.message : e
}`
);
log.info(
"Run 'bun add -d bun-types' manually and add \"types\": [\"bun-types\"] to tsconfig.json for Bun API autocompletion."
);
return false;
}
}
+4 -2
View File
@@ -6455,12 +6455,14 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md and skills)
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
**Subcommands:**
- \`refresh prompts\` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: skip the migration prompt for existing AGENTS.md / CLAUDE.md without the expected include; defaults to appending the include.
- \`--yes\` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh tsconfig\` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- \`--yes\` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
### resource
+4
View File
@@ -304,6 +304,10 @@ async function main() {
if (shouldRunFreshnessCheck(process.argv)) {
const { warnIfPromptsStale } = await import("./guidance/freshness.ts");
await warnIfPromptsStale({ argv: process.argv }).catch(() => {});
const { warnIfTsconfigStale } = await import(
"./commands/refresh/tsconfig.ts"
);
await warnIfTsconfigStale().catch(() => {});
}
await command.parse(args);
+11 -5
View File
@@ -131,12 +131,18 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => {
if (depth == 0) {
const relativeDeps = depsToGet.filter((f) => isTypescriptRelativePath(f.raw))
relativeDeps.forEach(async (f) => {
let path = f.raw.startsWith('/')
? f.raw
: '/' + config.scriptPath + (f.raw.startsWith('../') ? '/../' : '/.') + f.raw
// `$f/`/`$u/` are local-friendly aliases for the absolute workspace paths
// `/f/`,`/u/`. Normalize to the absolute form so the fetch URL and the
// registered extra-lib path match what the worker resolves (the editor's
// `paths` config maps `$f/` -> `/f/` back to this lib).
const raw =
f.raw.startsWith('$f/') || f.raw.startsWith('$u/') ? '/' + f.raw.slice(1) : f.raw
let path = raw.startsWith('/')
? raw
: '/' + config.scriptPath + (raw.startsWith('../') ? '/../' : '/.') + raw
let url = config.root + path
let localPath = f.raw
if ((f.raw.startsWith('.') || f.raw.startsWith('/')) && !f.raw.endsWith('.ts')) {
let localPath = raw
if ((raw.startsWith('.') || raw.startsWith('/')) && !raw.endsWith('.ts')) {
url += '.ts'
localPath += '.ts'
}
@@ -100,6 +100,14 @@ export function setMonacoTypescriptOptions() {
allowImportingTsExtensions: true,
allowSyntheticDefaultImports: true,
moduleResolution: ModuleResolutionKind.NodeJs,
// `$f/`/`$u/` are local-friendly aliases for the absolute workspace paths
// `/f/`,`/u/`. ATA registers imported scripts as extra libs under their
// absolute `/f/...` path, so map the aliases back so type acquisition resolves.
baseUrl: '/',
paths: {
'$f/*': ['/f/*'],
'$u/*': ['/u/*']
},
jsx: JsxEmit.React
})
}
+3 -1
View File
@@ -16,7 +16,9 @@ export function isTypescriptRelativePath(d: string) {
d.startsWith('../') ||
d.startsWith('/') ||
d.startsWith('.../') ||
d.startsWith('/')
// `$f/`/`$u/` are local-friendly aliases for the absolute workspace paths `/f/`,`/u/`.
d.startsWith('$f/') ||
d.startsWith('$u/')
)
}
@@ -430,12 +430,14 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md and skills)
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
**Subcommands:**
- `refresh prompts` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: skip the migration prompt for existing AGENTS.md / CLAUDE.md without the expected include; defaults to appending the include.
- `--yes` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh tsconfig` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- `--yes` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
### resource
+4 -2
View File
@@ -2980,12 +2980,14 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md and skills)
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
**Subcommands:**
- \`refresh prompts\` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- \`--yes\` - Non-interactive: skip the migration prompt for existing AGENTS.md / CLAUDE.md without the expected include; defaults to appending the include.
- \`--yes\` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh tsconfig\` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- \`--yes\` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
### resource
@@ -435,12 +435,14 @@ List all queues with their metrics
### refresh
Refresh wmill-managed project files (AGENTS.cli.md and skills)
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
**Subcommands:**
- `refresh prompts` - Refresh AGENTS.cli.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
- `--yes` - Non-interactive: skip the migration prompt for existing AGENTS.md / CLAUDE.md without the expected include; defaults to appending the include.
- `--yes` - Non-interactive: append the @AGENTS.cli.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- `refresh tsconfig` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
- `--yes` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).
### resource