@ldlework/workmark 1.3.1 → 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 +22 -24
- 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 +428 -55
- 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 +556 -60
- 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
CHANGED
|
@@ -16,29 +16,33 @@ pnpm add @ldlework/workmark
|
|
|
16
16
|
|
|
17
17
|
### Write a command
|
|
18
18
|
|
|
19
|
-
Create commands in `.wm/commands/`. Subdirectories become groups in the CLI, dashboard and MCP server.
|
|
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
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// .wm/commands/art/sprites.ts
|
|
23
|
-
|
|
23
|
+
/** Pack sprite sheets from raw assets */
|
|
24
|
+
import { cmd } from "@ldlework/workmark/define";
|
|
24
25
|
import { z } from "zod";
|
|
25
|
-
import type { CommandDef } from "@ldlework/workmark/types";
|
|
26
26
|
|
|
27
|
-
export default {
|
|
28
|
-
name: "sprites",
|
|
29
|
-
label: "Build Sprites",
|
|
30
|
-
description: "Pack sprite sheets from raw assets",
|
|
27
|
+
export default cmd({
|
|
31
28
|
args: {
|
|
32
29
|
target: z.enum(["all", "characters", "terrain", "ui"]).default("all"),
|
|
33
30
|
},
|
|
34
31
|
flags: {
|
|
35
32
|
watch: z.boolean().default(false),
|
|
36
33
|
},
|
|
37
|
-
handler:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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" });
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
### Run it
|
|
@@ -118,25 +122,19 @@ A **dynamic command** receives the workspace and builds its schema from project
|
|
|
118
122
|
|
|
119
123
|
```ts
|
|
120
124
|
// .wm/commands/deploy.ts
|
|
121
|
-
|
|
125
|
+
/** Deploy a project */
|
|
122
126
|
import { z } from "zod";
|
|
123
127
|
import type { DynamicCommandDef } from "@ldlework/workmark/types";
|
|
124
128
|
|
|
125
129
|
export default {
|
|
126
|
-
name: "deploy",
|
|
127
|
-
label: "Deploy",
|
|
128
|
-
description: "Deploy a project",
|
|
129
130
|
factory: (workspace) => {
|
|
130
|
-
const
|
|
131
|
-
const names = projects.map((p) => p.name);
|
|
132
|
-
|
|
131
|
+
const names = workspace.withCapability("deploy").map((p) => p.name);
|
|
133
132
|
return {
|
|
134
|
-
args: {
|
|
135
|
-
|
|
136
|
-
},
|
|
137
|
-
handler: async ({ project }) => {
|
|
133
|
+
args: { project: z.enum(names as [string, ...string[]]) },
|
|
134
|
+
handler: ({ project }) => {
|
|
138
135
|
const p = workspace.get(project as string);
|
|
139
|
-
return
|
|
136
|
+
return { content: [{ type: "text", text: `deploying ${p.name}` }] };
|
|
137
|
+
// or: return `./scripts/deploy.sh` (with cwd resolved to workspace root by default)
|
|
140
138
|
},
|
|
141
139
|
};
|
|
142
140
|
},
|
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[]>;
|