mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
* 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>
287 lines
8.3 KiB
TypeScript
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;
|