@ldlework/workmark 1.3.0 → 1.4.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 +263 -0
- package/dist/cli.js +9 -2
- package/dist/lib/define.d.ts +71 -2
- package/dist/lib/define.js +87 -1
- package/dist/lib/helpers.d.ts +3 -0
- package/dist/lib/helpers.js +20 -2
- package/dist/lib/load.d.ts +3 -2
- package/dist/lib/load.js +439 -66
- package/dist/lib/parse.d.ts +2 -4
- package/dist/lib/parse.js +42 -10
- package/dist/lib/project.d.ts +10 -5
- package/dist/lib/project.js +45 -12
- package/dist/lib/registry.d.ts +15 -0
- package/dist/lib/registry.js +64 -0
- package/dist/lib/types.d.ts +86 -35
- package/dist/lib/types.js +5 -1
- package/dist/lib/workspace.d.ts +6 -3
- package/dist/lib/workspace.js +61 -24
- package/package.json +1 -1
- package/src/cli.ts +8 -2
- package/src/lib/define.ts +180 -2
- package/src/lib/helpers.ts +24 -2
- package/src/lib/load.ts +564 -72
- package/src/lib/parse.ts +47 -10
- package/src/lib/project.ts +58 -13
- package/src/lib/registry.ts +80 -0
- package/src/lib/types.ts +99 -41
- package/src/lib/workspace.ts +67 -30
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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. 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.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// .wm/commands/art/sprites.ts
|
|
23
|
+
/** Pack sprite sheets from raw assets */
|
|
24
|
+
import { cmd } from "@ldlework/workmark/define";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
|
|
27
|
+
export default cmd({
|
|
28
|
+
args: {
|
|
29
|
+
target: z.enum(["all", "characters", "terrain", "ui"]).default("all"),
|
|
30
|
+
},
|
|
31
|
+
flags: {
|
|
32
|
+
watch: z.boolean().default(false),
|
|
33
|
+
},
|
|
34
|
+
handler: ({ target, watch }) =>
|
|
35
|
+
`./tools/pack-sprites.sh ${target}${watch ? " --watch" : ""}`,
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For a bare shell alias:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// .wm/commands/build.ts
|
|
43
|
+
/** Build the project */
|
|
44
|
+
import { cmd } from "@ldlework/workmark/define";
|
|
45
|
+
export default cmd({ handler: () => "cargo build" });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Run it
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
wm sprites # pack all sprite sheets
|
|
52
|
+
wm sprites characters --watch # pack characters, rebuild on change
|
|
53
|
+
wm --help # list all commands
|
|
54
|
+
wm sprites --help # per-command help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Projects
|
|
58
|
+
|
|
59
|
+
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:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// packages/api/wm.ts
|
|
63
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
64
|
+
|
|
65
|
+
export default defineProject({
|
|
66
|
+
name: "api",
|
|
67
|
+
tags: ["backend"],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// packages/web/wm.ts
|
|
73
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
74
|
+
|
|
75
|
+
export default defineProject({
|
|
76
|
+
name: "web",
|
|
77
|
+
tags: ["frontend"],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
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")`.
|
|
82
|
+
|
|
83
|
+
### Capabilities
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// packages/api/wm.ts
|
|
89
|
+
export default defineProject({
|
|
90
|
+
name: "api",
|
|
91
|
+
tags: ["backend"],
|
|
92
|
+
capabilities: { deploy: true },
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// packages/web/wm.ts
|
|
98
|
+
export default defineProject({
|
|
99
|
+
name: "web",
|
|
100
|
+
tags: ["frontend"],
|
|
101
|
+
capabilities: { deploy: true },
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`workspace.withCapability("deploy")` returns both projects. Capabilities can also carry structured data:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
export default defineProject({
|
|
109
|
+
name: "api",
|
|
110
|
+
capabilities: {
|
|
111
|
+
deploy: true,
|
|
112
|
+
build: { targets: ["esm", "cjs"] },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Commands read this with `project.capability<{ targets: string[] }>("build")`. This pattern works for anything — build targets, test runners, linting rules.
|
|
118
|
+
|
|
119
|
+
### Dynamic commands
|
|
120
|
+
|
|
121
|
+
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:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// .wm/commands/deploy.ts
|
|
125
|
+
/** Deploy a project */
|
|
126
|
+
import { z } from "zod";
|
|
127
|
+
import type { DynamicCommandDef } from "@ldlework/workmark/types";
|
|
128
|
+
|
|
129
|
+
export default {
|
|
130
|
+
factory: (workspace) => {
|
|
131
|
+
const names = workspace.withCapability("deploy").map((p) => p.name);
|
|
132
|
+
return {
|
|
133
|
+
args: { project: z.enum(names as [string, ...string[]]) },
|
|
134
|
+
handler: ({ project }) => {
|
|
135
|
+
const p = workspace.get(project as string);
|
|
136
|
+
return { content: [{ type: "text", text: `deploying ${p.name}` }] };
|
|
137
|
+
// or: return `./scripts/deploy.sh` (with cwd resolved to workspace root by default)
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
} satisfies DynamicCommandDef;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
wm deploy api
|
|
146
|
+
wm deploy web
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The CLI help, VS Code dropdowns, and MCP tool schema all show exactly the valid project names — no impossible combinations, no runtime validation needed.
|
|
150
|
+
|
|
151
|
+
## Features
|
|
152
|
+
|
|
153
|
+
### CLI
|
|
154
|
+
|
|
155
|
+
The `wm` binary auto-discovers your commands and generates help text, argument parsing, and type coercion.
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
$ wm --help
|
|
159
|
+
|
|
160
|
+
Usage: wm <command> [args...]
|
|
161
|
+
|
|
162
|
+
Commands:
|
|
163
|
+
sprites Pack sprite sheets from raw assets
|
|
164
|
+
deploy Deploy a project to an environment
|
|
165
|
+
db:migrate Run database migrations
|
|
166
|
+
|
|
167
|
+
Run wm <command> --help for details on a specific command.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Arguments support positional args and named flags:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
wm sprites characters # positional (from args)
|
|
174
|
+
wm sprites characters --watch # positional + flag
|
|
175
|
+
wm deploy api # dynamic command with project arg
|
|
176
|
+
wm deploy --project api # everything works as flags too
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### VS Code dashboard
|
|
180
|
+
|
|
181
|
+
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:
|
|
182
|
+
|
|
183
|
+
- Enum fields become dropdowns
|
|
184
|
+
- Booleans become checkboxes
|
|
185
|
+
- Numbers get validated inputs with min/max
|
|
186
|
+
- Required fields are enforced before execution
|
|
187
|
+
- Double-click any command to jump to its source file
|
|
188
|
+
|
|
189
|
+
Commands run in the integrated terminal, so you get full color output and interactivity.
|
|
190
|
+
|
|
191
|
+
### MCP server
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
// claude_desktop_config.json or .mcp.json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"workspace": {
|
|
200
|
+
"command": "node",
|
|
201
|
+
"args": ["./node_modules/@ldlework/workmark/dist/index.js"]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Once connected, your assistant can run `wm deploy api` the same way you do — with full schema validation and typed responses.
|
|
208
|
+
|
|
209
|
+
## Project structure
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
your-workspace/
|
|
213
|
+
├── .wm/
|
|
214
|
+
│ └── commands/
|
|
215
|
+
│ ├── deploy.ts # Root-level command
|
|
216
|
+
│ ├── art/
|
|
217
|
+
│ │ └── sprites.ts # Grouped under "Art"
|
|
218
|
+
│ └── db/
|
|
219
|
+
│ └── migrate.ts # Grouped under "Db"
|
|
220
|
+
├── packages/
|
|
221
|
+
│ ├── api/
|
|
222
|
+
│ │ └── wm.ts # Project definition
|
|
223
|
+
│ └── web/
|
|
224
|
+
│ └── wm.ts
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- **`wm.ts`** — project definitions, discovered recursively
|
|
228
|
+
- **`.wm/commands/**/*.ts`** — command files, grouped by directory name
|
|
229
|
+
|
|
230
|
+
## API reference
|
|
231
|
+
|
|
232
|
+
### Defining
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { defineProject } from "@ldlework/workmark/define";
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Helpers
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { ok, fail, exec, execAsync } from "@ldlework/workmark/helpers";
|
|
242
|
+
|
|
243
|
+
ok(data) // Wrap data in a success CallToolResult
|
|
244
|
+
fail(error) // Wrap error in an error CallToolResult
|
|
245
|
+
exec(cmd, { cwd }) // Synchronous shell exec, returns CallToolResult
|
|
246
|
+
execAsync(cmd, { cwd }) // Async shell exec, returns Promise<CallToolResult>
|
|
247
|
+
execRaw(cmd, { cwd }) // Synchronous shell exec, returns string (throws on error)
|
|
248
|
+
execAsyncRaw(cmd, { cwd }) // Async shell exec, returns Promise<string> (throws on error)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Loading
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { loadWorkspace } from "@ldlework/workmark/workspace";
|
|
255
|
+
import { loadCommands } from "@ldlework/workmark";
|
|
256
|
+
|
|
257
|
+
const workspace = await loadWorkspace();
|
|
258
|
+
const commands = await loadCommands(workspace);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -97,7 +97,14 @@ async function main() {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
const args = parseArgs(rest, cmd.positional, cmd.inputSchema);
|
|
100
|
-
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
result = await cmd.handler(args);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + "\n");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
101
108
|
for (const content of result.content) {
|
|
102
109
|
if (content.type === "text") {
|
|
103
110
|
const stream = result.isError ? process.stderr : process.stdout;
|
|
@@ -108,6 +115,6 @@ async function main() {
|
|
|
108
115
|
process.exit(1);
|
|
109
116
|
}
|
|
110
117
|
main().catch((err) => {
|
|
111
|
-
|
|
118
|
+
process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
|
|
112
119
|
process.exit(1);
|
|
113
120
|
});
|
package/dist/lib/define.d.ts
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { BaseCtx, FromArgsResolver, FromWorkspaceResolver, HandlerReturn, NeedsCtx, ProjectDef, ReduceFn, RunOptions, SchemaFields, SelectMode, Trait } from "./types.js";
|
|
3
|
+
/** Declare a trait: a named schema that projects fulfill and commands require. */
|
|
4
|
+
export declare function defineTrait<N extends string, S extends z.ZodType>(def: {
|
|
5
|
+
name: N;
|
|
6
|
+
schema: S;
|
|
7
|
+
description?: string;
|
|
8
|
+
}): Trait<N, z.output<S>>;
|
|
3
9
|
export declare function defineProject(def: ProjectDef): ProjectDef;
|
|
10
|
+
type TraitsOf<N extends readonly Trait<string, unknown>[]> = {
|
|
11
|
+
[K in N[number] as K["name"]]: K extends Trait<string, infer T> ? T : never;
|
|
12
|
+
};
|
|
13
|
+
type InferField<F> = F extends z.ZodType ? z.output<F> : unknown;
|
|
14
|
+
type InferFields<R extends SchemaFields | undefined> = R extends SchemaFields ? {
|
|
15
|
+
[K in keyof R]: InferField<R[K]>;
|
|
16
|
+
} : Record<string, never>;
|
|
17
|
+
type CtxFor<N extends readonly Trait<string, unknown>[]> = N extends readonly [] ? BaseCtx : NeedsCtx<TraitsOf<N>>;
|
|
18
|
+
export interface StaticCommandDef {
|
|
19
|
+
needs?: readonly Trait[];
|
|
20
|
+
select?: SelectMode;
|
|
21
|
+
for?: string;
|
|
22
|
+
run?: RunOptions;
|
|
23
|
+
args?: SchemaFields;
|
|
24
|
+
flags?: SchemaFields;
|
|
25
|
+
meta?: {
|
|
26
|
+
name?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
};
|
|
30
|
+
handler: (args: Record<string, unknown>, ctx: BaseCtx | NeedsCtx<Record<string, unknown>>) => HandlerReturn | Promise<HandlerReturn>;
|
|
31
|
+
}
|
|
32
|
+
/** Declare a command. `needs`, `args`, and `flags` are inferred for handler
|
|
33
|
+
* typing; the framework manages the auto-generated `project` arg separately.
|
|
34
|
+
*
|
|
35
|
+
* `for`: bind the command to a named project. No project arg is exposed on any
|
|
36
|
+
* surface; `ctx.project` is the bound project. Validated at load.
|
|
37
|
+
*/
|
|
38
|
+
export declare function cmd<const N extends readonly Trait<string, unknown>[] = readonly [], const A extends SchemaFields = Record<string, never>, const F extends SchemaFields = Record<string, never>>(def: {
|
|
39
|
+
needs?: N;
|
|
40
|
+
select?: SelectMode;
|
|
41
|
+
for?: string;
|
|
42
|
+
run?: RunOptions;
|
|
43
|
+
args?: A;
|
|
44
|
+
flags?: F;
|
|
45
|
+
meta?: {
|
|
46
|
+
name?: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
};
|
|
50
|
+
handler: (args: InferFields<A> & InferFields<F>, ctx: CtxFor<N>) => HandlerReturn | Promise<HandlerReturn>;
|
|
51
|
+
}): StaticCommandDef;
|
|
52
|
+
export declare const defineCommand: typeof cmd;
|
|
53
|
+
/** Declare a schema that depends on workspace state. The framework resolves
|
|
54
|
+
* the marker during command load and substitutes the returned zod schema. */
|
|
55
|
+
export declare function fromWorkspace(resolver: FromWorkspaceResolver): z.ZodType;
|
|
56
|
+
/** Declare a schema whose shape depends on other args supplied at invocation.
|
|
57
|
+
* The framework resolves this per-call, AFTER other args are parsed. The
|
|
58
|
+
* field's load-time JSON Schema is permissive (type-agnostic) — validation
|
|
59
|
+
* runs at invocation with the resolved schema. */
|
|
60
|
+
export declare function fromArgs(resolver: FromArgsResolver): z.ZodType;
|
|
61
|
+
/** Shortcut: enum of project names fulfilling a trait. */
|
|
62
|
+
export declare function projectsOf(trait: Trait): z.ZodType;
|
|
63
|
+
/** Build a schema whose shape depends on a specific project's trait data.
|
|
64
|
+
*
|
|
65
|
+
* Usage: `traitField(docker, d => z.enum(d.buildable)).forProject("ghost")`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function traitField<T extends Trait<string, unknown>>(trait: T, selector: (data: unknown) => z.ZodType): TraitFieldBuilder;
|
|
68
|
+
export interface TraitFieldBuilder {
|
|
69
|
+
forProject(name: string): z.ZodType;
|
|
70
|
+
fromArg(argName: string): z.ZodType;
|
|
71
|
+
}
|
|
72
|
+
export type { SelectMode, RunOptions, ReduceFn };
|
package/dist/lib/define.js
CHANGED
|
@@ -1,4 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FROM_ARGS, FROM_WORKSPACE } from "./types.js";
|
|
3
|
+
import { registerAmbient } from "./registry.js";
|
|
4
|
+
// ---- defineTrait -------------------------------------------------------
|
|
5
|
+
/** Declare a trait: a named schema that projects fulfill and commands require. */
|
|
6
|
+
export function defineTrait(def) {
|
|
7
|
+
const trait = {
|
|
8
|
+
name: def.name,
|
|
9
|
+
schema: def.schema,
|
|
10
|
+
description: def.description,
|
|
11
|
+
};
|
|
12
|
+
// Register into ambient registry (installed by the loader during trait import).
|
|
13
|
+
registerAmbient(trait);
|
|
14
|
+
return trait;
|
|
15
|
+
}
|
|
16
|
+
// ---- defineProject -----------------------------------------------------
|
|
2
17
|
export function defineProject(def) {
|
|
3
18
|
return def;
|
|
4
19
|
}
|
|
20
|
+
/** Declare a command. `needs`, `args`, and `flags` are inferred for handler
|
|
21
|
+
* typing; the framework manages the auto-generated `project` arg separately.
|
|
22
|
+
*
|
|
23
|
+
* `for`: bind the command to a named project. No project arg is exposed on any
|
|
24
|
+
* surface; `ctx.project` is the bound project. Validated at load.
|
|
25
|
+
*/
|
|
26
|
+
export function cmd(def) {
|
|
27
|
+
return def;
|
|
28
|
+
}
|
|
29
|
+
export const defineCommand = cmd;
|
|
30
|
+
// ---- fromWorkspace ------------------------------------------------------
|
|
31
|
+
/** Declare a schema that depends on workspace state. The framework resolves
|
|
32
|
+
* the marker during command load and substitutes the returned zod schema. */
|
|
33
|
+
export function fromWorkspace(resolver) {
|
|
34
|
+
const marker = z.lazy(() => {
|
|
35
|
+
throw new Error("fromWorkspace marker used at runtime — the framework should have replaced it at load time");
|
|
36
|
+
});
|
|
37
|
+
marker[FROM_WORKSPACE] = resolver;
|
|
38
|
+
return marker;
|
|
39
|
+
}
|
|
40
|
+
/** Declare a schema whose shape depends on other args supplied at invocation.
|
|
41
|
+
* The framework resolves this per-call, AFTER other args are parsed. The
|
|
42
|
+
* field's load-time JSON Schema is permissive (type-agnostic) — validation
|
|
43
|
+
* runs at invocation with the resolved schema. */
|
|
44
|
+
export function fromArgs(resolver) {
|
|
45
|
+
const marker = z.any();
|
|
46
|
+
marker[FROM_ARGS] = resolver;
|
|
47
|
+
return marker;
|
|
48
|
+
}
|
|
49
|
+
/** Shortcut: enum of project names fulfilling a trait. */
|
|
50
|
+
export function projectsOf(trait) {
|
|
51
|
+
return fromWorkspace((ws) => {
|
|
52
|
+
const names = ws.withTrait(trait).map((p) => p.name);
|
|
53
|
+
if (names.length === 0) {
|
|
54
|
+
throw new Error(`No projects fulfill trait "${trait.name}"`);
|
|
55
|
+
}
|
|
56
|
+
return z.enum(names);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Build a schema whose shape depends on a specific project's trait data.
|
|
60
|
+
*
|
|
61
|
+
* Usage: `traitField(docker, d => z.enum(d.buildable)).forProject("ghost")`.
|
|
62
|
+
*/
|
|
63
|
+
export function traitField(trait, selector) {
|
|
64
|
+
return {
|
|
65
|
+
forProject(name) {
|
|
66
|
+
return fromWorkspace((ws) => {
|
|
67
|
+
const project = ws.get(name);
|
|
68
|
+
if (!project.hasTrait(trait)) {
|
|
69
|
+
throw new Error(`traitField(${trait.name}).forProject("${name}") — project does not fulfill trait "${trait.name}"`);
|
|
70
|
+
}
|
|
71
|
+
const data = project.trait(trait);
|
|
72
|
+
return selector(data);
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
fromArg(argName) {
|
|
76
|
+
return fromArgs((ws, args) => {
|
|
77
|
+
const target = args[argName];
|
|
78
|
+
if (typeof target !== "string") {
|
|
79
|
+
throw new Error(`traitField(${trait.name}).fromArg("${argName}") — expected arg "${argName}" to be a project name string, got ${typeof target}`);
|
|
80
|
+
}
|
|
81
|
+
const project = ws.get(target);
|
|
82
|
+
if (!project.hasTrait(trait)) {
|
|
83
|
+
throw new Error(`traitField(${trait.name}).fromArg("${argName}") — project "${target}" does not fulfill trait "${trait.name}"`);
|
|
84
|
+
}
|
|
85
|
+
const data = project.trait(trait);
|
|
86
|
+
return selector(data);
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
package/dist/lib/helpers.d.ts
CHANGED
|
@@ -4,8 +4,11 @@ export declare function fail(error: unknown): CallToolResult;
|
|
|
4
4
|
export interface ExecOptions {
|
|
5
5
|
cwd: string;
|
|
6
6
|
timeout?: number;
|
|
7
|
+
env?: Record<string, string>;
|
|
7
8
|
}
|
|
8
9
|
export declare function execRaw(command: string, opts: ExecOptions): string;
|
|
9
10
|
export declare function exec(command: string, opts: ExecOptions): CallToolResult;
|
|
10
11
|
export declare function execAsyncRaw(command: string, opts: ExecOptions): Promise<string>;
|
|
12
|
+
/** Run a sequence of shell commands. Fails fast; concatenates outputs. */
|
|
13
|
+
export declare function shSeq(commands: readonly string[], opts: ExecOptions): Promise<CallToolResult>;
|
|
11
14
|
export declare function execAsync(command: string, opts: ExecOptions): Promise<CallToolResult>;
|
package/dist/lib/helpers.js
CHANGED
|
@@ -26,13 +26,14 @@ export function fail(error) {
|
|
|
26
26
|
return { content: [{ type: "text", text: msg }], isError: true };
|
|
27
27
|
}
|
|
28
28
|
export function execRaw(command, opts) {
|
|
29
|
-
const { cwd, timeout = 120_000 } = opts;
|
|
29
|
+
const { cwd, timeout = 120_000, env } = opts;
|
|
30
30
|
return execSync(command, {
|
|
31
31
|
cwd,
|
|
32
32
|
timeout,
|
|
33
33
|
encoding: "utf-8",
|
|
34
34
|
stdio: ["ignore", "pipe", "pipe"],
|
|
35
35
|
maxBuffer: 1024 * 1024 * 10,
|
|
36
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
export function exec(command, opts) {
|
|
@@ -45,15 +46,32 @@ export function exec(command, opts) {
|
|
|
45
46
|
}
|
|
46
47
|
const execPromise = promisify(cpExec);
|
|
47
48
|
export async function execAsyncRaw(command, opts) {
|
|
48
|
-
const { cwd, timeout = 120_000 } = opts;
|
|
49
|
+
const { cwd, timeout = 120_000, env } = opts;
|
|
49
50
|
const { stdout } = await execPromise(command, {
|
|
50
51
|
cwd,
|
|
51
52
|
timeout,
|
|
52
53
|
encoding: "utf-8",
|
|
53
54
|
maxBuffer: 1024 * 1024 * 10,
|
|
55
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
54
56
|
});
|
|
55
57
|
return stdout;
|
|
56
58
|
}
|
|
59
|
+
/** Run a sequence of shell commands. Fails fast; concatenates outputs. */
|
|
60
|
+
export async function shSeq(commands, opts) {
|
|
61
|
+
const outputs = [];
|
|
62
|
+
for (const cmd of commands) {
|
|
63
|
+
try {
|
|
64
|
+
outputs.push(await execAsyncRaw(cmd, opts));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const prior = outputs.join("\n");
|
|
68
|
+
const err = e instanceof Error ? e : null;
|
|
69
|
+
const errText = err?.stderr || err?.stdout || (err?.message ?? String(e));
|
|
70
|
+
return fail(prior ? `${prior}\n${errText}` : errText);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return ok(outputs.join("\n"));
|
|
74
|
+
}
|
|
57
75
|
export async function execAsync(command, opts) {
|
|
58
76
|
try {
|
|
59
77
|
return ok(await execAsyncRaw(command, opts));
|
package/dist/lib/load.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ResolvedCommand } from "./types.js";
|
|
2
|
+
import type { Workspace } from "./workspace.js";
|
|
3
|
+
export declare function loadCommands(workspace: Workspace): Promise<ResolvedCommand[]>;
|