@ldlework/workmark
v1.4.2
Published
Most workspaces accumulate a graveyard of shell scripts, Makefiles, npm scripts, and "just run this" tribal knowledge. Workmark lets you define these workspace operations in TypeScript and run them from:
Downloads
82
Readme
@ldlework/workmark
Most workspaces accumulate a graveyard of shell scripts, Makefiles, npm scripts, and "just run this" tribal knowledge. Workmark lets you define these workspace operations in TypeScript and run them from:
- CLI — the
wmcommand, with auto-generated help and argument parsing - VS Code — a dashboard extension with auto-generated forms for every command/parameter
- AI Agents — a built-in MCP server so any MCP client can discover and run your commands
Quick start
Install
pnpm add @ldlework/workmarkWrite a command
Create commands in .wm/commands/. Subdirectories become groups in the CLI, dashboard, and MCP server. The filename becomes the command name, a leading JSDoc becomes the description, and a string return is executed as a shell command in the workspace root.
// .wm/commands/art/sprites.ts
/** Pack sprite sheets from raw assets */
import { cmd } from "@ldlework/workmark/define";
import { z } from "zod";
export default cmd({
args: {
target: z.enum(["all", "characters", "terrain", "ui"]).default("all"),
},
flags: {
watch: z.boolean().default(false),
},
handler: ({ target, watch }) =>
`./tools/pack-sprites.sh ${target}${watch ? " --watch" : ""}`,
});For a bare shell alias:
// .wm/commands/build.ts
/** Build the project */
import { cmd } from "@ldlework/workmark/define";
export default cmd({ handler: () => "cargo build" });Run it
wm sprites # pack all sprite sheets
wm sprites characters --watch # pack characters, rebuild on change
wm --help # list all commands
wm sprites --help # per-command helpProjects
If your workspace contains multiple packages or services, you can define projects so that commands can discover and operate on them. Drop a wm.ts in any project directory:
// packages/api/wm.ts
import { defineProject } from "@ldlework/workmark/define";
export default defineProject({
name: "api",
tags: ["backend"],
});// packages/web/wm.ts
import { defineProject } from "@ldlework/workmark/define";
export default defineProject({
name: "web",
tags: ["frontend"],
});Workmark recursively discovers all wm.ts files from the workspace root (respecting .gitignore). Projects are available to commands via the workspace object — query them by name with workspace.get("api"), by tag with workspace.withTag("backend"), or by capability with workspace.withCapability("deploy").
Capabilities
Capabilities are structured metadata attached to projects. A project declares what it supports, and commands filter by it — this keeps project-specific config out of your command files. For example, marking a project with deploy: true advertises that it has the scripts/deploy.sh script that the deploy command expects.
// packages/api/wm.ts
export default defineProject({
name: "api",
tags: ["backend"],
capabilities: { deploy: true },
});// packages/web/wm.ts
export default defineProject({
name: "web",
tags: ["frontend"],
capabilities: { deploy: true },
});workspace.withCapability("deploy") returns both projects. Capabilities can also carry structured data:
export default defineProject({
name: "api",
capabilities: {
deploy: true,
build: { targets: ["esm", "cjs"] },
},
});Commands read this with project.capability<{ targets: string[] }>("build"). This pattern works for anything — build targets, test runners, linting rules.
Dynamic commands
A dynamic command receives the workspace and builds its schema from project metadata. Here, the deploy command finds all projects with the deploy capability and populates its argument from their names:
// .wm/commands/deploy.ts
/** Deploy a project */
import { z } from "zod";
import type { DynamicCommandDef } from "@ldlework/workmark/types";
export default {
factory: (workspace) => {
const names = workspace.withCapability("deploy").map((p) => p.name);
return {
args: { project: z.enum(names as [string, ...string[]]) },
handler: ({ project }) => {
const p = workspace.get(project as string);
return { content: [{ type: "text", text: `deploying ${p.name}` }] };
// or: return `./scripts/deploy.sh` (with cwd resolved to workspace root by default)
},
};
},
} satisfies DynamicCommandDef;wm deploy api
wm deploy webThe CLI help, VS Code dropdowns, and MCP tool schema all show exactly the valid project names — no impossible combinations, no runtime validation needed.
Features
CLI
The wm binary auto-discovers your commands and generates help text, argument parsing, and type coercion.
$ wm --help
Usage: wm <command> [args...]
Commands:
sprites Pack sprite sheets from raw assets
deploy Deploy a project to an environment
db:migrate Run database migrations
Run wm <command> --help for details on a specific command.Arguments support positional args and named flags:
wm sprites characters # positional (from args)
wm sprites characters --watch # positional + flag
wm deploy api # dynamic command with project arg
wm deploy --project api # everything works as flags tooVS Code dashboard
Install the workmark-vsc extension. It adds a Workspace panel to the activity bar showing all your commands grouped by category with auto-generated forms:
- Enum fields become dropdowns
- Booleans become checkboxes
- Numbers get validated inputs with min/max
- Required fields are enforced before execution
- Double-click any command to jump to its source file
Commands run in the integrated terminal, so you get full color output and interactivity.
MCP server
Workmark includes a built-in Model Context Protocol server. Every command you define is automatically exposed as an MCP tool, which means AI assistants like Claude can discover and invoke your workspace commands.
// claude_desktop_config.json or .mcp.json
{
"mcpServers": {
"workspace": {
"command": "node",
"args": ["./node_modules/@ldlework/workmark/dist/index.js"]
}
}
}Once connected, your assistant can run wm deploy api the same way you do — with full schema validation and typed responses.
Project structure
your-workspace/
├── .wm/
│ └── commands/
│ ├── deploy.ts # Root-level command
│ ├── art/
│ │ └── sprites.ts # Grouped under "Art"
│ └── db/
│ └── migrate.ts # Grouped under "Db"
├── packages/
│ ├── api/
│ │ └── wm.ts # Project definition
│ └── web/
│ └── wm.tswm.ts— project definitions, discovered recursively.wm/commands/**/*.ts— command files, grouped by directory name
API reference
Defining
import { defineProject } from "@ldlework/workmark/define";Helpers
import { ok, fail, exec, execAsync } from "@ldlework/workmark/helpers";
ok(data) // Wrap data in a success CallToolResult
fail(error) // Wrap error in an error CallToolResult
exec(cmd, { cwd }) // Synchronous shell exec, returns CallToolResult
execAsync(cmd, { cwd }) // Async shell exec, returns Promise<CallToolResult>
execRaw(cmd, { cwd }) // Synchronous shell exec, returns string (throws on error)
execAsyncRaw(cmd, { cwd }) // Async shell exec, returns Promise<string> (throws on error)Loading
import { loadWorkspace } from "@ldlework/workmark/workspace";
import { loadCommands } from "@ldlework/workmark";
const workspace = await loadWorkspace();
const commands = await loadCommands(workspace);License
MIT
