mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
feat(cli): improve agent prompts/skills and workspace fork workflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ce6e2f7ade
commit
1c744d2809
@@ -1,3 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import process from "node:process";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
@@ -53,11 +56,19 @@ export async function refreshPrompts(opts: {
|
||||
},
|
||||
});
|
||||
|
||||
log.info(colors.green("Refreshed AGENTS.cli.md"));
|
||||
log.info(colors.green("Refreshed AGENTS.wmill.md"));
|
||||
|
||||
if (result.legacyManagedRemoved) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
"Migrated legacy AGENTS.cli.md → AGENTS.wmill.md (removed the old file and rewrote any @AGENTS.cli.md include)."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
reportReconciliation({
|
||||
file: "AGENTS.md",
|
||||
includeLine: "@AGENTS.cli.md",
|
||||
includeLine: "@AGENTS.wmill.md",
|
||||
created: result.agentsCreated,
|
||||
migration: result.agentsMigration,
|
||||
});
|
||||
@@ -175,13 +186,44 @@ interface CommandOptions {
|
||||
|
||||
async function promptsAction(opts: CommandOptions): Promise<void> {
|
||||
await refreshPrompts({ yes: opts.yes === true });
|
||||
await refreshRtNamespaceIfPresent(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep an existing `rt.d.ts` resource-type namespace in sync when the user
|
||||
* runs `wmill refresh prompts`. `wmill init` already generates it on first
|
||||
* bind; here we only refresh it when the file is already present (so we never
|
||||
* introduce it into projects that don't use it). Best-effort: a missing
|
||||
* workspace/login or offline run just skips with a warning rather than failing
|
||||
* the whole refresh.
|
||||
*
|
||||
* Not wired into the shared `refreshPrompts` helper on purpose — `wmill init`
|
||||
* regenerates the namespace itself, and `refreshPrompts` must stay
|
||||
* network-free for its other callers.
|
||||
*/
|
||||
async function refreshRtNamespaceIfPresent(opts: CommandOptions): Promise<void> {
|
||||
const rtPath = join(process.cwd(), "rt.d.ts");
|
||||
if (!existsSync(rtPath)) return;
|
||||
|
||||
try {
|
||||
const { generateRTNamespace } = await import(
|
||||
"../resource-type/resource-type.ts"
|
||||
);
|
||||
await generateRTNamespace(opts as any);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`Could not refresh rt.d.ts resource type namespace: ${
|
||||
error instanceof Error ? error.message : error
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
.description("Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.")
|
||||
.option(
|
||||
"--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."
|
||||
"Non-interactive: append the @AGENTS.wmill.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);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import tsconfigCommand from "./tsconfig.ts";
|
||||
|
||||
const command = new Command()
|
||||
.description(
|
||||
"Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)"
|
||||
"Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)"
|
||||
)
|
||||
.command("prompts", promptsCommand)
|
||||
.command("tsconfig", tsconfigCommand);
|
||||
|
||||
@@ -22,7 +22,7 @@ const WORKSPACE_IMPORT_DIRS = ["f", "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 files and are never overwritten. This mirrors how AGENTS.wmill.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";
|
||||
@@ -33,7 +33,7 @@ const MANAGED_NOTICE =
|
||||
|
||||
// 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.
|
||||
// — mirroring the prompts freshness marker in AGENTS.wmill.md.
|
||||
const TSCONFIG_HASH_PREFIX = "// wmill-tsconfig-hash: ";
|
||||
const TSCONFIG_HASH_REGEX = /^\/\/ wmill-tsconfig-hash: ([0-9a-f]{12})/m;
|
||||
|
||||
@@ -291,7 +291,7 @@ async function refreshManagedDenoImportMap(mode: WireMode) {
|
||||
|
||||
/**
|
||||
* Ensure a user-owned config file references the wmill-managed file. Mirrors how
|
||||
* `wmill refresh prompts` wires `@AGENTS.cli.md` into AGENTS.md:
|
||||
* `wmill refresh prompts` wires `@AGENTS.wmill.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).
|
||||
|
||||
@@ -5,15 +5,23 @@ import * as log from "../../core/log.ts";
|
||||
import { setClient } from "../../core/client.ts";
|
||||
import { allWorkspaces, list, removeWorkspace } from "./workspace.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, isGitRepository } from "../../utils/git.ts";
|
||||
import {
|
||||
getCurrentGitBranch,
|
||||
getOriginalBranchForWorkspaceForks,
|
||||
gitBranchExists,
|
||||
isGitRepository,
|
||||
renameCurrentGitBranch,
|
||||
} from "../../utils/git.ts";
|
||||
import { WM_FORK_PREFIX } from "../../core/constants.ts";
|
||||
import { tryResolveBranchWorkspace } from "../../core/context.ts";
|
||||
import { findWorkspaceByGitBranch, readConfigFile } from "../../core/conf.ts";
|
||||
|
||||
async function createWorkspaceFork(
|
||||
opts: GlobalOptions & {
|
||||
createWorkspaceName: string | undefined;
|
||||
color: string | undefined;
|
||||
datatableBehavior: string | undefined;
|
||||
fromBranch: string | undefined;
|
||||
yes: boolean | undefined;
|
||||
},
|
||||
workspaceName: string | undefined,
|
||||
@@ -23,7 +31,37 @@ async function createWorkspaceFork(
|
||||
throw new Error("You can only create forks within a git repo. Forks are tracked with git and synced to your instance with the git sync workflow.");
|
||||
}
|
||||
|
||||
const workspace = await tryResolveBranchWorkspace(opts);
|
||||
const currentBranch = getCurrentGitBranch()
|
||||
if (!currentBranch) {
|
||||
throw new Error("Could not get git branch name");
|
||||
}
|
||||
|
||||
// `--from-branch` enables the "already on a working branch" workflow: the
|
||||
// fork is based on <fromBranch> (the parent), and the current branch is
|
||||
// later renamed onto the fork branch. Without it, the fork is based on the
|
||||
// current branch and the user checks out a fresh fork branch.
|
||||
const fromBranch = opts.fromBranch;
|
||||
|
||||
let workspace;
|
||||
if (fromBranch) {
|
||||
if (fromBranch === currentBranch) {
|
||||
throw new Error(
|
||||
`--from-branch is for converting a *different* working branch into the fork branch, but you are already on \`${currentBranch}\`. ` +
|
||||
`Either omit --from-branch (a fresh fork branch is created with \`git checkout -b\`), or check out the working branch you want to convert first.`,
|
||||
);
|
||||
}
|
||||
const config = await readConfigFile({ warnIfMissing: false });
|
||||
const match = findWorkspaceByGitBranch(config.workspaces, fromBranch);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Could not find a workspace mapped to branch \`${fromBranch}\` in wmill.yaml's workspaces section. ` +
|
||||
`Pass the base branch your fork should be based on (e.g. the branch bound to the parent workspace).`,
|
||||
);
|
||||
}
|
||||
workspace = await tryResolveBranchWorkspace(opts, match[0]);
|
||||
} else {
|
||||
workspace = await tryResolveBranchWorkspace(opts);
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("Could not resolve workspace from branch name. Make sure you are in a git repo to use workspace forks");
|
||||
@@ -31,14 +69,12 @@ async function createWorkspaceFork(
|
||||
|
||||
log.info(`You are forking workspace (${workspace.workspaceId})`)
|
||||
|
||||
const currentBranch = getCurrentGitBranch()
|
||||
if (!currentBranch) {
|
||||
throw new Error("Could not get git branch name");
|
||||
}
|
||||
const originalBranchIfForked = getOriginalBranchForWorkspaceForks(currentBranch);
|
||||
|
||||
let clonedBranchName: string | null;
|
||||
if (originalBranchIfForked) {
|
||||
if (fromBranch) {
|
||||
clonedBranchName = fromBranch;
|
||||
} else if (originalBranchIfForked) {
|
||||
log.info(`You are creating a fork of a fork. The branch will be linked to the original branch this was forked from, i.e. \`${originalBranchIfForked}\`, for all settings and overrides.`);
|
||||
clonedBranchName = originalBranchIfForked;
|
||||
} else {
|
||||
@@ -248,9 +284,51 @@ async function createWorkspaceFork(
|
||||
|
||||
const newBranchName = `${WM_FORK_PREFIX}/${clonedBranchName}/${workspaceId}`
|
||||
|
||||
log.info(`Created forked workspace ${trueWorkspaceId}. To start contributing to your fork, create and push edits to the branch \`${newBranchName}\` by using the command:
|
||||
// Workflow B (`--from-branch`): turn the current working branch into the
|
||||
// fork branch in place so its commits become the fork's. Workflow A: leave
|
||||
// the user on their base branch and have them check out a fresh fork branch.
|
||||
let onForkBranch = false;
|
||||
if (fromBranch) {
|
||||
if (currentBranch === newBranchName) {
|
||||
onForkBranch = true;
|
||||
log.info(colors.green(`Your current branch is already \`${newBranchName}\`.`));
|
||||
} else if (gitBranchExists(newBranchName)) {
|
||||
log.warn(
|
||||
`Branch \`${newBranchName}\` already exists locally, so the current branch \`${currentBranch}\` was not renamed. ` +
|
||||
`Check out the fork branch yourself (e.g. \`git checkout ${newBranchName}\`).`,
|
||||
);
|
||||
} else {
|
||||
let doRename = opts.yes === true;
|
||||
if (!doRename) {
|
||||
const { Select } = await import("@cliffy/prompt/select");
|
||||
const choice = await Select.prompt({
|
||||
message: `Rename your current branch \`${currentBranch}\` → \`${newBranchName}\` so its commits become the fork's branch?`,
|
||||
options: [
|
||||
{ name: "Yes, rename it", value: "confirm" },
|
||||
{ name: "No, I'll switch branches myself", value: "cancel" },
|
||||
],
|
||||
});
|
||||
doRename = choice === "confirm";
|
||||
}
|
||||
if (doRename) {
|
||||
renameCurrentGitBranch(newBranchName);
|
||||
onForkBranch = true;
|
||||
log.info(
|
||||
colors.green(
|
||||
`Renamed \`${currentBranch}\` → \`${newBranchName}\`. Your existing commits are now on the fork branch.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\t`+colors.white(`git checkout -b ${newBranchName}`) + `
|
||||
const checkoutHint = onForkBranch
|
||||
? `Created forked workspace ${trueWorkspaceId}. You are on the fork branch \`${newBranchName}\` — push it to sync your fork.`
|
||||
: `Created forked workspace ${trueWorkspaceId}. To start contributing to your fork, create and push edits to the branch \`${newBranchName}\` by using the command:
|
||||
|
||||
\t` + colors.white(`git checkout -b ${newBranchName}`);
|
||||
|
||||
log.info(`${checkoutHint}
|
||||
|
||||
When doing operations on the forked workspace, it will use the remote setup in the workspaces section for the branch it was forked from.
|
||||
|
||||
|
||||
@@ -812,7 +812,11 @@ const command = new Command()
|
||||
"--datatable-behavior <behavior:string>",
|
||||
"How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)"
|
||||
)
|
||||
.option("-y --yes", "Skip interactive prompts (defaults datatable behavior to 'skip')")
|
||||
.option(
|
||||
"--from-branch <branch:string>",
|
||||
"Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead."
|
||||
)
|
||||
.option("-y --yes", "Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)")
|
||||
.action(createWorkspaceFork as any)
|
||||
.command("delete-fork")
|
||||
.description("Delete a forked workspace and git branch")
|
||||
|
||||
@@ -3,18 +3,28 @@
|
||||
*
|
||||
* `wmill` writes two files:
|
||||
*
|
||||
* - `AGENTS.cli.md` — managed CLI / workspace guidance, refreshed by
|
||||
* - `AGENTS.wmill.md` — managed CLI / workspace guidance, refreshed by
|
||||
* `wmill refresh prompts` (and the implicit refresh inside `wmill init`).
|
||||
* - `AGENTS.md` — user-owned project entry point. The default skeleton
|
||||
* references `AGENTS.cli.md` via an `@`-include so the managed content is
|
||||
* references `AGENTS.wmill.md` via an `@`-include so the managed content is
|
||||
* pulled in automatically.
|
||||
*
|
||||
* The managed file used to be named `AGENTS.cli.md`; `wmill init` /
|
||||
* `wmill refresh prompts` migrate the old name to `AGENTS.wmill.md` (and
|
||||
* rewrite the `@`-include) automatically. The legacy constants below exist
|
||||
* solely for that migration.
|
||||
*/
|
||||
|
||||
export const AGENTS_CLI_INCLUDE_LINE = "@AGENTS.cli.md";
|
||||
export const AGENTS_WMILL_FILENAME = "AGENTS.wmill.md";
|
||||
export const AGENTS_WMILL_INCLUDE_LINE = "@AGENTS.wmill.md";
|
||||
|
||||
/** Legacy managed filename / include line, migrated away from on init/refresh. */
|
||||
export const LEGACY_AGENTS_CLI_FILENAME = "AGENTS.cli.md";
|
||||
export const LEGACY_AGENTS_CLI_INCLUDE_LINE = "@AGENTS.cli.md";
|
||||
|
||||
/**
|
||||
* Lightweight, user-owned AGENTS.md skeleton. Written only when no AGENTS.md
|
||||
* exists in the project. Everything below the `@AGENTS.cli.md` include is for
|
||||
* exists in the project. Everything below the `@AGENTS.wmill.md` include is for
|
||||
* the user to edit; nothing in this file is refreshed by `wmill`.
|
||||
*/
|
||||
export function generateAgentsMdSkeleton(): string {
|
||||
@@ -28,7 +38,7 @@ The line below pulls in Windmill's managed CLI guidance (skills, deploy flow,
|
||||
debugging jobs, etc.). Refresh it with \`wmill refresh prompts\`. Remove the
|
||||
include line if you don't want the managed guidance in this project.
|
||||
|
||||
${AGENTS_CLI_INCLUDE_LINE}
|
||||
${AGENTS_WMILL_INCLUDE_LINE}
|
||||
|
||||
## Project-specific instructions
|
||||
|
||||
@@ -41,8 +51,12 @@ ${AGENTS_CLI_INCLUDE_LINE}
|
||||
}
|
||||
|
||||
/**
|
||||
* Managed AGENTS.cli.md content. Rewritten by `wmill init` and
|
||||
* Managed AGENTS.wmill.md content. Rewritten by `wmill init` and
|
||||
* `wmill refresh prompts` every time.
|
||||
*
|
||||
* NOTE: `system_prompts/generate.py` extracts this template by anchoring on
|
||||
* the function name `generateAgentsCliMdContent` — keep the name in sync if
|
||||
* you rename it.
|
||||
*/
|
||||
export function generateAgentsCliMdContent(skillsReference: string): string {
|
||||
return `# Windmill CLI Agent Instructions
|
||||
@@ -112,10 +126,11 @@ There are two ways local changes reach the workspace. Pick based on how the repo
|
||||
Before deploying, check whether this repo has a **GitHub Actions (or other CI) workflow that runs \`wmill sync push\` on push**. That workflow is the signal that pushing a branch will deploy:
|
||||
|
||||
- Look for \`.github/workflows/*.yml\` (or other CI configs) that invoke \`wmill sync push\`, \`wmill\` deployment commands, or similar.
|
||||
- Cache the result for the rest of the session — don't re-scan on every deploy.
|
||||
|
||||
If such a workflow exists → **use \`git push\`** (Option A). Otherwise → **use \`wmill sync push\`** directly (Option B).
|
||||
|
||||
**Save the preference so you don't re-detect it every session.** Once you've determined which option this repo uses (or the user tells you), record it in the **project-specific instructions** section of \`AGENTS.md\` (user-owned — never overwritten by \`wmill refresh prompts\`), e.g. a line like \`Deploy mode: git push (CI runs wmill sync push)\` or \`Deploy mode: wmill sync push (no CI wiring)\`. On later sessions, read that line first and skip the scan. Re-detect only if the CI wiring visibly changed.
|
||||
|
||||
### Option A — \`git push\` (CI is wired to sync)
|
||||
|
||||
The CI workflow will pick up the commit and run \`wmill sync push\` on the backend, which is how deployments are intended to happen in this repo. Don't bypass it.
|
||||
@@ -137,6 +152,17 @@ No CI workflow runs \`wmill sync push\` automatically, so deploy directly from t
|
||||
|
||||
Only deploy when the user explicitly asks to deploy, publish, push, or ship — not when they say "run", "try", or "test". For testing local edits use the per-entity \`preview\` commands (\`wmill script preview\`, \`wmill flow preview\`) — they don't deploy.
|
||||
|
||||
## Workspace forks
|
||||
|
||||
A **fork** is an isolated copy of a workspace for parallel or experimental work — make changes (including to datatables, which are cloned per fork) without touching the parent, then merge back after review. Each fork is paired with a git branch named \`wm-fork/<base>/<id>\`. Forks require a git repo.
|
||||
|
||||
Create one with \`wmill workspace fork\`. There are two branch workflows — pick by where you are:
|
||||
|
||||
- **Starting from the base branch** (no in-progress work to carry over): run \`wmill workspace fork\`. It bases the fork on your current branch and prints a \`git checkout -b wm-fork/<base>/<id>\` to start the fork branch.
|
||||
- **Already on a working branch you want to turn into the fork** (e.g. you've branched and already edited a forked datatable): run \`wmill workspace fork --from-branch <base>\`. It bases the fork on \`<base>\` (the parent's branch) and renames your current branch onto \`wm-fork/<base>/<id>\` in place, preserving its commits.
|
||||
|
||||
Merge a fork back into its parent with \`wmill workspace merge\` (or the Merge UI on the fork's home page). Full reference: https://www.windmill.dev/docs/advanced/workspace_forks
|
||||
|
||||
## Debugging Jobs
|
||||
|
||||
When the user reports a script or flow failure, is investigating unexpected output, or asks why something ran the way it did, use the CLI to fetch job details before speculating. See the \`cli-commands\` skill for all flags.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Versioning + freshness check for the managed AGENTS.cli.md bundle.
|
||||
* Versioning + freshness check for the managed AGENTS.wmill.md bundle.
|
||||
*
|
||||
* We embed a short hash of "what this CLI would write" into AGENTS.cli.md as
|
||||
* We embed a short hash of "what this CLI would write" into AGENTS.wmill.md as
|
||||
* an HTML comment. On every `wmill` command (with a few exceptions), we read
|
||||
* the stored hash and compare against the current CLI's hash. Mismatch =>
|
||||
* one-line warning telling the user to `wmill refresh prompts`.
|
||||
*
|
||||
* The hash covers all inputs that affect the rendered bundle: the
|
||||
* AGENTS.cli.md template, every skill body, schemas and schema mappings, and
|
||||
* AGENTS.wmill.md template, every skill body, schemas and schema mappings, and
|
||||
* the nonDottedPaths setting. It is *not* tied to the CLI's package version,
|
||||
* so non-prompt CLI releases don't produce false positives.
|
||||
*/
|
||||
@@ -15,7 +15,11 @@ import { createHash } from "node:crypto";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { readTextFile } from "../utils/utils.ts";
|
||||
import { generateAgentsCliMdContent } from "./core.ts";
|
||||
import {
|
||||
AGENTS_WMILL_FILENAME,
|
||||
LEGACY_AGENTS_CLI_FILENAME,
|
||||
generateAgentsCliMdContent,
|
||||
} from "./core.ts";
|
||||
import {
|
||||
SCHEMAS,
|
||||
SCHEMA_MAPPINGS,
|
||||
@@ -43,7 +47,7 @@ export function extractPromptsHash(content: string): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the hash marker into rendered AGENTS.cli.md content. The marker
|
||||
* Insert the hash marker into rendered AGENTS.wmill.md content. The marker
|
||||
* goes on the line right after the title so it's easy to find and doesn't
|
||||
* break the rendered Markdown structure.
|
||||
*/
|
||||
@@ -73,7 +77,7 @@ export function currentPromptsHash(nonDottedPaths: boolean): string {
|
||||
hasher.update(generateAgentsCliMdContent("__PLACEHOLDER__"));
|
||||
|
||||
// Skill metadata (names + descriptions) — fed into the skills reference
|
||||
// line in AGENTS.cli.md and the wrapper frontmatter.
|
||||
// line in AGENTS.wmill.md and the wrapper frontmatter.
|
||||
hasher.update("\nskills:");
|
||||
hasher.update(JSON.stringify(SKILLS));
|
||||
|
||||
@@ -101,10 +105,14 @@ export function currentPromptsHash(nonDottedPaths: boolean): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read AGENTS.cli.md in the current working directory, compare its embedded
|
||||
* Read AGENTS.wmill.md in the current working directory, compare its embedded
|
||||
* hash to the current CLI's hash, and print a one-line warning if they
|
||||
* differ. Silent on every other code path (no AGENTS.cli.md, no marker,
|
||||
* differ. Silent on every other code path (no managed file, no marker,
|
||||
* matching hash, IO error, …) so it never gets in the user's way.
|
||||
*
|
||||
* Back-compat: if only the legacy `AGENTS.cli.md` is present (no
|
||||
* `AGENTS.wmill.md` yet), warn that the managed file should be migrated —
|
||||
* `wmill refresh prompts` renames it and rewrites the include.
|
||||
*/
|
||||
export async function warnIfPromptsStale(opts?: {
|
||||
cwd?: string;
|
||||
@@ -114,9 +122,19 @@ export async function warnIfPromptsStale(opts?: {
|
||||
if (opts?.argv && !shouldRunFreshnessCheck(opts.argv)) return;
|
||||
|
||||
const cwd = opts?.cwd ?? process.cwd();
|
||||
const path = `${cwd}/AGENTS.cli.md`;
|
||||
const path = `${cwd}/${AGENTS_WMILL_FILENAME}`;
|
||||
|
||||
if (!(await stat(path).catch(() => null))) return;
|
||||
if (!(await stat(path).catch(() => null))) {
|
||||
// No AGENTS.wmill.md. If the legacy AGENTS.cli.md is still around, nudge
|
||||
// the user to migrate it; otherwise this project just isn't wmill-managed.
|
||||
const legacyPath = `${cwd}/${LEGACY_AGENTS_CLI_FILENAME}`;
|
||||
if (await stat(legacyPath).catch(() => null)) {
|
||||
emitWarning(
|
||||
"Your AGENTS.cli.md is using the old managed filename. Run `wmill refresh prompts` to migrate it to AGENTS.wmill.md."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -127,10 +145,10 @@ export async function warnIfPromptsStale(opts?: {
|
||||
|
||||
const stored = extractPromptsHash(content);
|
||||
if (!stored) {
|
||||
// Older AGENTS.cli.md without a marker. Warn so the user re-runs
|
||||
// Older AGENTS.wmill.md without a marker. Warn so the user re-runs
|
||||
// refresh and picks up the new format.
|
||||
emitWarning(
|
||||
"Your AGENTS.cli.md predates prompt versioning. Run `wmill refresh prompts` to refresh and add a version marker."
|
||||
"Your AGENTS.wmill.md predates prompt versioning. Run `wmill refresh prompts` to refresh and add a version marker."
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +170,7 @@ export async function warnIfPromptsStale(opts?: {
|
||||
const current = currentPromptsHash(nonDottedPaths);
|
||||
if (stored !== current) {
|
||||
emitWarning(
|
||||
"Your AGENTS.cli.md is out of date. Run `wmill refresh prompts` to refresh."
|
||||
"Your AGENTS.wmill.md is out of date. Run `wmill refresh prompts` to refresh."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +291,10 @@ import Stripe from "stripe";
|
||||
import { someFunction } from "some-package";
|
||||
\`\`\`
|
||||
|
||||
## Prefer \`//native\` when the runtime allows it
|
||||
|
||||
If a script only needs \`fetch\` and the JavaScript standard library — including when it uses \`windmill-client\` — prefer making it a **native** script: add \`//native\` as the first line and write it with the \`write-script-bunnative\` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. \`windmill-client\` works on the native worker (its calls go over \`fetch\`), so needing the Windmill client is **not** a reason to avoid \`//native\`. Use the regular \`bun\` language only when the code (or a dependency) needs Node/Bun runtime APIs — \`node:*\` modules, the filesystem, child processes, or native addons.
|
||||
|
||||
## Windmill Client
|
||||
|
||||
Import the windmill client for platform interactions:
|
||||
@@ -299,7 +303,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
\`\`\`
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to \`fetch\`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -1012,7 +1018,9 @@ export async function main(url: string) {
|
||||
|
||||
## Windmill Client
|
||||
|
||||
\`windmill-client\` is available for Windmill-specific primitives such as the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). Use \`fetch\` for plain HTTP.
|
||||
\`windmill-client\` works on the native worker (its calls go over \`fetch\`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). It handles auth, the workspace, and the base URL for you. Reserve raw \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a \`fetch\` against the Windmill API.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -1813,7 +1821,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
\`\`\`
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to \`fetch\`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -4494,7 +4504,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (\`wmill flow preview\` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (\`wmill sync push\`, \`wmill generate-metadata\`) so the user can approve them. The options:
|
||||
|
||||
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files. Add \`--step <step_id>\` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- \`wmill flow run <path>\` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
@@ -4960,7 +4970,7 @@ The runnable ID is the filename without extension. For example, \`get_user.ts\`
|
||||
| C# | \`.cs\` | \`myFunc.cs\` |
|
||||
| Java | \`.java\` | \`myFunc.java\` |
|
||||
|
||||
After creating a runnable, tell the user they can generate lock files by running:
|
||||
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
|
||||
\`\`\`bash
|
||||
wmill generate-metadata
|
||||
\`\`\`
|
||||
@@ -6456,12 +6466,12 @@ List all queues with their metrics
|
||||
|
||||
### refresh
|
||||
|
||||
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
|
||||
Refresh wmill-managed project files (AGENTS.wmill.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: 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 prompts\` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
|
||||
- \`--yes\` - Non-interactive: append the @AGENTS.wmill.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).
|
||||
|
||||
@@ -6752,7 +6762,8 @@ workspace related commands
|
||||
- \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
|
||||
- \`--color <color:string>\` - Workspace color (hex code, e.g. #ff0000)
|
||||
- \`--datatable-behavior <behavior:string>\` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip')
|
||||
- \`--from-branch <branch:string>\` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
|
||||
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace and git branch
|
||||
- \`-y --yes\` - Skip confirmation prompt
|
||||
- \`workspace merge\` - Compare and deploy changes between a fork and its parent workspace
|
||||
|
||||
+78
-12
@@ -1,8 +1,11 @@
|
||||
import { cp, mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import { cp, mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { readTextFile } from "../utils/utils.ts";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
AGENTS_CLI_INCLUDE_LINE,
|
||||
AGENTS_WMILL_FILENAME,
|
||||
AGENTS_WMILL_INCLUDE_LINE,
|
||||
LEGACY_AGENTS_CLI_FILENAME,
|
||||
LEGACY_AGENTS_CLI_INCLUDE_LINE,
|
||||
generateAgentsCliMdContent,
|
||||
generateAgentsMdSkeleton,
|
||||
} from "./core.ts";
|
||||
@@ -25,7 +28,7 @@ type ResolvedSkillMetadata = SkillMetadata & {
|
||||
/**
|
||||
* How to reconcile an existing user-owned guidance file (AGENTS.md or
|
||||
* CLAUDE.md) that doesn't reference the managed file below it
|
||||
* (`@AGENTS.cli.md` for AGENTS.md, `@AGENTS.md` for CLAUDE.md).
|
||||
* (`@AGENTS.wmill.md` for AGENTS.md, `@AGENTS.md` for CLAUDE.md).
|
||||
*
|
||||
* - `append`: leave the file as-is and append the include line.
|
||||
* - `overwrite`: replace the file with the managed skeleton.
|
||||
@@ -45,13 +48,13 @@ export interface WriteAiGuidanceOptions {
|
||||
nonDottedPaths?: boolean;
|
||||
/** Skill source override (testing / source-of-truth bundling). */
|
||||
skillsSourcePath?: string;
|
||||
/** AGENTS.cli.md source override (testing). */
|
||||
/** AGENTS.wmill.md source override (testing). */
|
||||
agentsSourcePath?: string;
|
||||
/** CLAUDE.md source override (testing). */
|
||||
claudeSourcePath?: string;
|
||||
/**
|
||||
* Optional resolver invoked when an existing AGENTS.md lacks an
|
||||
* `@AGENTS.cli.md` reference. Callers are expected to prompt the user; if
|
||||
* `@AGENTS.wmill.md` reference. Callers are expected to prompt the user; if
|
||||
* omitted, the writer defaults to `append` (non-destructive).
|
||||
*/
|
||||
resolveAgentsMdMigration?: () => Promise<AgentsMdMigration>;
|
||||
@@ -64,6 +67,12 @@ export interface WriteAiGuidanceResult {
|
||||
claudeCreated: boolean;
|
||||
claudeMigration: ReconcileOutcome;
|
||||
skillCount: number;
|
||||
/**
|
||||
* True when a legacy `AGENTS.cli.md` was found and removed (its content is
|
||||
* superseded by `AGENTS.wmill.md`, and any `@AGENTS.cli.md` includes were
|
||||
* rewritten to `@AGENTS.wmill.md`).
|
||||
*/
|
||||
legacyManagedRemoved: boolean;
|
||||
}
|
||||
|
||||
export const WMILL_INIT_AI_SKILLS_SOURCE_ENV = "WMILL_INIT_AI_SKILLS_SOURCE";
|
||||
@@ -94,7 +103,7 @@ export async function writeAiGuidanceFiles(
|
||||
? await readSkillMetadataFromDirectory(options.skillsSourcePath)
|
||||
: getGeneratedSkillMetadata();
|
||||
|
||||
// AGENTS.cli.md — always (re)written, this is the managed file.
|
||||
// AGENTS.wmill.md — always (re)written, this is the managed file.
|
||||
// We embed a content-hash marker so other `wmill` commands can detect a
|
||||
// stale bundle and prompt the user to `wmill refresh prompts`.
|
||||
const rawAgentsCliContent =
|
||||
@@ -105,23 +114,29 @@ export async function writeAiGuidanceFiles(
|
||||
rawAgentsCliContent,
|
||||
currentPromptsHash(nonDottedPaths)
|
||||
);
|
||||
const agentsCliPath = join(options.targetDir, "AGENTS.cli.md");
|
||||
const agentsCliPath = join(options.targetDir, AGENTS_WMILL_FILENAME);
|
||||
await writeFile(agentsCliPath, agentsCliContent, "utf8");
|
||||
const agentsCliWritten = true;
|
||||
|
||||
// Migrate the legacy `AGENTS.cli.md`: rewrite `@AGENTS.cli.md` includes in
|
||||
// user-owned files to `@AGENTS.wmill.md`, then remove the stale managed
|
||||
// file. Done before reconciliation so the rewritten include reads as
|
||||
// "already-linked" rather than triggering a duplicate append.
|
||||
const legacyManagedRemoved = await migrateLegacyManagedFile(options.targetDir);
|
||||
|
||||
// Cache the user's first migration answer and reuse it for every file
|
||||
// that needs reconciling in this run — there's never a good reason to ask
|
||||
// the same question twice in a row.
|
||||
const resolveMigration = cacheOnce(options.resolveAgentsMdMigration);
|
||||
|
||||
// AGENTS.md — user-owned. Three paths:
|
||||
// 1. doesn't exist → create skeleton (which already includes @AGENTS.cli.md).
|
||||
// 2. exists and already references @AGENTS.cli.md → leave alone.
|
||||
// 3. exists but doesn't reference @AGENTS.cli.md → ask caller via
|
||||
// 1. doesn't exist → create skeleton (which already includes @AGENTS.wmill.md).
|
||||
// 2. exists and already references @AGENTS.wmill.md → leave alone.
|
||||
// 3. exists but doesn't reference @AGENTS.wmill.md → ask caller via
|
||||
// resolveMigration (defaults to append).
|
||||
const agentsMdResult = await reconcileIncludingFile({
|
||||
path: join(options.targetDir, "AGENTS.md"),
|
||||
includeLine: AGENTS_CLI_INCLUDE_LINE,
|
||||
includeLine: AGENTS_WMILL_INCLUDE_LINE,
|
||||
skeleton: generateAgentsMdSkeleton(),
|
||||
resolveMigration,
|
||||
});
|
||||
@@ -153,9 +168,60 @@ export async function writeAiGuidanceFiles(
|
||||
claudeCreated: claudeMdResult.created,
|
||||
claudeMigration: claudeMdResult.migration,
|
||||
skillCount: skillMetadata.length,
|
||||
legacyManagedRemoved,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time migration from the old managed filename (`AGENTS.cli.md`) to
|
||||
* `AGENTS.wmill.md`:
|
||||
*
|
||||
* 1. Rewrite the `@AGENTS.cli.md` include token → `@AGENTS.wmill.md` in
|
||||
* AGENTS.md and CLAUDE.md (only when it appears as a standalone
|
||||
* whitespace-delimited token, so lookalikes like `@AGENTS.cli.md.backup`
|
||||
* are left intact).
|
||||
* 2. Delete the stale `AGENTS.cli.md` — its content is fully superseded by
|
||||
* the freshly written `AGENTS.wmill.md`.
|
||||
*
|
||||
* Returns true when a legacy `AGENTS.cli.md` was present and removed.
|
||||
*/
|
||||
async function migrateLegacyManagedFile(targetDir: string): Promise<boolean> {
|
||||
for (const fileName of ["AGENTS.md", "CLAUDE.md"]) {
|
||||
const filePath = join(targetDir, fileName);
|
||||
const existing = await readTextFile(filePath).catch(() => null);
|
||||
if (existing == null) continue;
|
||||
const rewritten = rewriteIncludeToken(
|
||||
existing,
|
||||
LEGACY_AGENTS_CLI_INCLUDE_LINE,
|
||||
AGENTS_WMILL_INCLUDE_LINE
|
||||
);
|
||||
if (rewritten !== existing) {
|
||||
await writeFile(filePath, rewritten, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const legacyPath = join(targetDir, LEGACY_AGENTS_CLI_FILENAME);
|
||||
const legacyExists = (await stat(legacyPath).catch(() => null)) != null;
|
||||
if (legacyExists) {
|
||||
await rm(legacyPath, { force: true });
|
||||
}
|
||||
return legacyExists;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `from` with `to` only where `from` appears as a standalone
|
||||
* whitespace-delimited token. Line endings and surrounding content are
|
||||
* preserved (`\s` in the lookahead matches `\r`), so a CRLF file stays CRLF.
|
||||
*/
|
||||
function rewriteIncludeToken(content: string, from: string, to: string): string {
|
||||
const re = new RegExp(`(?<=^|\\s)${escapeRegExp(from)}(?=\\s|$)`, "gm");
|
||||
return content.replace(re, to);
|
||||
}
|
||||
|
||||
function cacheOnce(
|
||||
resolver: (() => Promise<AgentsMdMigration>) | undefined
|
||||
): (() => Promise<AgentsMdMigration>) | undefined {
|
||||
@@ -212,7 +278,7 @@ function referencesIncludeLine(content: string, includeLine: string): boolean {
|
||||
// line by itself: our own CLAUDE.md default is `Instructions are in
|
||||
// @AGENTS.md` (one sentence), and a strict equality check made `wmill
|
||||
// refresh prompts` re-prompt every run on files wmill itself wrote.
|
||||
// Skipping comment-bearing lines keeps `<!-- @AGENTS.cli.md -->` from
|
||||
// Skipping comment-bearing lines keeps `<!-- @AGENTS.wmill.md -->` from
|
||||
// false-positiving.
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
+1
-1
@@ -302,7 +302,7 @@ async function main() {
|
||||
return response;
|
||||
});
|
||||
|
||||
// Warn (one line) if AGENTS.cli.md predates this CLI's prompts bundle.
|
||||
// Warn (one line) if AGENTS.wmill.md predates this CLI's prompts bundle.
|
||||
// The check is gated on argv parsing (cheap) so the ~360 KB skills.gen.ts
|
||||
// bundle stays out of the import graph for help/version/init/refresh/etc.
|
||||
if (shouldRunFreshnessCheck(process.argv)) {
|
||||
|
||||
@@ -20,6 +20,33 @@ export function getCurrentGitBranch(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a local branch with this exact name exists. */
|
||||
export function gitBranchExists(branchName: string): boolean {
|
||||
const r = spawnSync(
|
||||
"git",
|
||||
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
||||
{ stdio: "pipe" },
|
||||
);
|
||||
return r.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently checked-out branch (`git branch -m <newName>`). Used by
|
||||
* `wmill workspace fork --from-branch` to turn an existing working branch into
|
||||
* the `wm-fork/<base>/<id>` fork branch in place, preserving its commits.
|
||||
*/
|
||||
export function renameCurrentGitBranch(newName: string): void {
|
||||
const r = spawnSync("git", ["branch", "-m", newName], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
if ((r.status ?? 1) !== 0) {
|
||||
throw new Error(
|
||||
`git branch -m ${newName} failed (exit ${r.status}): ${r.stderr ?? ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getOriginalBranchForWorkspaceForks(branchName: string | null): string | null {
|
||||
if (!branchName || !branchName.startsWith(WM_FORK_PREFIX)) {
|
||||
return null
|
||||
|
||||
@@ -143,7 +143,7 @@ Copied from source bundle.
|
||||
});
|
||||
});
|
||||
|
||||
test("AGENTS.cli.md gets the skills reference from copied directory names", async () => {
|
||||
test("AGENTS.wmill.md gets the skills reference from copied directory names", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const sourceSkillsDir = join(tempDir, "source-skills");
|
||||
await writeSkill(
|
||||
@@ -163,7 +163,7 @@ Copied from source bundle.
|
||||
skillsSourcePath: sourceSkillsDir,
|
||||
});
|
||||
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
|
||||
expect(agentsCli).toContain(".agents/skills/custom-folder/SKILL.md");
|
||||
expect(agentsCli).not.toContain(".agents/skills/write-flow/SKILL.md");
|
||||
// The skill reference points at the .agents/ tree — not .claude/ —
|
||||
@@ -172,7 +172,7 @@ Copied from source bundle.
|
||||
});
|
||||
});
|
||||
|
||||
test("AGENTS.cli.md and CLAUDE.md are written even if skills creation fails", async () => {
|
||||
test("AGENTS.wmill.md and CLAUDE.md are written even if skills creation fails", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
// Create a file at .claude so mkdir of .claude/skills throws.
|
||||
await writeFile(join(tempDir, ".claude"), "not a directory\n", "utf8");
|
||||
@@ -181,11 +181,11 @@ Copied from source bundle.
|
||||
writeAiGuidanceFiles({ targetDir: tempDir })
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(await readFile(join(tempDir, "AGENTS.cli.md"), "utf8")).toContain(
|
||||
expect(await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")).toContain(
|
||||
".agents/skills/"
|
||||
);
|
||||
expect(await readFile(join(tempDir, "AGENTS.md"), "utf8")).toContain(
|
||||
"@AGENTS.cli.md"
|
||||
"@AGENTS.wmill.md"
|
||||
);
|
||||
expect(await readFile(join(tempDir, "CLAUDE.md"), "utf8")).toContain(
|
||||
"@AGENTS.md"
|
||||
@@ -195,20 +195,20 @@ Copied from source bundle.
|
||||
});
|
||||
|
||||
describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
|
||||
test("creates a skeleton AGENTS.md (with @AGENTS.cli.md include) when none exists", async () => {
|
||||
test("creates a skeleton AGENTS.md (with @AGENTS.wmill.md include) when none exists", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
expect(result.agentsCreated).toBe(true);
|
||||
expect(result.agentsMigration).toBe("not-applicable");
|
||||
|
||||
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
expect(agentsMd).toContain("@AGENTS.cli.md");
|
||||
expect(agentsMd).toContain("@AGENTS.wmill.md");
|
||||
});
|
||||
});
|
||||
|
||||
test("leaves an existing AGENTS.md alone when it already references @AGENTS.cli.md", async () => {
|
||||
test("leaves an existing AGENTS.md alone when it already references @AGENTS.wmill.md", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const original = "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.cli.md\n";
|
||||
const original = "# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.wmill.md\n";
|
||||
await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
|
||||
|
||||
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
@@ -219,7 +219,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("appends @AGENTS.cli.md when the resolver returns 'append'", async () => {
|
||||
test("appends @AGENTS.wmill.md when the resolver returns 'append'", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const original = "# Existing custom AGENTS.md\n\nproject rules here.\n";
|
||||
await writeFile(join(tempDir, "AGENTS.md"), original, "utf8");
|
||||
@@ -233,7 +233,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
|
||||
|
||||
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
expect(updated).toStartWith(original);
|
||||
expect(updated).toContain("@AGENTS.cli.md");
|
||||
expect(updated).toContain("@AGENTS.wmill.md");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,7 +251,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
|
||||
|
||||
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
expect(updated).not.toBe(original);
|
||||
expect(updated).toContain("@AGENTS.cli.md");
|
||||
expect(updated).toContain("@AGENTS.wmill.md");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,7 +281,7 @@ describe("writeAiGuidanceFiles — AGENTS.md reconciliation", () => {
|
||||
|
||||
const updated = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
expect(updated).toStartWith(original);
|
||||
expect(updated).toContain("@AGENTS.cli.md");
|
||||
expect(updated).toContain("@AGENTS.wmill.md");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -385,19 +385,78 @@ describe("writeAiGuidanceFiles — CLAUDE.md reconciliation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeAiGuidanceFiles — legacy AGENTS.cli.md migration", () => {
|
||||
test("removes a legacy AGENTS.cli.md and rewrites the @AGENTS.cli.md include", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
// Simulate a project initialized by an older CLI.
|
||||
await writeFile(
|
||||
join(tempDir, "AGENTS.cli.md"),
|
||||
"# old managed file\n",
|
||||
"utf8"
|
||||
);
|
||||
await writeFile(
|
||||
join(tempDir, "AGENTS.md"),
|
||||
"# My AGENTS.md\n\nlocal stuff\n\n@AGENTS.cli.md\n",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
|
||||
// Legacy file is gone; the new managed file is present.
|
||||
expect(result.legacyManagedRemoved).toBe(true);
|
||||
await expect(
|
||||
readFile(join(tempDir, "AGENTS.cli.md"), "utf8")
|
||||
).rejects.toThrow();
|
||||
expect(
|
||||
await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8")
|
||||
).toContain(".agents/skills/");
|
||||
|
||||
// The include was rewritten in place (so it reads as already-linked,
|
||||
// not a duplicate append).
|
||||
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
expect(agentsMd).toContain("@AGENTS.wmill.md");
|
||||
expect(agentsMd).not.toContain("@AGENTS.cli.md");
|
||||
expect(result.agentsMigration).toBe("already-linked");
|
||||
});
|
||||
});
|
||||
|
||||
test("legacyManagedRemoved is false when there is no legacy file", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const result = await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
expect(result.legacyManagedRemoved).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("does not rewrite a @AGENTS.cli.md.backup lookalike token", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
await writeFile(
|
||||
join(tempDir, "AGENTS.md"),
|
||||
"see @AGENTS.cli.md.backup\n\n@AGENTS.wmill.md\n",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
|
||||
const agentsMd = await readFile(join(tempDir, "AGENTS.md"), "utf8");
|
||||
// The standalone backup reference (a different file) is preserved.
|
||||
expect(agentsMd).toContain("@AGENTS.cli.md.backup");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", () => {
|
||||
test.each([
|
||||
["bare line", "@AGENTS.cli.md"],
|
||||
["between blank lines", "before\n\n@AGENTS.cli.md\n\nafter"],
|
||||
["leading whitespace then include", " @AGENTS.cli.md\n"],
|
||||
["CRLF line endings", "line one\r\n@AGENTS.cli.md\r\nline three"],
|
||||
["bare line", "@AGENTS.wmill.md"],
|
||||
["between blank lines", "before\n\n@AGENTS.wmill.md\n\nafter"],
|
||||
["leading whitespace then include", " @AGENTS.wmill.md\n"],
|
||||
["CRLF line endings", "line one\r\n@AGENTS.wmill.md\r\nline three"],
|
||||
// Mid-sentence include: this is how our own CLAUDE.md default looks
|
||||
// ("Instructions are in @AGENTS.md"). A strict line-equality check made
|
||||
// `wmill refresh prompts` re-prompt every run on files wmill wrote.
|
||||
["mid-sentence include", "Instructions are in @AGENTS.cli.md\n"],
|
||||
["mid-sentence include", "Instructions are in @AGENTS.wmill.md\n"],
|
||||
// `>` blockquote prefix doesn't disable Claude's `@`-import expansion,
|
||||
// so we treat it as a reference too.
|
||||
["blockquoted include", "> @AGENTS.cli.md"],
|
||||
["blockquoted include", "> @AGENTS.wmill.md"],
|
||||
])("treats %s as a reference (no append)", async (_label, content) => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
|
||||
@@ -408,11 +467,11 @@ describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", ()
|
||||
});
|
||||
|
||||
test.each([
|
||||
["@AGENTS.cli.md.backup", "@AGENTS.cli.md.backup"],
|
||||
["@AGENTS.cli.mdx", "@AGENTS.cli.mdx"],
|
||||
["@AGENTS.wmill.md.backup", "@AGENTS.wmill.md.backup"],
|
||||
["@AGENTS.wmill.mdx", "@AGENTS.wmill.mdx"],
|
||||
["@AGENTS-cli-md (lookalike)", "@AGENTS-cli-md"],
|
||||
["@AGENTS.cli.md without surrounding whitespace", "foo@AGENTS.cli.md"],
|
||||
["commented-out include", "<!-- @AGENTS.cli.md -->"],
|
||||
["@AGENTS.wmill.md without surrounding whitespace", "foo@AGENTS.wmill.md"],
|
||||
["commented-out include", "<!-- @AGENTS.wmill.md -->"],
|
||||
])("does not treat %s as a reference (append happens)", async (_label, content) => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
await writeFile(join(tempDir, "AGENTS.md"), content, "utf8");
|
||||
@@ -426,10 +485,10 @@ describe("writeAiGuidanceFiles — referencesAgentsCli (via reconciliation)", ()
|
||||
});
|
||||
|
||||
describe("prompts freshness — hash marker", () => {
|
||||
test("AGENTS.cli.md written by writeAiGuidanceFiles carries a hash marker", async () => {
|
||||
test("AGENTS.wmill.md written by writeAiGuidanceFiles carries a hash marker", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
|
||||
const hash = extractPromptsHash(agentsCli);
|
||||
expect(hash).not.toBeNull();
|
||||
expect(hash).toMatch(/^[0-9a-f]{12}$/);
|
||||
@@ -441,7 +500,7 @@ describe("prompts freshness — hash marker", () => {
|
||||
// writeAiGuidanceFiles defaults nonDottedPaths to `false` (matching
|
||||
// core/conf.ts's missing-key default).
|
||||
await writeAiGuidanceFiles({ targetDir: tempDir });
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.cli.md"), "utf8");
|
||||
const agentsCli = await readFile(join(tempDir, "AGENTS.wmill.md"), "utf8");
|
||||
expect(extractPromptsHash(agentsCli)).toBe(currentPromptsHash(false));
|
||||
});
|
||||
});
|
||||
@@ -516,9 +575,9 @@ describe("prompts freshness — additional invariants", () => {
|
||||
|
||||
test("warnIfPromptsStale writes to stderr (never stdout)", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
// Write a tampered AGENTS.cli.md so the freshness check trips.
|
||||
// Write a tampered AGENTS.wmill.md so the freshness check trips.
|
||||
await writeFile(
|
||||
join(tempDir, "AGENTS.cli.md"),
|
||||
join(tempDir, "AGENTS.wmill.md"),
|
||||
"# Windmill CLI Agent Instructions\n<!-- wmill-prompts-hash: 000000000000 -->\nbody\n",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
@@ -430,12 +430,12 @@ List all queues with their metrics
|
||||
|
||||
### refresh
|
||||
|
||||
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
|
||||
Refresh wmill-managed project files (AGENTS.wmill.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: 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 prompts` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
|
||||
- `--yes` - Non-interactive: append the @AGENTS.wmill.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).
|
||||
|
||||
@@ -726,7 +726,8 @@ workspace related commands
|
||||
- `--create-workspace-name <workspace_name:string>` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
|
||||
- `--color <color:string>` - Workspace color (hex code, e.g. #ff0000)
|
||||
- `--datatable-behavior <behavior:string>` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip')
|
||||
- `--from-branch <branch:string>` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
|
||||
- `workspace delete-fork <fork_name:string>` - Delete a forked workspace and git branch
|
||||
- `-y --yes` - Skip confirmation prompt
|
||||
- `workspace merge` - Compare and deploy changes between a fork and its parent workspace
|
||||
|
||||
@@ -2981,12 +2981,12 @@ List all queues with their metrics
|
||||
|
||||
### refresh
|
||||
|
||||
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
|
||||
Refresh wmill-managed project files (AGENTS.wmill.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: 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 prompts\` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
|
||||
- \`--yes\` - Non-interactive: append the @AGENTS.wmill.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).
|
||||
|
||||
@@ -3277,7 +3277,8 @@ workspace related commands
|
||||
- \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
|
||||
- \`--color <color:string>\` - Workspace color (hex code, e.g. #ff0000)
|
||||
- \`--datatable-behavior <behavior:string>\` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip')
|
||||
- \`--from-branch <branch:string>\` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
|
||||
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace and git branch
|
||||
- \`-y --yes\` - Skip confirmation prompt
|
||||
- \`workspace merge\` - Compare and deploy changes between a fork and its parent workspace
|
||||
@@ -3451,6 +3452,10 @@ import Stripe from "stripe";
|
||||
import { someFunction } from "some-package";
|
||||
\`\`\`
|
||||
|
||||
## Prefer \`//native\` when the runtime allows it
|
||||
|
||||
If a script only needs \`fetch\` and the JavaScript standard library — including when it uses \`windmill-client\` — prefer making it a **native** script: add \`//native\` as the first line and write it with the \`write-script-bunnative\` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. \`windmill-client\` works on the native worker (its calls go over \`fetch\`), so needing the Windmill client is **not** a reason to avoid \`//native\`. Use the regular \`bun\` language only when the code (or a dependency) needs Node/Bun runtime APIs — \`node:*\` modules, the filesystem, child processes, or native addons.
|
||||
|
||||
## Windmill Client
|
||||
|
||||
Import the windmill client for platform interactions:
|
||||
@@ -3459,7 +3464,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
\`\`\`
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to \`fetch\`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -3575,7 +3582,9 @@ export async function main(url: string) {
|
||||
|
||||
## Windmill Client
|
||||
|
||||
\`windmill-client\` is available for Windmill-specific primitives such as the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). Use \`fetch\` for plain HTTP.
|
||||
\`windmill-client\` works on the native worker (its calls go over \`fetch\`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). It handles auth, the workspace, and the base URL for you. Reserve raw \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a \`fetch\` against the Windmill API.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -3740,7 +3749,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
\`\`\`
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to \`fetch\`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -162,6 +162,10 @@ import Stripe from "stripe";
|
||||
import { someFunction } from "some-package";
|
||||
```
|
||||
|
||||
## Prefer `//native` when the runtime allows it
|
||||
|
||||
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
|
||||
|
||||
## Windmill Client
|
||||
|
||||
Import the windmill client for platform interactions:
|
||||
@@ -170,7 +174,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -286,7 +292,9 @@ export async function main(url: string) {
|
||||
|
||||
## Windmill Client
|
||||
|
||||
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
|
||||
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
@@ -451,7 +459,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -435,12 +435,12 @@ List all queues with their metrics
|
||||
|
||||
### refresh
|
||||
|
||||
Refresh wmill-managed project files (AGENTS.cli.md, skills, tsconfig.wmill.json)
|
||||
Refresh wmill-managed project files (AGENTS.wmill.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: 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 prompts` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
|
||||
- `--yes` - Non-interactive: append the @AGENTS.wmill.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).
|
||||
|
||||
@@ -731,7 +731,8 @@ workspace related commands
|
||||
- `--create-workspace-name <workspace_name:string>` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
|
||||
- `--color <color:string>` - Workspace color (hex code, e.g. #ff0000)
|
||||
- `--datatable-behavior <behavior:string>` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip')
|
||||
- `--from-branch <branch:string>` - Fork based on this base branch (its bound workspace is the parent) and rename your current working branch onto the fork branch wm-fork/<branch>/<id>. Use when you're already on a branch (e.g. with forked-datatable edits) you want to turn into the fork. Omit to base the fork on the current branch and check out a fresh fork branch instead.
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip', and confirms the --from-branch rename)
|
||||
- `workspace delete-fork <fork_name:string>` - Delete a forked workspace and git branch
|
||||
- `-y --yes` - Skip confirmation prompt
|
||||
- `workspace merge` - Compare and deploy changes between a fork and its parent workspace
|
||||
|
||||
@@ -126,7 +126,7 @@ The runnable ID is the filename without extension. For example, `get_user.ts` cr
|
||||
| C# | `.cs` | `myFunc.cs` |
|
||||
| Java | `.java` | `myFunc.java` |
|
||||
|
||||
After creating a runnable, tell the user they can generate lock files by running:
|
||||
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
|
||||
```bash
|
||||
wmill generate-metadata
|
||||
```
|
||||
|
||||
@@ -44,7 +44,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (`wmill flow preview` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (`wmill sync push`, `wmill generate-metadata`) so the user can approve them. The options:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
|
||||
@@ -78,6 +78,10 @@ import Stripe from "stripe";
|
||||
import { someFunction } from "some-package";
|
||||
```
|
||||
|
||||
## Prefer `//native` when the runtime allows it
|
||||
|
||||
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
|
||||
|
||||
## Windmill Client
|
||||
|
||||
Import the windmill client for platform interactions:
|
||||
@@ -86,7 +90,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -87,7 +87,9 @@ export async function main(url: string) {
|
||||
|
||||
## Windmill Client
|
||||
|
||||
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
|
||||
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
After writing, act on the user's intent instead of just listing commands. Run the safe, non-deploying command yourself when it fits (`wmill flow preview` — see "After writing — offer to run, don't wait passively" below); only *name* the commands that deploy or rewrite files (`wmill sync push`, `wmill generate-metadata`) so the user can approve them. The options:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
|
||||
@@ -121,7 +121,7 @@ The runnable ID is the filename without extension. For example, `get_user.ts` cr
|
||||
| C# | `.cs` | `myFunc.cs` |
|
||||
| Java | `.java` | `myFunc.java` |
|
||||
|
||||
After creating a runnable, tell the user they can generate lock files by running:
|
||||
After creating a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
|
||||
```bash
|
||||
wmill generate-metadata
|
||||
```
|
||||
|
||||
@@ -1827,7 +1827,7 @@ CONTEXT7_REPO_NAME = "windmill-cli-docs"
|
||||
|
||||
|
||||
def extract_agents_md_template() -> str:
|
||||
"""Extract the AGENTS.cli.md template string from cli/src/guidance/core.ts.
|
||||
"""Extract the AGENTS.wmill.md template string from cli/src/guidance/core.ts.
|
||||
|
||||
Keeping a single source of truth in TypeScript avoids drift between what
|
||||
`wmill init` writes locally and what we publish for context7 ingestion.
|
||||
@@ -1844,7 +1844,7 @@ def extract_agents_md_template() -> str:
|
||||
)
|
||||
if not match:
|
||||
raise RuntimeError(
|
||||
f"Could not extract AGENTS.cli.md template from {core_ts_path}"
|
||||
f"Could not extract AGENTS.wmill.md template from {core_ts_path}"
|
||||
)
|
||||
return _unescape_ts_template_literal(match.group(1))
|
||||
|
||||
@@ -1866,7 +1866,7 @@ def _unescape_ts_template_literal(raw: str) -> str:
|
||||
def render_agents_md_for_docs(
|
||||
skills: list[str], skill_desc_map: dict[str, str]
|
||||
) -> str:
|
||||
"""Render AGENTS.cli.md exactly as `wmill init` would, for the docs repo.
|
||||
"""Render AGENTS.wmill.md exactly as `wmill init` would, for the docs repo.
|
||||
|
||||
The skill reference paths point at `.agents/skills/` (the canonical tree
|
||||
that Codex/Pi read directly and that Claude Code mirrors under
|
||||
@@ -2010,7 +2010,7 @@ def generate_context7_repo(
|
||||
skill_desc_map = build_skill_desc_map(skills)
|
||||
|
||||
# AGENTS.md — the managed CLI guidance (what `wmill init` writes as
|
||||
# AGENTS.cli.md locally). Kept under the `AGENTS.md` filename here to
|
||||
# AGENTS.wmill.md locally). Kept under the `AGENTS.md` filename here to
|
||||
# preserve the existing context7 ingest path; docs consumers read this
|
||||
# as the canonical AGENTS file.
|
||||
(target_dir / "AGENTS.md").write_text(
|
||||
|
||||
@@ -38,6 +38,10 @@ import Stripe from "stripe";
|
||||
import { someFunction } from "some-package";
|
||||
```
|
||||
|
||||
## Prefer `//native` when the runtime allows it
|
||||
|
||||
If a script only needs `fetch` and the JavaScript standard library — including when it uses `windmill-client` — prefer making it a **native** script: add `//native` as the first line and write it with the `write-script-bunnative` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. `windmill-client` works on the native worker (its calls go over `fetch`), so needing the Windmill client is **not** a reason to avoid `//native`. Use the regular `bun` language only when the code (or a dependency) needs Node/Bun runtime APIs — `node:*` modules, the filesystem, child processes, or native addons.
|
||||
|
||||
## Windmill Client
|
||||
|
||||
Import the windmill client for platform interactions:
|
||||
@@ -46,7 +50,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ export async function main(url: string) {
|
||||
|
||||
## Windmill Client
|
||||
|
||||
`windmill-client` is available for Windmill-specific primitives such as the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). Use `fetch` for plain HTTP.
|
||||
`windmill-client` works on the native worker (its calls go over `fetch`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (`loadS3File`, `loadS3FileStream`, `writeS3File`, `S3Object`). It handles auth, the workspace, and the base URL for you. Reserve raw `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a `fetch` against the Windmill API.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ Import the windmill client for platform interactions:
|
||||
import * as wmill from "windmill-client";
|
||||
```
|
||||
|
||||
See the SDK documentation for available methods.
|
||||
**Prefer `windmill-client` over raw `fetch` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve `fetch` for calling *external* HTTP APIs that aren't Windmill.
|
||||
|
||||
The full `windmill-client` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to `fetch`.
|
||||
|
||||
## Preprocessor Scripts
|
||||
|
||||
|
||||
Reference in New Issue
Block a user