@ldlework/workmark 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -0
- package/dist/lib/load.js +18 -18
- package/package.json +1 -1
- package/src/lib/load.ts +15 -19
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# @ldlework/workmark
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
- **CLI** — the `wm` command, with auto-generated help and argument parsing
|
|
6
|
+
- **VS Code** — a dashboard extension with auto-generated forms for every command/parameter
|
|
7
|
+
- **AI Agents** — a built-in MCP server so any MCP client can discover and run your commands
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
### Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @ldlework/workmark
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Write a command
|
|
18
|
+
|
|
19
|
+
Create commands in `.wm/commands/`. Subdirectories become groups in the CLI, dashboard and MCP server.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// .wm/commands/art/sprites.ts
|
|
23
|
+
import { exec } from "@ldlework/workmark/helpers";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import type { CommandDef } from "@ldlework/workmark/types";
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
name: "sprites",
|
|
29
|
+
label: "Build Sprites",
|
|
30
|
+
description: "Pack sprite sheets from raw assets",
|
|
31
|
+
args: {
|
|
32
|
+
target: z.enum(["all", "characters", "terrain", "ui"]).default("all"),
|
|
33
|
+
},
|
|
34
|
+
flags: {
|
|
35
|
+
watch: z.boolean().default(false),
|
|
36
|
+
},
|
|
37
|
+
handler: async ({ target, watch }) => {
|
|
38
|
+
const cmd = `./tools/pack-sprites.sh ${target}${watch ? " --watch" : ""}`;
|
|
39
|
+
return exec(cmd, { cwd: process.cwd() });
|
|
40
|
+
},
|
|
41
|
+
} satisfies CommandDef;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Run it
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
wm sprites # pack all sprite sheets
|
|
48
|
+
wm sprites characters --watch # pack characters, rebuild on change
|
|
49
|
+
wm --help # list all commands
|
|
50
|
+
wm sprites --help # per-command help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Projects
|
|
54
|
+
|
|
55
|
+
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:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// packages/api/wm.ts
|
|
59
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
60
|
+
|
|
61
|
+
export default defineProject({
|
|
62
|
+
name: "api",
|
|
63
|
+
tags: ["backend"],
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// packages/web/wm.ts
|
|
69
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
70
|
+
|
|
71
|
+
export default defineProject({
|
|
72
|
+
name: "web",
|
|
73
|
+
tags: ["frontend"],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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")`.
|
|
78
|
+
|
|
79
|
+
### Capabilities
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// packages/api/wm.ts
|
|
85
|
+
export default defineProject({
|
|
86
|
+
name: "api",
|
|
87
|
+
tags: ["backend"],
|
|
88
|
+
capabilities: { deploy: true },
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// packages/web/wm.ts
|
|
94
|
+
export default defineProject({
|
|
95
|
+
name: "web",
|
|
96
|
+
tags: ["frontend"],
|
|
97
|
+
capabilities: { deploy: true },
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`workspace.withCapability("deploy")` returns both projects. Capabilities can also carry structured data:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export default defineProject({
|
|
105
|
+
name: "api",
|
|
106
|
+
capabilities: {
|
|
107
|
+
deploy: true,
|
|
108
|
+
build: { targets: ["esm", "cjs"] },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Commands read this with `project.capability<{ targets: string[] }>("build")`. This pattern works for anything — build targets, test runners, linting rules.
|
|
114
|
+
|
|
115
|
+
### Dynamic commands
|
|
116
|
+
|
|
117
|
+
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:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// .wm/commands/deploy.ts
|
|
121
|
+
import { exec } from "@ldlework/workmark/helpers";
|
|
122
|
+
import { z } from "zod";
|
|
123
|
+
import type { DynamicCommandDef } from "@ldlework/workmark/types";
|
|
124
|
+
|
|
125
|
+
export default {
|
|
126
|
+
name: "deploy",
|
|
127
|
+
label: "Deploy",
|
|
128
|
+
description: "Deploy a project",
|
|
129
|
+
factory: (workspace) => {
|
|
130
|
+
const projects = workspace.withCapability("deploy");
|
|
131
|
+
const names = projects.map((p) => p.name);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
args: {
|
|
135
|
+
project: z.enum(names as [string, ...string[]]),
|
|
136
|
+
},
|
|
137
|
+
handler: async ({ project }) => {
|
|
138
|
+
const p = workspace.get(project as string);
|
|
139
|
+
return exec(`./scripts/deploy.sh`, { cwd: p.dir });
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
} satisfies DynamicCommandDef;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
wm deploy api
|
|
148
|
+
wm deploy web
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The CLI help, VS Code dropdowns, and MCP tool schema all show exactly the valid project names — no impossible combinations, no runtime validation needed.
|
|
152
|
+
|
|
153
|
+
## Features
|
|
154
|
+
|
|
155
|
+
### CLI
|
|
156
|
+
|
|
157
|
+
The `wm` binary auto-discovers your commands and generates help text, argument parsing, and type coercion.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
$ wm --help
|
|
161
|
+
|
|
162
|
+
Usage: wm <command> [args...]
|
|
163
|
+
|
|
164
|
+
Commands:
|
|
165
|
+
sprites Pack sprite sheets from raw assets
|
|
166
|
+
deploy Deploy a project to an environment
|
|
167
|
+
db:migrate Run database migrations
|
|
168
|
+
|
|
169
|
+
Run wm <command> --help for details on a specific command.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Arguments support positional args and named flags:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
wm sprites characters # positional (from args)
|
|
176
|
+
wm sprites characters --watch # positional + flag
|
|
177
|
+
wm deploy api # dynamic command with project arg
|
|
178
|
+
wm deploy --project api # everything works as flags too
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### VS Code dashboard
|
|
182
|
+
|
|
183
|
+
Install the [`workmark-vsc`](https://marketplace.visualstudio.com/items?itemName=ldlework.workmark-vsc) extension. It adds a **Workspace** panel to the activity bar showing all your commands grouped by category with auto-generated forms:
|
|
184
|
+
|
|
185
|
+
- Enum fields become dropdowns
|
|
186
|
+
- Booleans become checkboxes
|
|
187
|
+
- Numbers get validated inputs with min/max
|
|
188
|
+
- Required fields are enforced before execution
|
|
189
|
+
- Double-click any command to jump to its source file
|
|
190
|
+
|
|
191
|
+
Commands run in the integrated terminal, so you get full color output and interactivity.
|
|
192
|
+
|
|
193
|
+
### MCP server
|
|
194
|
+
|
|
195
|
+
Workmark includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) 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.
|
|
196
|
+
|
|
197
|
+
```jsonc
|
|
198
|
+
// claude_desktop_config.json or .mcp.json
|
|
199
|
+
{
|
|
200
|
+
"mcpServers": {
|
|
201
|
+
"workspace": {
|
|
202
|
+
"command": "node",
|
|
203
|
+
"args": ["./node_modules/@ldlework/workmark/dist/index.js"]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Once connected, your assistant can run `wm deploy api` the same way you do — with full schema validation and typed responses.
|
|
210
|
+
|
|
211
|
+
## Project structure
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
your-workspace/
|
|
215
|
+
├── .wm/
|
|
216
|
+
│ └── commands/
|
|
217
|
+
│ ├── deploy.ts # Root-level command
|
|
218
|
+
│ ├── art/
|
|
219
|
+
│ │ └── sprites.ts # Grouped under "Art"
|
|
220
|
+
│ └── db/
|
|
221
|
+
│ └── migrate.ts # Grouped under "Db"
|
|
222
|
+
├── packages/
|
|
223
|
+
│ ├── api/
|
|
224
|
+
│ │ └── wm.ts # Project definition
|
|
225
|
+
│ └── web/
|
|
226
|
+
│ └── wm.ts
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- **`wm.ts`** — project definitions, discovered recursively
|
|
230
|
+
- **`.wm/commands/**/*.ts`** — command files, grouped by directory name
|
|
231
|
+
|
|
232
|
+
## API reference
|
|
233
|
+
|
|
234
|
+
### Defining
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Helpers
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { ok, fail, exec, execAsync } from "@ldlework/workmark/helpers";
|
|
244
|
+
|
|
245
|
+
ok(data) // Wrap data in a success CallToolResult
|
|
246
|
+
fail(error) // Wrap error in an error CallToolResult
|
|
247
|
+
exec(cmd, { cwd }) // Synchronous shell exec, returns CallToolResult
|
|
248
|
+
execAsync(cmd, { cwd }) // Async shell exec, returns Promise<CallToolResult>
|
|
249
|
+
execRaw(cmd, { cwd }) // Synchronous shell exec, returns string (throws on error)
|
|
250
|
+
execAsyncRaw(cmd, { cwd }) // Async shell exec, returns Promise<string> (throws on error)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Loading
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { loadWorkspace } from "@ldlework/workmark/workspace";
|
|
257
|
+
import { loadCommands } from "@ldlework/workmark";
|
|
258
|
+
|
|
259
|
+
const workspace = await loadWorkspace();
|
|
260
|
+
const commands = await loadCommands(workspace);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
package/dist/lib/load.js
CHANGED
|
@@ -17,33 +17,33 @@ function fieldToJsonSchema(field) {
|
|
|
17
17
|
}
|
|
18
18
|
return field;
|
|
19
19
|
}
|
|
20
|
+
/** A field is required unless it has a default or was wrapped with .optional(). */
|
|
21
|
+
function isRequired(field, schema) {
|
|
22
|
+
if ("default" in schema)
|
|
23
|
+
return false;
|
|
24
|
+
if (field instanceof z.ZodType && field.safeParse(undefined).success)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
20
28
|
/** Merge args + flags into a single JSON Schema object and return positional names. */
|
|
21
29
|
function mergeSchemas(args, flags) {
|
|
22
30
|
const properties = {};
|
|
23
31
|
const required = [];
|
|
24
32
|
const positional = [];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
properties[name] = schema;
|
|
30
|
-
positional.push(name);
|
|
31
|
-
// If no default and not explicitly optional, it's required
|
|
32
|
-
if (!("default" in schema)) {
|
|
33
|
-
required.push(name);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// Flags are named (--key value)
|
|
38
|
-
if (flags) {
|
|
39
|
-
for (const [name, field] of Object.entries(flags)) {
|
|
33
|
+
const collect = (source, isPositional) => {
|
|
34
|
+
if (!source)
|
|
35
|
+
return;
|
|
36
|
+
for (const [name, field] of Object.entries(source)) {
|
|
40
37
|
const schema = fieldToJsonSchema(field);
|
|
41
38
|
properties[name] = schema;
|
|
42
|
-
if (
|
|
39
|
+
if (isPositional)
|
|
40
|
+
positional.push(name);
|
|
41
|
+
if (isRequired(field, schema))
|
|
43
42
|
required.push(name);
|
|
44
|
-
}
|
|
45
43
|
}
|
|
46
|
-
}
|
|
44
|
+
};
|
|
45
|
+
collect(args, true);
|
|
46
|
+
collect(flags, false);
|
|
47
47
|
const inputSchema = {
|
|
48
48
|
type: "object",
|
|
49
49
|
properties,
|
package/package.json
CHANGED
package/src/lib/load.ts
CHANGED
|
@@ -22,6 +22,13 @@ function fieldToJsonSchema(field: z.ZodType | Record<string, unknown>): Record<s
|
|
|
22
22
|
return field;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** A field is required unless it has a default or was wrapped with .optional(). */
|
|
26
|
+
function isRequired(field: z.ZodType | Record<string, unknown>, schema: Record<string, unknown>): boolean {
|
|
27
|
+
if ("default" in schema) return false;
|
|
28
|
+
if (field instanceof z.ZodType && field.safeParse(undefined).success) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/** Merge args + flags into a single JSON Schema object and return positional names. */
|
|
26
33
|
function mergeSchemas(
|
|
27
34
|
args?: SchemaFields,
|
|
@@ -31,29 +38,18 @@ function mergeSchemas(
|
|
|
31
38
|
const required: string[] = [];
|
|
32
39
|
const positional: string[] = [];
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for (const [name, field] of Object.entries(
|
|
41
|
+
const collect = (source: SchemaFields | undefined, isPositional: boolean) => {
|
|
42
|
+
if (!source) return;
|
|
43
|
+
for (const [name, field] of Object.entries(source)) {
|
|
37
44
|
const schema = fieldToJsonSchema(field);
|
|
38
45
|
properties[name] = schema;
|
|
39
|
-
positional.push(name);
|
|
40
|
-
|
|
41
|
-
if (!("default" in schema)) {
|
|
42
|
-
required.push(name);
|
|
43
|
-
}
|
|
46
|
+
if (isPositional) positional.push(name);
|
|
47
|
+
if (isRequired(field, schema)) required.push(name);
|
|
44
48
|
}
|
|
45
|
-
}
|
|
49
|
+
};
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const [name, field] of Object.entries(flags)) {
|
|
50
|
-
const schema = fieldToJsonSchema(field);
|
|
51
|
-
properties[name] = schema;
|
|
52
|
-
if (!("default" in schema)) {
|
|
53
|
-
required.push(name);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
51
|
+
collect(args, true);
|
|
52
|
+
collect(flags, false);
|
|
57
53
|
|
|
58
54
|
const inputSchema: Record<string, unknown> = {
|
|
59
55
|
type: "object",
|