mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat(cli): improve agent prompts/skills and workspace fork workflow (#9531)
* feat(cli): improve agent prompts/skills and workspace fork workflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): refuse fork --from-branch rename of a base branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): auto-detect fork branch workflow, drop rt.d.ts refresh and legacy-name warning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): reconcile raw-app generate-metadata stance (agent offers+runs)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): agent runs all CLI commands, gated on intent not on user typing them
Extends #9467's safe-vs-destructive model: the agent runs consequential commands (sync push, generate-metadata) itself too, gated on explicit user intent rather than handed to the user to type. The explicit-intent rule is the safeguard; an approval prompt is treated as a possible backstop, not assumed (auto-approve/headless runs have none).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Revert "docs(skills): agent runs all CLI commands, gated on intent not on user typing them"
Reverts 9225e1759b. That commit over-reached: #9467 already established the safe-vs-destructive split, and the targeted item-6 fix already removed the passive "tell the user they can run <safe next step>" phrasing. The blanket "agent runs everything" principle pushed deploys to be more eager and carried a wrong "permission layer prompts for approval" claim (untrue in auto-approve/headless mode). Keep deploys conservative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): default fork workspace name/id to the current branch when renaming it
When 'wmill workspace fork' converts the current working branch into the fork branch, default the fork's name and id to that branch (sanitized to a slug, since branch names can contain '/'). Interactive: the prompt is pre-filled (enter to accept); non-interactive (--yes): used automatically. Adds a unit test for the slug derivation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): address fork review — guard fork-branch rename, cap+validate fork id
Two P2s from review:
- --from-branch refused when the current branch is already a fork branch (would detach the existing fork by renaming its branch).
- fork id slug capped to 42 chars (backend max 50 incl. wm-fork- prefix); auto-derived id is slugged; full id validated client-side before existsWorkspace/datatable cloning so an invalid id fails fast instead of leaving cloned Postgres databases behind.
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:
@@ -53,11 +53,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,
|
||||
});
|
||||
@@ -178,10 +186,10 @@ async function promptsAction(opts: CommandOptions): Promise<void> {
|
||||
}
|
||||
|
||||
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,29 @@ 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 process from "node:process";
|
||||
import { WM_FORK_PREFIX } from "../../core/constants.ts";
|
||||
import { tryResolveBranchWorkspace } from "../../core/context.ts";
|
||||
import {
|
||||
findWorkspaceByGitBranch,
|
||||
getEffectiveGitBranch,
|
||||
getWorkspaceNames,
|
||||
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 +37,84 @@ 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");
|
||||
}
|
||||
|
||||
const config = await readConfigFile({ warnIfMissing: false });
|
||||
const originalBranchIfForked = getOriginalBranchForWorkspaceForks(currentBranch);
|
||||
|
||||
// A "base branch" is one we must not rename onto a fork branch: mapped to a
|
||||
// workspace in wmill.yaml, or a conventional default (main/master).
|
||||
const isBaseBranch = (branch: string): boolean =>
|
||||
branch === "main" ||
|
||||
branch === "master" ||
|
||||
findWorkspaceByGitBranch(config.workspaces, branch) !== undefined;
|
||||
|
||||
// Decide the base branch the fork links to, and whether to rename the
|
||||
// current working branch onto the fork branch. Auto-detected from where you
|
||||
// are; `--from-branch` is the explicit/non-interactive override.
|
||||
let clonedBranchName: string;
|
||||
let renameCurrent: boolean;
|
||||
|
||||
if (opts.fromBranch) {
|
||||
// Explicit override: base on <fromBranch>, rename the current branch.
|
||||
if (opts.fromBranch === currentBranch) {
|
||||
throw new Error(
|
||||
`--from-branch is for converting a *different* working branch into the fork branch, but you are already on \`${currentBranch}\`. ` +
|
||||
`Omit --from-branch to create a fresh fork branch with \`git checkout -b\`.`,
|
||||
);
|
||||
}
|
||||
if (isBaseBranch(currentBranch)) {
|
||||
throw new Error(
|
||||
`Refusing to rename your current branch \`${currentBranch}\` — it looks like a base branch (mapped to a workspace in wmill.yaml, or main/master). ` +
|
||||
`Check out the disposable working branch you want to convert first.`,
|
||||
);
|
||||
}
|
||||
if (getOriginalBranchForWorkspaceForks(currentBranch)) {
|
||||
// Current branch is itself a fork branch (wm-fork/<base>/<old-id>).
|
||||
// Renaming it onto the new fork branch would detach the existing fork.
|
||||
throw new Error(
|
||||
`Refusing to rename your current branch \`${currentBranch}\` — it is already a fork branch. ` +
|
||||
`To fork a fork, omit --from-branch: \`wmill workspace fork\` bases the new fork on this fork's original branch and creates a fresh fork branch without renaming.`,
|
||||
);
|
||||
}
|
||||
if (!findWorkspaceByGitBranch(config.workspaces, opts.fromBranch)) {
|
||||
throw new Error(
|
||||
`Could not find a workspace mapped to branch \`${opts.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).`,
|
||||
);
|
||||
}
|
||||
clonedBranchName = opts.fromBranch;
|
||||
renameCurrent = true;
|
||||
} else if (originalBranchIfForked) {
|
||||
// Fork of a fork: link to the original branch; user checks out a new branch.
|
||||
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;
|
||||
renameCurrent = false;
|
||||
} else if (isBaseBranch(currentBranch)) {
|
||||
// On a base branch: base the fork on it; user checks out a fresh fork branch.
|
||||
clonedBranchName = currentBranch;
|
||||
renameCurrent = false;
|
||||
} else {
|
||||
// On a non-base working branch: offer to base the fork on it and rename it.
|
||||
clonedBranchName = await resolveWorkingBranchBase(config, opts, currentBranch);
|
||||
renameCurrent = true;
|
||||
}
|
||||
|
||||
// Resolve the parent workspace. When the base differs from the current
|
||||
// branch (the rename workflows), resolve via the base branch's workspace;
|
||||
// otherwise use plain branch resolution.
|
||||
let workspace;
|
||||
if (clonedBranchName === currentBranch) {
|
||||
workspace = await tryResolveBranchWorkspace(opts);
|
||||
} else {
|
||||
const baseMatch = findWorkspaceByGitBranch(config.workspaces, clonedBranchName);
|
||||
workspace = baseMatch
|
||||
? await tryResolveBranchWorkspace(opts, baseMatch[0])
|
||||
: 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,24 +122,6 @@ 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) {
|
||||
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 {
|
||||
clonedBranchName = currentBranch;
|
||||
}
|
||||
|
||||
if (!clonedBranchName) {
|
||||
throw new Error("Failed to get current branch name, aborting operation");
|
||||
}
|
||||
|
||||
if (opts.workspace) {
|
||||
log.info(
|
||||
colors.red.bold(
|
||||
@@ -58,18 +131,39 @@ async function createWorkspaceFork(
|
||||
return;
|
||||
}
|
||||
|
||||
while (workspaceName === undefined) {
|
||||
if (!workspaceName) {
|
||||
workspaceName = await Input.prompt("Name this forked workspace:");
|
||||
// When we're converting the current branch into the fork branch, default
|
||||
// the fork's name/id to that branch — almost always what you want, and it
|
||||
// keeps the fork branch named after the work you already have
|
||||
// (wm-fork/<base>/<branch>). Interactive: pre-fill the prompt (press enter
|
||||
// to accept). Non-interactive (`--yes`): use it automatically.
|
||||
const branchDefaultId = renameCurrent ? branchToForkId(currentBranch) : undefined;
|
||||
const interactive = process.stdin.isTTY && opts.yes !== true;
|
||||
|
||||
if (workspaceName === undefined) {
|
||||
if (branchDefaultId && !interactive) {
|
||||
workspaceName = branchDefaultId;
|
||||
log.info(`Naming the fork after the current branch: \`${workspaceName}\``);
|
||||
} else {
|
||||
workspaceName = await Input.prompt({
|
||||
message: "Name this forked workspace:",
|
||||
default: branchDefaultId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
workspaceId = await Input.prompt({
|
||||
message: `Enter the ID of this forked workspace, it will then be prefixed by ${WM_FORK_PREFIX}. It will also determine the branch name`,
|
||||
default: workspaceName,
|
||||
suggestions: [workspaceName],
|
||||
});
|
||||
// The id (unlike the display name) must be a valid slug — derive it from
|
||||
// the name rather than using the free-form name verbatim.
|
||||
const idDefault = branchToForkId(workspaceName);
|
||||
if (branchDefaultId && !interactive) {
|
||||
workspaceId = idDefault;
|
||||
} else {
|
||||
workspaceId = await Input.prompt({
|
||||
message: `Enter the ID of this forked workspace, it will then be prefixed by ${WM_FORK_PREFIX}. It will also determine the branch name`,
|
||||
default: idDefault,
|
||||
suggestions: [idDefault],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const token = workspace.token;
|
||||
@@ -87,6 +181,11 @@ async function createWorkspaceFork(
|
||||
log.info(colors.blue(`Creating forked workspace: ${workspaceName}...`));
|
||||
|
||||
const trueWorkspaceId = `${WM_FORK_PREFIX}-${workspaceId}`;
|
||||
// Fail fast on an invalid id (e.g. an explicit positional id, or a long
|
||||
// branch name under --yes) before existsWorkspace, datatable cloning, and
|
||||
// branch creation — a late backend rejection would leave cloned databases
|
||||
// behind.
|
||||
validateForkWorkspaceId(trueWorkspaceId);
|
||||
let alreadyExists = false;
|
||||
try {
|
||||
alreadyExists = await wmill.existsWorkspace({
|
||||
@@ -248,9 +347,39 @@ 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:
|
||||
// Rename workflow: turn the current working branch into the fork branch in
|
||||
// place so its commits become the fork's. (Consent was already established —
|
||||
// by `--from-branch`, or the interactive prompt for a non-base branch.)
|
||||
// Otherwise: leave the user on their branch and have them check out a fresh
|
||||
// fork branch.
|
||||
let onForkBranch = false;
|
||||
if (renameCurrent) {
|
||||
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 {
|
||||
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.
|
||||
|
||||
@@ -261,6 +390,122 @@ To merge changes back to the parent workspace, you can:
|
||||
See: https://www.windmill.dev/docs/advanced/workspace_forks`);
|
||||
}
|
||||
|
||||
/**
|
||||
* When `wmill workspace fork` is run from a non-base working branch, confirm
|
||||
* the user wants to turn it into a fork branch, and resolve which base branch
|
||||
* the fork should be linked to. Throws in non-interactive mode (where the user
|
||||
* must pass `--from-branch <base>` instead).
|
||||
*/
|
||||
async function resolveWorkingBranchBase(
|
||||
config: Awaited<ReturnType<typeof readConfigFile>>,
|
||||
opts: { yes?: boolean },
|
||||
currentBranch: string,
|
||||
): Promise<string> {
|
||||
const interactive = process.stdin.isTTY && opts.yes !== true;
|
||||
if (!interactive) {
|
||||
throw new Error(
|
||||
`You are on working branch \`${currentBranch}\`, which is not a base branch. ` +
|
||||
`Pass --from-branch <base> to base the fork on a base branch and rename this branch onto the fork branch, ` +
|
||||
`or check out a base branch and run \`wmill workspace fork\` to create a fresh fork branch.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { Select } = await import("@cliffy/prompt/select");
|
||||
const proceed = await Select.prompt({
|
||||
message: `You're on working branch \`${currentBranch}\`, not a base branch. Base a fork on it and rename it onto the fork branch?`,
|
||||
options: [
|
||||
{ name: "Yes, base the fork on this branch and rename it", value: "yes" },
|
||||
{ name: "No, cancel", value: "no" },
|
||||
],
|
||||
});
|
||||
if (proceed !== "yes") {
|
||||
throw new Error("Fork cancelled. Check out a base branch to create a fresh fork branch instead.");
|
||||
}
|
||||
|
||||
const baseBranches = listConfiguredBaseBranches(config);
|
||||
if (baseBranches.length === 0) {
|
||||
throw new Error(
|
||||
`No base branches are configured in wmill.yaml's workspaces section, so the fork can't be linked to a parent. ` +
|
||||
`Add the parent workspace to wmill.yaml, or pass --from-branch <base>.`,
|
||||
);
|
||||
}
|
||||
if (baseBranches.length === 1) {
|
||||
log.info(`Basing the fork on \`${baseBranches[0]}\`.`);
|
||||
return baseBranches[0];
|
||||
}
|
||||
return await Select.prompt({
|
||||
message: "Which base branch is this fork based on (the parent)?",
|
||||
options: baseBranches.map((b) => ({ name: b, value: b })),
|
||||
});
|
||||
}
|
||||
|
||||
// The backend caps a fork workspace id (`wm-fork-<slug>`) at 50 chars total
|
||||
// (validate_fork_workspace_id in windmill-common), so the slug is at most
|
||||
// 50 - "wm-fork-".length (8) = 42.
|
||||
const MAX_FORK_ID_SLUG = 42;
|
||||
|
||||
/**
|
||||
* Derive a workspace-id-safe slug from a git branch name. Branch names can
|
||||
* contain `/` and other characters that aren't valid in a workspace id and
|
||||
* would break the `wm-fork/<base>/<id>` branch-name parsing, so collapse any
|
||||
* invalid run to a single dash, trim, and cap to the backend length limit.
|
||||
*/
|
||||
function branchToForkId(branch: string): string {
|
||||
const slug = branch
|
||||
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, MAX_FORK_ID_SLUG)
|
||||
.replace(/-+$/g, ""); // re-trim if the cut landed on a dash
|
||||
return slug || "fork";
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror the backend `validate_fork_workspace_id` so an invalid id fails fast
|
||||
* — before `existsWorkspace`, datatable cloning (which creates real per-fork
|
||||
* Postgres databases), and branch creation, none of which get cleaned up on a
|
||||
* late backend rejection. `id` is the full `wm-fork-<slug>` workspace id.
|
||||
*/
|
||||
function validateForkWorkspaceId(id: string): void {
|
||||
const reject = (reason: string): never => {
|
||||
throw new Error(
|
||||
`Fork workspace id \`${id}\` is invalid: ${reason}. Choose a shorter or simpler name/id.`,
|
||||
);
|
||||
};
|
||||
if (id.length > 50) {
|
||||
reject(`too long (${id.length} chars; max 50 including the \`${WM_FORK_PREFIX}-\` prefix)`);
|
||||
}
|
||||
if (id.endsWith(".")) reject("cannot end with '.'");
|
||||
if (id.endsWith(".lock")) reject("cannot end with '.lock'");
|
||||
if (id.includes("..")) reject("cannot contain '..'");
|
||||
if (id.includes("@{")) reject("cannot contain '@{'");
|
||||
if (id.includes("//")) reject("cannot contain '//'");
|
||||
for (const ch of id) {
|
||||
if (":~^?*[\\ ".includes(ch)) reject(`contains forbidden character '${ch}'`);
|
||||
const code = ch.charCodeAt(0);
|
||||
if (code < 0x20 || code === 0x7f) reject("contains a control character");
|
||||
}
|
||||
for (const component of id.split("/")) {
|
||||
if (component.startsWith(".")) reject("a path component cannot start with '.'");
|
||||
if (component.endsWith(".lock")) reject("a path component cannot end with '.lock'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base branches configured in wmill.yaml, mirroring how `findWorkspaceByGitBranch`
|
||||
* keys them (effective git branch = `gitBranch ?? workspaceName`, reserved keys
|
||||
* excluded) so the chosen base resolves to a workspace afterwards.
|
||||
*/
|
||||
function listConfiguredBaseBranches(
|
||||
config: Awaited<ReturnType<typeof readConfigFile>>,
|
||||
): string[] {
|
||||
const workspaces = config.workspaces;
|
||||
const branches = new Set<string>();
|
||||
for (const name of getWorkspaceNames(workspaces)) {
|
||||
branches.add(getEffectiveGitBranch(name, workspaces![name]));
|
||||
}
|
||||
return [...branches];
|
||||
}
|
||||
|
||||
async function deleteWorkspaceFork(
|
||||
opts: GlobalOptions & {
|
||||
yes?: boolean;
|
||||
@@ -332,4 +577,9 @@ async function deleteWorkspaceFork(
|
||||
}
|
||||
}
|
||||
|
||||
export { createWorkspaceFork, deleteWorkspaceFork };
|
||||
export {
|
||||
branchToForkId,
|
||||
createWorkspaceFork,
|
||||
deleteWorkspaceFork,
|
||||
validateForkWorkspaceId,
|
||||
};
|
||||
|
||||
@@ -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>",
|
||||
"Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch `wmill workspace fork` offers this interactively; from a base branch it creates a fresh fork branch."
|
||||
)
|
||||
.option("-y --yes", "Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.")
|
||||
.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,19 @@ 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.
|
||||
|
||||
Just run \`wmill workspace fork\` — it adapts to where you are:
|
||||
|
||||
- **On a base branch** (e.g. \`main\`, or a branch bound to a workspace): it bases the fork on that branch and prints a \`git checkout -b wm-fork/<base>/<id>\` to start a fresh fork branch.
|
||||
- **On a working branch** (e.g. you've branched and already edited a forked datatable): it offers to base the fork on that branch and rename it onto \`wm-fork/<base>/<id>\` in place, preserving its commits — asking which base branch is the parent if there's more than one.
|
||||
|
||||
For non-interactive runs from a working branch, pass \`--from-branch <base>\` to skip the prompts. The CLI refuses to rename a base branch.
|
||||
|
||||
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,16 @@ export function currentPromptsHash(nonDottedPaths: boolean): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read AGENTS.cli.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,
|
||||
* Read the managed guidance file 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 managed file, no marker,
|
||||
* matching hash, IO error, …) so it never gets in the user's way.
|
||||
*
|
||||
* Back-compat: prefers `AGENTS.wmill.md` but falls back to the legacy
|
||||
* `AGENTS.cli.md` and runs the exact same staleness check on it. We do NOT
|
||||
* warn merely because the old filename is in use — an up-to-date `AGENTS.cli.md`
|
||||
* stays quiet; only a stale hash (which `wmill refresh prompts` fixes, and
|
||||
* which also migrates the filename) trips the warning.
|
||||
*/
|
||||
export async function warnIfPromptsStale(opts?: {
|
||||
cwd?: string;
|
||||
@@ -114,9 +124,18 @@ export async function warnIfPromptsStale(opts?: {
|
||||
if (opts?.argv && !shouldRunFreshnessCheck(opts.argv)) return;
|
||||
|
||||
const cwd = opts?.cwd ?? process.cwd();
|
||||
const path = `${cwd}/AGENTS.cli.md`;
|
||||
|
||||
if (!(await stat(path).catch(() => null))) return;
|
||||
// Prefer the current filename; fall back to the legacy one for back-compat.
|
||||
let fileName = AGENTS_WMILL_FILENAME;
|
||||
let path = `${cwd}/${fileName}`;
|
||||
if (!(await stat(path).catch(() => null))) {
|
||||
fileName = LEGACY_AGENTS_CLI_FILENAME;
|
||||
path = `${cwd}/${fileName}`;
|
||||
if (!(await stat(path).catch(() => null))) {
|
||||
// Neither file present — this project just isn't wmill-managed.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -127,10 +146,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
|
||||
// refresh and picks up the new format.
|
||||
// Managed file without a marker. Warn so the user re-runs refresh and
|
||||
// picks up the new format (and the new filename).
|
||||
emitWarning(
|
||||
"Your AGENTS.cli.md predates prompt versioning. Run `wmill refresh prompts` to refresh and add a version marker."
|
||||
`Your ${fileName} predates prompt versioning. Run \`wmill refresh prompts\` to refresh and add a version marker.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +171,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 ${fileName} 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
|
||||
\`\`\`
|
||||
@@ -5051,15 +5061,16 @@ data:
|
||||
|
||||
## CLI Commands
|
||||
|
||||
\`wmill app new\` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
|
||||
Two commands you run yourself, not the user:
|
||||
- \`wmill app new\` — run it with flags, per the "Creating a Raw App" section above.
|
||||
- \`wmill generate-metadata\` — generates local lock files; offer it and run it on consent, per "After creating a runnable" above (it writes local lock files, not a deploy).
|
||||
|
||||
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
|
||||
For the rest, tell the user which command fits their intent and let them run it — these deploy to the workspace, overwrite local files, or launch a long-running server, so the user should consent each time:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| \`wmill app dev\` | Start dev server with live reload (see the \`preview\` skill for the full open-the-app-in-the-IDE-pane procedure). |
|
||||
| \`wmill app generate-agents\` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| \`wmill generate-metadata\` | Generate lock files for backend runnables |
|
||||
| \`wmill sync push\` | Deploy app to Windmill |
|
||||
| \`wmill sync pull\` | Pull latest from Windmill |
|
||||
|
||||
@@ -6456,12 +6467,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 +6763,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>\` - Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch \`wmill workspace fork\` offers this interactively; from a base branch it creates a fresh fork branch.
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.
|
||||
- \`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
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
branchToForkId,
|
||||
validateForkWorkspaceId,
|
||||
} from "../src/commands/workspace/fork.ts";
|
||||
|
||||
describe("branchToForkId — branch name → fork workspace id slug", () => {
|
||||
test.each<[string, string]>([
|
||||
["feature-x", "feature-x"],
|
||||
["my_feature", "my_feature"],
|
||||
// `/` (common in branch names) must not survive — it would break the
|
||||
// wm-fork/<base>/<id> branch-name parsing.
|
||||
["feat/foo", "feat-foo"],
|
||||
["feature/JIRA-123", "feature-JIRA-123"],
|
||||
["a/b/c", "a-b-c"],
|
||||
// Other invalid characters collapse to a single dash.
|
||||
["hot fix!", "hot-fix"],
|
||||
["weird@@name", "weird-name"],
|
||||
// Leading/trailing separators are trimmed.
|
||||
["/leading", "leading"],
|
||||
["trailing/", "trailing"],
|
||||
["--dashes--", "dashes"],
|
||||
// Degenerate input falls back to a usable id.
|
||||
["///", "fork"],
|
||||
["", "fork"],
|
||||
])("%p → %p", (branch, expected) => {
|
||||
expect(branchToForkId(branch)).toBe(expected);
|
||||
});
|
||||
|
||||
test("result never contains a slash (would break fork branch parsing)", () => {
|
||||
for (const branch of ["a/b", "x/y/z", "feat/foo/bar"]) {
|
||||
expect(branchToForkId(branch)).not.toContain("/");
|
||||
}
|
||||
});
|
||||
|
||||
test("caps the slug so wm-fork-<slug> stays within the backend's 50-char limit", () => {
|
||||
const long = "feature/TICKET-1234-" + "a".repeat(80);
|
||||
const slug = branchToForkId(long);
|
||||
expect(slug.length).toBeLessThanOrEqual(42);
|
||||
// The full id the backend validates is `wm-fork-<slug>`.
|
||||
expect(`wm-fork-${slug}`.length).toBeLessThanOrEqual(50);
|
||||
// Truncation must not leave a trailing dash.
|
||||
expect(slug.endsWith("-")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateForkWorkspaceId — mirrors backend validate_fork_workspace_id", () => {
|
||||
test("accepts a normal slugged id", () => {
|
||||
expect(() => validateForkWorkspaceId("wm-fork-feature-x")).not.toThrow();
|
||||
});
|
||||
|
||||
test.each<[string, string]>([
|
||||
["too long (> 50 chars)", "wm-fork-" + "a".repeat(60)],
|
||||
["ends with '.'", "wm-fork-foo."],
|
||||
["ends with '.lock'", "wm-fork-foo.lock"],
|
||||
["contains '..'", "wm-fork-foo..bar"],
|
||||
["contains '//'", "wm-fork-foo//bar"],
|
||||
["contains a space", "wm-fork-foo bar"],
|
||||
["contains a forbidden char", "wm-fork-foo~bar"],
|
||||
])("rejects: %s", (_label, id) => {
|
||||
expect(() => validateForkWorkspaceId(id)).toThrow();
|
||||
});
|
||||
|
||||
test("a branchToForkId slug always passes validation (with the prefix)", () => {
|
||||
for (const branch of [
|
||||
"feat/foo",
|
||||
"feature/TICKET-1234-" + "a".repeat(80),
|
||||
"weird@@name",
|
||||
"///",
|
||||
]) {
|
||||
const id = `wm-fork-${branchToForkId(branch)}`;
|
||||
expect(() => validateForkWorkspaceId(id)).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
@@ -555,4 +614,64 @@ describe("prompts freshness — additional invariants", () => {
|
||||
expect(stdoutJoined).not.toContain("out of date");
|
||||
});
|
||||
});
|
||||
|
||||
// Back-compat: a legacy AGENTS.cli.md is still hash-checked, and we do NOT
|
||||
// warn merely because of the old filename — only when it's actually stale.
|
||||
test("warnIfPromptsStale stays silent for an up-to-date legacy AGENTS.cli.md", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
const content = injectPromptsHashMarker(
|
||||
"# Windmill CLI Agent Instructions\nbody\n",
|
||||
currentPromptsHash(false)
|
||||
);
|
||||
await writeFile(join(tempDir, "AGENTS.cli.md"), content, "utf8");
|
||||
|
||||
const stderrWrites: string[] = [];
|
||||
const originalStderr = process.stderr.write.bind(process.stderr);
|
||||
// @ts-expect-error — overriding write for the test
|
||||
process.stderr.write = (chunk: any) => {
|
||||
stderrWrites.push(String(chunk));
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await warnIfPromptsStale({
|
||||
cwd: tempDir,
|
||||
nonDottedPaths: false,
|
||||
argv: ["node", "wmill", "sync", "push"],
|
||||
});
|
||||
} finally {
|
||||
process.stderr.write = originalStderr;
|
||||
}
|
||||
expect(stderrWrites.join("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
test("warnIfPromptsStale warns (naming the legacy file) for a stale AGENTS.cli.md", async () => {
|
||||
await withTempDir(async (tempDir) => {
|
||||
await writeFile(
|
||||
join(tempDir, "AGENTS.cli.md"),
|
||||
"# Windmill CLI Agent Instructions\n<!-- wmill-prompts-hash: 000000000000 -->\nbody\n",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const stderrWrites: string[] = [];
|
||||
const originalStderr = process.stderr.write.bind(process.stderr);
|
||||
// @ts-expect-error — overriding write for the test
|
||||
process.stderr.write = (chunk: any) => {
|
||||
stderrWrites.push(String(chunk));
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await warnIfPromptsStale({
|
||||
cwd: tempDir,
|
||||
nonDottedPaths: false,
|
||||
argv: ["node", "wmill", "sync", "push"],
|
||||
});
|
||||
} finally {
|
||||
process.stderr.write = originalStderr;
|
||||
}
|
||||
const out = stderrWrites.join("");
|
||||
expect(out).toContain("out of date");
|
||||
expect(out).toContain("AGENTS.cli.md");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>` - Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch `wmill workspace fork` offers this interactively; from a base branch it creates a fresh fork branch.
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.
|
||||
- `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>\` - Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch \`wmill workspace fork\` offers this interactively; from a base branch it creates a fresh fork branch.
|
||||
- \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.
|
||||
- \`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>` - Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch `wmill workspace fork` offers this interactively; from a base branch it creates a fresh fork branch.
|
||||
- `-y --yes` - Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.
|
||||
- `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
|
||||
```
|
||||
@@ -217,15 +217,16 @@ data:
|
||||
|
||||
## CLI Commands
|
||||
|
||||
`wmill app new` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
|
||||
Two commands you run yourself, not the user:
|
||||
- `wmill app new` — run it with flags, per the "Creating a Raw App" section above.
|
||||
- `wmill generate-metadata` — generates local lock files; offer it and run it on consent, per "After creating a runnable" above (it writes local lock files, not a deploy).
|
||||
|
||||
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
|
||||
For the rest, tell the user which command fits their intent and let them run it — these deploy to the workspace, overwrite local files, or launch a long-running server, so the user should consent each time:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wmill app dev` | Start dev server with live reload (see the `preview` skill for the full open-the-app-in-the-IDE-pane procedure). |
|
||||
| `wmill app generate-agents` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| `wmill generate-metadata` | Generate lock files for backend runnables |
|
||||
| `wmill sync push` | Deploy app to Windmill |
|
||||
| `wmill sync pull` | Pull latest from Windmill |
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -212,15 +212,16 @@ data:
|
||||
|
||||
## CLI Commands
|
||||
|
||||
`wmill app new` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
|
||||
Two commands you run yourself, not the user:
|
||||
- `wmill app new` — run it with flags, per the "Creating a Raw App" section above.
|
||||
- `wmill generate-metadata` — generates local lock files; offer it and run it on consent, per "After creating a runnable" above (it writes local lock files, not a deploy).
|
||||
|
||||
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
|
||||
For the rest, tell the user which command fits their intent and let them run it — these deploy to the workspace, overwrite local files, or launch a long-running server, so the user should consent each time:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wmill app dev` | Start dev server with live reload (see the `preview` skill for the full open-the-app-in-the-IDE-pane procedure). |
|
||||
| `wmill app generate-agents` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| `wmill generate-metadata` | Generate lock files for backend runnables |
|
||||
| `wmill sync push` | Deploy app to Windmill |
|
||||
| `wmill sync pull` | Pull latest from Windmill |
|
||||
|
||||
|
||||
@@ -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