@ldlework/workmark 1.2.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 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/cli.js CHANGED
@@ -6,7 +6,7 @@ import { parseArgs } from "./lib/parse.js";
6
6
  // Help
7
7
  // ---------------------------------------------------------------------------
8
8
  function printHelp(commands) {
9
- console.log("Usage: ws <command> [args...]\n");
9
+ console.log("Usage: wm <command> [args...]\n");
10
10
  // Group by group name
11
11
  const groups = new Map();
12
12
  for (const cmd of commands) {
@@ -43,7 +43,7 @@ function printHelp(commands) {
43
43
  }
44
44
  function printCommandHelp(cmd) {
45
45
  const pos = cmd.positional.map((p) => `<${p}>`).join(" ");
46
- console.log(`Usage: ws ${cmd.name} ${pos}\n`);
46
+ console.log(`Usage: wm ${cmd.name} ${pos}\n`);
47
47
  console.log(cmd.description);
48
48
  const schema = cmd.inputSchema;
49
49
  const props = schema.properties;
@@ -88,7 +88,7 @@ async function main() {
88
88
  const cmd = cmdMap.get(command);
89
89
  if (!cmd) {
90
90
  console.error(`Unknown command: ${command}`);
91
- console.error(`Run 'ws --help' for available commands.`);
91
+ console.error(`Run 'wm --help' for available commands.`);
92
92
  process.exit(1);
93
93
  }
94
94
  // Per-command help
@@ -1,6 +1,6 @@
1
1
  import type { JitiOptions } from "jiti";
2
2
  /**
3
- * Build jiti options that let ws.ts / command files import from
3
+ * Build jiti options that let wm.ts / command files import from
4
4
  * `@ldlework/workmark/*` and from workmark's own dependencies (zod, etc.)
5
5
  * regardless of how the package is installed or linked.
6
6
  */
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
5
5
  const SRC_LIB = join(PKG_ROOT, "src", "lib");
6
6
  /**
7
- * Build jiti options that let ws.ts / command files import from
7
+ * Build jiti options that let wm.ts / command files import from
8
8
  * `@ldlework/workmark/*` and from workmark's own dependencies (zod, etc.)
9
9
  * regardless of how the package is installed or linked.
10
10
  */
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
- // Args are positional, in key order
26
- if (args) {
27
- for (const [name, field] of Object.entries(args)) {
28
- const schema = fieldToJsonSchema(field);
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 (!("default" in schema)) {
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,
@@ -72,7 +72,7 @@ function resolve(def, workspace, group, sourceFile) {
72
72
  return { name: def.name, label: def.label, group, description: def.description, inputSchema, positional, handler, sourceFile };
73
73
  }
74
74
  export async function loadCommands(workspace) {
75
- const commandsDir = join(workspace.root, ".ws", "commands");
75
+ const commandsDir = join(workspace.root, ".wm", "commands");
76
76
  if (!existsSync(commandsDir))
77
77
  return [];
78
78
  const jiti = createJiti(commandsDir, jitiOptions());
@@ -7,7 +7,7 @@ export type SchemaFields = Record<string, z.ZodType | Record<string, unknown>>;
7
7
  export interface ProjectDef {
8
8
  /** Unique identifier. */
9
9
  name: string;
10
- /** Project directory relative to the ws.ts location. Defaults to ".". */
10
+ /** Project directory relative to the wm.ts location. Defaults to ".". */
11
11
  dir?: string;
12
12
  /** Arbitrary string tags for grouping/filtering. */
13
13
  tags?: string[];
@@ -44,8 +44,8 @@ function loadIgnore(root) {
44
44
  }
45
45
  return ig;
46
46
  }
47
- /** Recursively find all ws.ts files, respecting .gitignore rules. */
48
- function findWsFiles(root) {
47
+ /** Recursively find all wm.ts files, respecting .gitignore rules. */
48
+ function findWmFiles(root) {
49
49
  const ig = loadIgnore(root);
50
50
  const results = [];
51
51
  function walk(dir) {
@@ -55,7 +55,7 @@ function findWsFiles(root) {
55
55
  if (!ig.ignores(rel + "/"))
56
56
  walk(join(dir, entry.name));
57
57
  }
58
- else if (entry.name === "ws.ts") {
58
+ else if (entry.name === "wm.ts") {
59
59
  results.push(join(dir, entry.name));
60
60
  }
61
61
  }
@@ -63,35 +63,35 @@ function findWsFiles(root) {
63
63
  walk(root);
64
64
  return results;
65
65
  }
66
- /** Find the workspace root by walking up from cwd looking for .ws/. */
66
+ /** Find the workspace root by walking up from cwd looking for .wm/. */
67
67
  function findRoot(from) {
68
68
  let dir = from;
69
69
  while (true) {
70
- if (existsSync(join(dir, ".ws"))) {
70
+ if (existsSync(join(dir, ".wm"))) {
71
71
  return dir;
72
72
  }
73
73
  const parent = dirname(dir);
74
74
  if (parent === dir)
75
- throw new Error("Could not find workspace root (.ws/ directory)");
75
+ throw new Error("Could not find workspace root (.wm/ directory)");
76
76
  dir = parent;
77
77
  }
78
78
  }
79
79
  export async function loadWorkspace(from) {
80
80
  const root = from ?? process.env.WORKSPACE_ROOT ?? findRoot(process.cwd());
81
81
  const jiti = createJiti(root, jitiOptions());
82
- // Recursively find all ws.ts files
83
- const wsFiles = findWsFiles(root);
82
+ // Recursively find all wm.ts files
83
+ const wmFiles = findWmFiles(root);
84
84
  // Import each and build Project instances
85
85
  const projects = [];
86
- for (const wsFile of wsFiles) {
87
- const dir = dirname(wsFile);
86
+ for (const wmFile of wmFiles) {
87
+ const dir = dirname(wmFile);
88
88
  let exported;
89
89
  try {
90
- exported = await jiti.import(wsFile);
90
+ exported = await jiti.import(wmFile);
91
91
  }
92
92
  catch (err) {
93
- // Log import failures — helps debug bad ws.ts files
94
- console.error(`[workspace] Skipping ${wsFile}: ${err.message}`);
93
+ // Log import failures — helps debug bad wm.ts files
94
+ console.error(`[workspace] Skipping ${wmFile}: ${err.message}`);
95
95
  continue;
96
96
  }
97
97
  // jiti may return { default: ... } when interopDefault doesn't fully unwrap
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ldlework/workmark",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "main": "dist/lib/load.js",
6
6
  "bin": {
7
- "ws": "dist/cli.js"
7
+ "wm": "dist/cli.js"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=18"
package/src/cli.ts CHANGED
@@ -10,7 +10,7 @@ import type { ResolvedCommand } from "./lib/types.js";
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
12
  function printHelp(commands: ResolvedCommand[]): void {
13
- console.log("Usage: ws <command> [args...]\n");
13
+ console.log("Usage: wm <command> [args...]\n");
14
14
 
15
15
  // Group by group name
16
16
  const groups = new Map<string, ResolvedCommand[]>();
@@ -51,7 +51,7 @@ function printHelp(commands: ResolvedCommand[]): void {
51
51
 
52
52
  function printCommandHelp(cmd: ResolvedCommand): void {
53
53
  const pos = cmd.positional.map((p) => `<${p}>`).join(" ");
54
- console.log(`Usage: ws ${cmd.name} ${pos}\n`);
54
+ console.log(`Usage: wm ${cmd.name} ${pos}\n`);
55
55
  console.log(cmd.description);
56
56
 
57
57
  const schema = cmd.inputSchema as {
@@ -103,7 +103,7 @@ async function main(): Promise<void> {
103
103
  const cmd = cmdMap.get(command);
104
104
  if (!cmd) {
105
105
  console.error(`Unknown command: ${command}`);
106
- console.error(`Run 'ws --help' for available commands.`);
106
+ console.error(`Run 'wm --help' for available commands.`);
107
107
  process.exit(1);
108
108
  }
109
109
 
@@ -7,7 +7,7 @@ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
7
7
  const SRC_LIB = join(PKG_ROOT, "src", "lib");
8
8
 
9
9
  /**
10
- * Build jiti options that let ws.ts / command files import from
10
+ * Build jiti options that let wm.ts / command files import from
11
11
  * `@ldlework/workmark/*` and from workmark's own dependencies (zod, etc.)
12
12
  * regardless of how the package is installed or linked.
13
13
  */
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
- // Args are positional, in key order
35
- if (args) {
36
- for (const [name, field] of Object.entries(args)) {
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
- // If no default and not explicitly optional, it's required
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
- // Flags are named (--key value)
48
- if (flags) {
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",
@@ -87,7 +83,7 @@ function resolve(def: CommandDef, workspace: IWorkspace, group: string, sourceFi
87
83
  }
88
84
 
89
85
  export async function loadCommands(workspace: IWorkspace): Promise<ResolvedCommand[]> {
90
- const commandsDir = join(workspace.root, ".ws", "commands");
86
+ const commandsDir = join(workspace.root, ".wm", "commands");
91
87
  if (!existsSync(commandsDir)) return [];
92
88
 
93
89
  const jiti = createJiti(commandsDir, jitiOptions());
package/src/lib/types.ts CHANGED
@@ -7,12 +7,12 @@ export type InputSchema = z.ZodType | Record<string, unknown>;
7
7
  /** A record of named Zod schemas or raw JSON Schema property objects. */
8
8
  export type SchemaFields = Record<string, z.ZodType | Record<string, unknown>>;
9
9
 
10
- // ---- Project definitions (used in ws.ts files) ----
10
+ // ---- Project definitions (used in wm.ts files) ----
11
11
 
12
12
  export interface ProjectDef {
13
13
  /** Unique identifier. */
14
14
  name: string;
15
- /** Project directory relative to the ws.ts location. Defaults to ".". */
15
+ /** Project directory relative to the wm.ts location. Defaults to ".". */
16
16
  dir?: string;
17
17
  /** Arbitrary string tags for grouping/filtering. */
18
18
  tags?: string[];
@@ -52,8 +52,8 @@ function loadIgnore(root: string): Ignore {
52
52
  return ig;
53
53
  }
54
54
 
55
- /** Recursively find all ws.ts files, respecting .gitignore rules. */
56
- function findWsFiles(root: string): string[] {
55
+ /** Recursively find all wm.ts files, respecting .gitignore rules. */
56
+ function findWmFiles(root: string): string[] {
57
57
  const ig = loadIgnore(root);
58
58
  const results: string[] = [];
59
59
 
@@ -62,7 +62,7 @@ function findWsFiles(root: string): string[] {
62
62
  const rel = relative(root, join(dir, entry.name));
63
63
  if (entry.isDirectory()) {
64
64
  if (!ig.ignores(rel + "/")) walk(join(dir, entry.name));
65
- } else if (entry.name === "ws.ts") {
65
+ } else if (entry.name === "wm.ts") {
66
66
  results.push(join(dir, entry.name));
67
67
  }
68
68
  }
@@ -72,15 +72,15 @@ function findWsFiles(root: string): string[] {
72
72
  return results;
73
73
  }
74
74
 
75
- /** Find the workspace root by walking up from cwd looking for .ws/. */
75
+ /** Find the workspace root by walking up from cwd looking for .wm/. */
76
76
  function findRoot(from: string): string {
77
77
  let dir = from;
78
78
  while (true) {
79
- if (existsSync(join(dir, ".ws"))) {
79
+ if (existsSync(join(dir, ".wm"))) {
80
80
  return dir;
81
81
  }
82
82
  const parent = dirname(dir);
83
- if (parent === dir) throw new Error("Could not find workspace root (.ws/ directory)");
83
+ if (parent === dir) throw new Error("Could not find workspace root (.wm/ directory)");
84
84
  dir = parent;
85
85
  }
86
86
  }
@@ -89,20 +89,20 @@ export async function loadWorkspace(from?: string): Promise<Workspace> {
89
89
  const root = from ?? process.env.WORKSPACE_ROOT ?? findRoot(process.cwd());
90
90
  const jiti = createJiti(root, jitiOptions());
91
91
 
92
- // Recursively find all ws.ts files
93
- const wsFiles = findWsFiles(root);
92
+ // Recursively find all wm.ts files
93
+ const wmFiles = findWmFiles(root);
94
94
 
95
95
  // Import each and build Project instances
96
96
  const projects: Project[] = [];
97
97
 
98
- for (const wsFile of wsFiles) {
99
- const dir = dirname(wsFile);
98
+ for (const wmFile of wmFiles) {
99
+ const dir = dirname(wmFile);
100
100
  let exported: unknown;
101
101
  try {
102
- exported = await jiti.import(wsFile);
102
+ exported = await jiti.import(wmFile);
103
103
  } catch (err) {
104
- // Log import failures — helps debug bad ws.ts files
105
- console.error(`[workspace] Skipping ${wsFile}: ${(err as Error).message}`);
104
+ // Log import failures — helps debug bad wm.ts files
105
+ console.error(`[workspace] Skipping ${wmFile}: ${(err as Error).message}`);
106
106
  continue;
107
107
  }
108
108