@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 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
- import { exec } from "@ldlework/workmark/helpers";
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: async ({ target, watch }) => {
38
- const cmd = `./tools/pack-sprites.sh ${target}${watch ? " --watch" : ""}`;
39
- return exec(cmd, { cwd: process.cwd() });
40
- },
41
- } satisfies CommandDef;
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
- import { exec } from "@ldlework/workmark/helpers";
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 projects = workspace.withCapability("deploy");
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
- project: z.enum(names as [string, ...string[]]),
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 exec(`./scripts/deploy.sh`, { cwd: p.dir });
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
- const result = await cmd.handler(args);
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
- console.error("Fatal:", err);
118
+ process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
112
119
  process.exit(1);
113
120
  });
@@ -1,3 +1,72 @@
1
- import type { ProjectDef } from "./types.js";
2
- /** Define a project. Identity function for type safety. */
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 };
@@ -1,4 +1,90 @@
1
- /** Define a project. Identity function for type safety. */
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
+ }
@@ -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>;
@@ -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));
@@ -1,2 +1,3 @@
1
- import type { IWorkspace, ResolvedCommand } from "./types.js";
2
- export declare function loadCommands(workspace: IWorkspace): Promise<ResolvedCommand[]>;
1
+ import type { ResolvedCommand } from "./types.js";
2
+ import type { Workspace } from "./workspace.js";
3
+ export declare function loadCommands(workspace: Workspace): Promise<ResolvedCommand[]>;