Files
windmill/cli/src/commands/app/generate_agents.ts
T
Ruben FiszelandClaude Opus 4.6 4fedfdfd11 feat(cli): add consistent get/list/new subcommands for all item types (#8047)
* feat(cli): add consistent get/list/new subcommands for all item types

Make the CLI consistent so every item type (script, flow, app, resource,
resource-type, variable, schedule, folder, trigger) supports get/list/new
subcommands, enabling the CLI to be used as a full API client in bash
scripts with jq piping.

- Add --json flag to all list commands for machine-readable output
- Register explicit "list" subcommand alongside default action
- Add "get <path> [--json]" subcommand to fetch single items from API
- Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias
- Add "new" subcommand for resource, resource-type, variable, schedule,
  folder, and trigger to create local template YAML files
- Update cli-commands skill documentation for wmill init
- Add integration tests for all new commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* feat: install wmill CLI in Docker images and use it for bash variable/resource access

- Install windmill-cli via bun in all Dockerfiles that include bun
- DockerfileCli: switch from node:slim to oven/bun:slim
- CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars
  as last-resort fallback when no workspace is configured
- Frontend: replace curl-based bash snippets with wmill variable/resource get
- Add backend integration tests for wmill CLI in bash scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): install windmill-cli in backend test workflow

Ensures wmill is available on PATH for bash integration tests
that use `wmill variable get` and `wmill resource get`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): replace @std/* Deno dependencies with Node.js equivalents

Replace @std/log with a lightweight custom logger (core/log.ts),
@std/path with node:path, and @std/yaml with the yaml npm package.
Also fix process hang on exit, add --node option to install_dev.sh,
and add missing hasRequiredPermissions to NpmProvider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* all

* refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives

Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation,
replace @std/encoding with Node.js Buffer.toString("hex"), and fix
@windmill-labs/shared-utils to use direct npm instead of JSR mirror.
Also resolve merge conflicts in sync.ts and fix pre-existing type errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull

The yaml library defaults to double quotes, but the codebase (and tests)
expect single-quoted strings. Add singleQuote: true to yamlOptions and
pass yamlOptions to gitsync-settings pull writeFile calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* fix(cli): address code review feedback

- Install CLI from source in backend tests instead of npm
- Fix script bootstrap catch block to re-throw "File already exists"
- Add type-safe local variable after trigger kind validation
- Use created_by instead of policy.on_behalf_of for app get output
- Note --kind is recommended for faster trigger lookup in help text
- Document node symlink purpose in Dockerfiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): install wmill to ~/.local/bin to avoid permission issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(backend): switch to Blacksmith runner and add cargo caching

- Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds
- Add stickydisk for cargo target directory (persistent NVMe cache across runs)
- Add cache for cargo registry and git dependencies
- Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1
- Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix ci

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:53:28 +00:00

287 lines
8.3 KiB
TypeScript

import * as fs from "node:fs";
import { writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import * as log from "../../core/log.ts";
import { yamlParseFile } from "../../utils/yaml.ts";
import { GlobalOptions } from "../../types.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { requireLogin } from "../../core/auth.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { DataTableSchema } from "../../../gen/types.gen.ts";
import { generateAgentsDocumentation } from "../sync/sync.ts";
import {
getFolderSuffix,
hasFolderSuffix,
loadNonDottedPathsSetting,
} from "../../utils/resource_folders.ts";
interface GenerateAgentsOptions extends GlobalOptions {
output?: string;
}
/**
* Generates DATATABLES.md content from remote workspace datatable schemas.
* This file focuses on schema information. For full instructions, see AGENTS.md.
*/
function generateDatatablesMarkdown(
schemas: DataTableSchema[],
localData?: {
tables?: string[];
datatable?: string;
schema?: string;
}
): string {
const defaultDatatable = localData?.datatable;
const defaultSchema = localData?.schema;
const tables = localData?.tables ?? [];
let content = `# Data Tables
This file contains database schema information for this app.
**For full instructions on using datatables, see \`AGENTS.md\`.**
## ⚠️ IMPORTANT
**You can ONLY use tables listed in \`data.tables\` in \`raw_app.yaml\`.**
To use a table from the schemas below, first add it to the whitelist.
## Current Configuration
${defaultDatatable
? `**Default Datatable:** \`${defaultDatatable}\`${defaultSchema ? ` | **Default Schema:** \`${defaultSchema}\`` : ''}`
: `**No default datatable configured.** Set \`data.datatable\` in \`raw_app.yaml\`.`}
### Whitelisted Tables (You Can Use These)
${tables.length > 0
? tables.map((t) => `- \`${t}\``).join("\n")
: `*No tables whitelisted yet.*`}
### To Add a Table
Edit \`raw_app.yaml\`:
\`\`\`yaml
data:
datatable: ${defaultDatatable || 'main'}
tables:
${tables.length > 0 ? tables.map((t) => ` - ${t}`).join("\n") : ' # Add tables here'}
- ${defaultDatatable || 'main'}/${defaultSchema ? defaultSchema + ':' : ''}table_name # ← Add like this
\`\`\`
---
## Available Schemas
`;
if (schemas.length === 0) {
content += `*No datatables configured in this workspace.*
Configure datatables in Workspace Settings > Windmill Data Tables.
`;
} else {
for (const dt of schemas) {
const isDefault = dt.datatable_name === defaultDatatable;
content += `### ${isDefault ? '★ ' : ''}Datatable: \`${dt.datatable_name}\`${isDefault ? ' (default)' : ''}
`;
if (dt.error) {
content += `> ⚠️ Error loading schema: ${dt.error}
`;
continue;
}
if (!dt.schemas || Object.keys(dt.schemas).length === 0) {
content += `*No schemas found.*
`;
continue;
}
for (const [schemaName, schemaTables] of Object.entries(dt.schemas)) {
const isDefaultSchema = schemaName === defaultSchema;
content += `#### ${isDefaultSchema ? '★ ' : ''}Schema: \`${schemaName}\`${isDefaultSchema ? ' (default)' : ''}
`;
if (!schemaTables || Object.keys(schemaTables).length === 0) {
content += `*No tables.*
`;
continue;
}
for (const [tableName, columns] of Object.entries(schemaTables)) {
const fullTableRef = schemaName === 'public'
? `${dt.datatable_name}/${tableName}`
: `${dt.datatable_name}/${schemaName}:${tableName}`;
const isWhitelisted = tables.some(t =>
t === fullTableRef ||
t === `${dt.datatable_name}/${schemaName}.${tableName}` ||
t === dt.datatable_name
);
content += `**\`${tableName}\`**${isWhitelisted ? ' ✓' : ''} → add as \`${fullTableRef}\`
| Column | Type |
|--------|------|
`;
for (const [colName, colType] of Object.entries(columns)) {
content += `| ${colName} | \`${colType}\` |
`;
}
content += `
`;
}
}
}
}
content += `---
*Generated by \`wmill app generate-agents\`. See \`AGENTS.md\` for full instructions.*
`;
return content;
}
/**
* Regenerates AGENTS.md and DATATABLES.md for a raw app.
* Can be called from CLI or programmatically (e.g., after SQL migration).
*/
export async function regenerateAgentDocs(
workspaceId: string,
targetDir: string,
silent = false
): Promise<void> {
// Check for raw_app.yaml
const rawAppPath = path.join(targetDir, "raw_app.yaml");
if (!fs.existsSync(rawAppPath)) {
if (!silent) {
log.error(colors.red(`Error: raw_app.yaml not found in ${targetDir}`));
}
return;
}
if (!silent) {
log.info(colors.cyan("Refreshing agent documentation..."));
}
// Fetch schemas
let schemas: DataTableSchema[] = [];
try {
schemas = await wmill.listDataTableSchemas({ workspace: workspaceId });
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (!silent) {
log.warn(colors.yellow(`Could not fetch datatable schemas: ${errorMessage}`));
}
}
// Read local data configuration from raw_app.yaml
let localData: { tables?: string[]; datatable?: string; schema?: string } | undefined;
try {
const rawApp = (await yamlParseFile(rawAppPath)) as Record<string, unknown>;
if (rawApp.data && typeof rawApp.data === "object") {
localData = rawApp.data as typeof localData;
}
} catch {
// Ignore errors reading raw_app.yaml
}
// Generate and write AGENTS.md
const agentsContent = generateAgentsDocumentation(localData);
await writeFile(path.join(targetDir, "AGENTS.md"), agentsContent, "utf-8");
// Generate and write CLAUDE.md referencing AGENTS.md
await writeFile(path.join(targetDir, "CLAUDE.md"), `Instructions are in @AGENTS.md\n`, "utf-8");
// Generate and write DATATABLES.md
const datatablesContent = generateDatatablesMarkdown(schemas, localData);
await writeFile(path.join(targetDir, "DATATABLES.md"), datatablesContent, "utf-8");
if (!silent) {
log.info(colors.green(`✓ Generated AGENTS.md, CLAUDE.md, and DATATABLES.md`));
// Summary
const datatableCount = schemas.length;
const tableCount = schemas.reduce((acc, dt) => {
if (!dt.schemas) return acc;
return (
acc +
Object.values(dt.schemas).reduce((schemaAcc, tables) => {
return schemaAcc + Object.keys(tables || {}).length;
}, 0)
);
}, 0);
log.info(colors.gray(` Found ${datatableCount} datatable(s) with ${tableCount} table(s)`));
if (localData?.datatable) {
log.info(colors.gray(` App configured for datatable: ${localData.datatable}`));
}
}
}
async function generateAgents(
opts: GenerateAgentsOptions,
appFolder?: string
) {
// Resolve the app folder
const cwd = process.cwd();
let targetDir = cwd;
if (appFolder) {
targetDir = path.isAbsolute(appFolder)
? appFolder
: path.join(cwd, appFolder);
}
// Load nonDottedPaths setting before using folder suffix functions
await loadNonDottedPathsSetting();
// Ensure we're in a raw_app folder or targeting one
const dirName = path.basename(targetDir);
if (!hasFolderSuffix(dirName, "raw_app")) {
// Check if current directory is a raw_app folder
if (!hasFolderSuffix(path.basename(cwd), "raw_app") && !appFolder) {
log.error(
colors.red(
`Error: Must be run inside a ${getFolderSuffix("raw_app")} folder or specify one as argument.`
)
);
log.info(colors.gray("Usage: wmill app generate-agents [app_folder]"));
process.exit(1);
}
}
// Check for raw_app.yaml
const rawAppPath = path.join(targetDir, "raw_app.yaml");
if (!fs.existsSync(rawAppPath)) {
log.error(
colors.red(`Error: raw_app.yaml not found in ${targetDir}`)
);
process.exit(1);
}
// Resolve workspace and authenticate
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await regenerateAgentDocs(workspace.workspaceId, targetDir);
}
const command = new Command()
.description("regenerate AGENTS.md and DATATABLES.md from remote workspace")
.arguments("[app_folder:string]")
.action(generateAgents as any);
export default command;