@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/dist/lib/project.js
CHANGED
|
@@ -3,24 +3,28 @@ export class Project {
|
|
|
3
3
|
name;
|
|
4
4
|
dir;
|
|
5
5
|
tags;
|
|
6
|
-
|
|
6
|
+
description;
|
|
7
|
+
/** Parsed (defaulted) trait data, keyed by trait name. */
|
|
8
|
+
traitData;
|
|
7
9
|
paths;
|
|
8
|
-
constructor(def, wsDir) {
|
|
10
|
+
constructor(def, wsDir, traitData) {
|
|
9
11
|
this.name = def.name;
|
|
10
12
|
this.dir = def.dir ? join(wsDir, def.dir) : wsDir;
|
|
11
|
-
this.tags = Object.freeze(def.tags ?? []);
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
13
|
+
this.tags = Object.freeze([...(def.tags ?? [])]);
|
|
14
|
+
this.description = def.description;
|
|
15
|
+
this.traitData = Object.freeze(traitData);
|
|
16
|
+
this.paths = Object.freeze({ ...(def.paths ?? {}) });
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
hasTrait(trait) {
|
|
19
|
+
const name = typeof trait === "string" ? trait : trait.name;
|
|
20
|
+
return name in this.traitData;
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
throw new Error(`Project "${this.name}" does not
|
|
22
|
+
trait(trait) {
|
|
23
|
+
const data = this.traitData[trait.name];
|
|
24
|
+
if (data === undefined) {
|
|
25
|
+
throw new Error(`Project "${this.name}" does not fulfill trait "${trait.name}"`);
|
|
22
26
|
}
|
|
23
|
-
return
|
|
27
|
+
return data;
|
|
24
28
|
}
|
|
25
29
|
path(name) {
|
|
26
30
|
const rel = this.paths[name];
|
|
@@ -30,3 +34,32 @@ export class Project {
|
|
|
30
34
|
return join(this.dir, rel);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
/** Validate a project's `has` against the registry, returning parsed data. */
|
|
38
|
+
export function validateHas(def, registry, sourceFile) {
|
|
39
|
+
const out = {};
|
|
40
|
+
const has = def.has ?? {};
|
|
41
|
+
for (const [traitName, raw] of Object.entries(has)) {
|
|
42
|
+
const trait = registry.get(traitName);
|
|
43
|
+
if (!trait) {
|
|
44
|
+
const known = registry.all().map((t) => t.name).join(", ") || "(none)";
|
|
45
|
+
throw new Error(`Project "${def.name}" declares has.${traitName}, but no such trait is defined\n at ${sourceFile}\n known traits: ${known}`);
|
|
46
|
+
}
|
|
47
|
+
// Marker-trait sugar: `has: { x: true }` parses the trait with `{}`, which
|
|
48
|
+
// triggers per-field defaults. Zod 4's outer `.default({})` returns `{}`
|
|
49
|
+
// for undefined input — useful but NOT deeply defaulted — so we pass `{}`
|
|
50
|
+
// explicitly to get nested defaults.
|
|
51
|
+
const input = raw === true ? {} : raw;
|
|
52
|
+
const result = trait.schema.safeParse(input);
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
const issues = result.error.issues
|
|
55
|
+
.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
const hint = raw === true
|
|
58
|
+
? `\n hint: "true" is sugar for defaults; trait "${traitName}" has no top-level .default(). Provide an object literal.`
|
|
59
|
+
: "";
|
|
60
|
+
throw new Error(`Project "${def.name}" has.${traitName} failed validation:\n${issues}\n at ${sourceFile}${hint}`);
|
|
61
|
+
}
|
|
62
|
+
out[traitName] = result.data;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Trait } from "./types.js";
|
|
2
|
+
/** Per-load trait registry. A fresh instance is created for each `loadWorkspace`
|
|
3
|
+
* call so reloads don't trip duplicate-name checks against prior state. */
|
|
4
|
+
export declare class TraitRegistry {
|
|
5
|
+
private readonly traits;
|
|
6
|
+
private readonly sources;
|
|
7
|
+
register(trait: Trait, sourceFile?: string): void;
|
|
8
|
+
get(name: string): Trait | undefined;
|
|
9
|
+
require(name: string): Trait;
|
|
10
|
+
has(name: string): boolean;
|
|
11
|
+
all(): readonly Trait[];
|
|
12
|
+
}
|
|
13
|
+
export declare function installRegistry(reg: TraitRegistry): void;
|
|
14
|
+
export declare function uninstallRegistry(): void;
|
|
15
|
+
export declare function registerAmbient(trait: Trait, sourceFile?: string): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Per-load trait registry. A fresh instance is created for each `loadWorkspace`
|
|
2
|
+
* call so reloads don't trip duplicate-name checks against prior state. */
|
|
3
|
+
export class TraitRegistry {
|
|
4
|
+
traits = new Map();
|
|
5
|
+
sources = new Map();
|
|
6
|
+
register(trait, sourceFile) {
|
|
7
|
+
const existing = this.traits.get(trait.name);
|
|
8
|
+
const prevSource = this.sources.get(trait.name);
|
|
9
|
+
if (existing) {
|
|
10
|
+
// Same source file re-executed (jiti moduleCache:false) is idempotent.
|
|
11
|
+
// Different source files with the same name is an error.
|
|
12
|
+
if (sourceFile && prevSource && sourceFile !== prevSource) {
|
|
13
|
+
throw new Error(`Trait "${trait.name}" defined twice:\n ${prevSource}\n ${sourceFile}`);
|
|
14
|
+
}
|
|
15
|
+
// Keep the first-registered trait object as canonical.
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.traits.set(trait.name, trait);
|
|
19
|
+
if (sourceFile)
|
|
20
|
+
this.sources.set(trait.name, sourceFile);
|
|
21
|
+
}
|
|
22
|
+
get(name) {
|
|
23
|
+
return this.traits.get(name);
|
|
24
|
+
}
|
|
25
|
+
require(name) {
|
|
26
|
+
const t = this.traits.get(name);
|
|
27
|
+
if (!t) {
|
|
28
|
+
const known = [...this.traits.keys()].join(", ") || "(none)";
|
|
29
|
+
throw new Error(`Unknown trait "${name}". Known: ${known}`);
|
|
30
|
+
}
|
|
31
|
+
return t;
|
|
32
|
+
}
|
|
33
|
+
has(name) {
|
|
34
|
+
return this.traits.has(name);
|
|
35
|
+
}
|
|
36
|
+
all() {
|
|
37
|
+
return [...this.traits.values()];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Ambient registry used by `defineTrait` at module-import time.
|
|
41
|
+
*
|
|
42
|
+
* Stored on `globalThis` so it survives the pnpm+jiti module-duplication problem:
|
|
43
|
+
* the CLI's workmark module and the jiti-loaded trait file's workmark module are
|
|
44
|
+
* distinct instances, but both see the same globalThis slot.
|
|
45
|
+
*
|
|
46
|
+
* The loader installs a fresh registry, imports trait files (which call
|
|
47
|
+
* defineTrait → registerAmbient → reads this slot), then uninstalls. */
|
|
48
|
+
const GLOBAL_KEY = Symbol.for("workmark.ambientRegistry");
|
|
49
|
+
function slot() {
|
|
50
|
+
return globalThis;
|
|
51
|
+
}
|
|
52
|
+
export function installRegistry(reg) {
|
|
53
|
+
slot()[GLOBAL_KEY] = reg;
|
|
54
|
+
}
|
|
55
|
+
export function uninstallRegistry() {
|
|
56
|
+
delete slot()[GLOBAL_KEY];
|
|
57
|
+
}
|
|
58
|
+
export function registerAmbient(trait, sourceFile) {
|
|
59
|
+
const reg = slot()[GLOBAL_KEY];
|
|
60
|
+
if (!reg) {
|
|
61
|
+
throw new Error(`defineTrait called outside of a workspace load. Traits must live in .wm/traits/*.ts files.`);
|
|
62
|
+
}
|
|
63
|
+
reg.register(trait, sourceFile);
|
|
64
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,69 +1,120 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { z } from "zod";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/** A named, schema-backed contract that projects fulfill and commands require.
|
|
4
|
+
*
|
|
5
|
+
* Identity is `name`. Created via `defineTrait`; the returned object carries a
|
|
6
|
+
* phantom type so `cmd({ needs: [docker] })` can infer trait-data types into
|
|
7
|
+
* the handler's `ctx.traits`. */
|
|
8
|
+
export interface Trait<N extends string = string, T = unknown> {
|
|
9
|
+
readonly name: N;
|
|
10
|
+
readonly schema: z.ZodType;
|
|
11
|
+
readonly description?: string;
|
|
12
|
+
/** Phantom for inference. Never accessed at runtime. */
|
|
13
|
+
readonly __brand?: readonly ["trait", N, T];
|
|
14
|
+
}
|
|
15
|
+
/** A field schema: a zod type or raw JSON Schema object. */
|
|
16
|
+
export type SchemaField = z.ZodType | Record<string, unknown>;
|
|
17
|
+
/** A record of named field schemas. */
|
|
18
|
+
export type SchemaFields = Record<string, SchemaField>;
|
|
7
19
|
export interface ProjectDef {
|
|
8
20
|
/** Unique identifier. */
|
|
9
21
|
name: string;
|
|
10
22
|
/** Project directory relative to the wm.ts location. Defaults to ".". */
|
|
11
23
|
dir?: string;
|
|
12
|
-
/**
|
|
24
|
+
/** Free-form labels. Filter only — no handler injection. */
|
|
13
25
|
tags?: string[];
|
|
14
26
|
/** Named directories, resolved relative to the project dir. */
|
|
15
27
|
paths?: Record<string, string>;
|
|
16
|
-
/**
|
|
17
|
-
|
|
28
|
+
/** Trait fulfillments. Keys are trait names; values are validated against
|
|
29
|
+
* the trait schema at load time. */
|
|
30
|
+
has?: Record<string, unknown>;
|
|
31
|
+
/** Optional human-readable description. */
|
|
32
|
+
description?: string;
|
|
18
33
|
}
|
|
19
34
|
export interface IProject {
|
|
20
35
|
readonly name: string;
|
|
21
36
|
readonly dir: string;
|
|
22
37
|
readonly tags: readonly string[];
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
readonly description?: string;
|
|
39
|
+
/** Check whether this project fulfills a trait (by object or name). */
|
|
40
|
+
hasTrait(trait: Trait | string): boolean;
|
|
41
|
+
/** Get the parsed trait data. Throws if the project does not fulfill it. */
|
|
42
|
+
trait<T>(trait: Trait<string, T>): T;
|
|
43
|
+
/** Resolve a named path relative to this project's directory. */
|
|
26
44
|
path(name: string): string;
|
|
27
45
|
}
|
|
28
46
|
export interface IWorkspace {
|
|
29
47
|
readonly root: string;
|
|
30
48
|
readonly projects: readonly IProject[];
|
|
31
49
|
get(name: string): IProject;
|
|
32
|
-
|
|
50
|
+
withTrait(trait: Trait | string): IProject[];
|
|
33
51
|
withTag(tag: string): IProject[];
|
|
34
52
|
}
|
|
35
|
-
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/** How many projects a command runs against. */
|
|
54
|
+
export type SelectMode = "one" | "one-or-many" | "all";
|
|
55
|
+
/** Framework-standard context passed to every handler. */
|
|
56
|
+
export interface BaseCtx {
|
|
57
|
+
workspace: IWorkspace;
|
|
58
|
+
/** Run a shell command (or sequence) in the resolved cwd. An array is executed
|
|
59
|
+
* sequentially; fails fast; output is concatenated. */
|
|
60
|
+
sh: (cmd: string | readonly string[], opts?: {
|
|
61
|
+
timeout?: number;
|
|
62
|
+
env?: Record<string, string>;
|
|
63
|
+
}) => Promise<CallToolResult>;
|
|
64
|
+
/** Run a shell command with explicit options (cwd, timeout, env). */
|
|
65
|
+
exec: (cmd: string, opts: {
|
|
66
|
+
cwd: string;
|
|
67
|
+
timeout?: number;
|
|
68
|
+
env?: Record<string, string>;
|
|
69
|
+
}) => Promise<CallToolResult>;
|
|
70
|
+
ok: (data: unknown) => CallToolResult;
|
|
71
|
+
fail: (error: unknown) => CallToolResult;
|
|
72
|
+
/** Invoke another command by name with wire-shape args. */
|
|
73
|
+
invoke: (name: string, args: Record<string, unknown>) => Promise<CallToolResult>;
|
|
45
74
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
factory: (workspace: IWorkspace) => {
|
|
51
|
-
args?: SchemaFields;
|
|
52
|
-
flags?: SchemaFields;
|
|
53
|
-
handler: ToolHandler;
|
|
54
|
-
};
|
|
75
|
+
/** Context for commands with `needs`. Adds `project` and `traits`. */
|
|
76
|
+
export interface NeedsCtx<Traits extends Record<string, unknown>> extends BaseCtx {
|
|
77
|
+
project: IProject;
|
|
78
|
+
traits: Traits;
|
|
55
79
|
}
|
|
56
|
-
export type
|
|
80
|
+
export type HandlerReturn = CallToolResult;
|
|
81
|
+
/** A resolved, runnable command — what the framework holds after load. */
|
|
57
82
|
export interface ResolvedCommand {
|
|
58
83
|
name: string;
|
|
59
84
|
label: string;
|
|
60
85
|
group: string;
|
|
61
86
|
description: string;
|
|
62
|
-
/** Merged JSON Schema from args + flags. */
|
|
63
87
|
inputSchema: Record<string, unknown>;
|
|
64
|
-
/** Positional arg names in order. */
|
|
65
88
|
positional: string[];
|
|
66
|
-
handler:
|
|
67
|
-
/** Absolute path to the command source file. */
|
|
89
|
+
handler: ResolvedHandler;
|
|
68
90
|
sourceFile?: string;
|
|
91
|
+
select: SelectMode;
|
|
92
|
+
/** Names of traits this command requires. Empty array = no needs. */
|
|
93
|
+
needs: string[];
|
|
94
|
+
}
|
|
95
|
+
export type ResolvedHandler = (args: Record<string, unknown>) => Promise<CallToolResult>;
|
|
96
|
+
/** Per-project result shape for multi-target commands. */
|
|
97
|
+
export interface ProjectResult {
|
|
98
|
+
project: string;
|
|
99
|
+
ok: boolean;
|
|
100
|
+
output?: string;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
/** Optional reducer for custom multi-target aggregation. */
|
|
104
|
+
export type ReduceFn = (results: ProjectResult[]) => HandlerReturn | Promise<HandlerReturn>;
|
|
105
|
+
export interface RunOptions {
|
|
106
|
+
order?: "parallel" | "serial";
|
|
107
|
+
concurrency?: number;
|
|
108
|
+
stopOnFailure?: boolean;
|
|
109
|
+
reduce?: ReduceFn;
|
|
110
|
+
}
|
|
111
|
+
/** Internal marker stamped on schemas produced by `fromWorkspace`. */
|
|
112
|
+
export declare const FROM_WORKSPACE: unique symbol;
|
|
113
|
+
/** Marker for invocation-time (args-dependent) schemas — resolved per-call. */
|
|
114
|
+
export declare const FROM_ARGS: unique symbol;
|
|
115
|
+
export interface FromWorkspaceResolver {
|
|
116
|
+
(ws: IWorkspace): z.ZodType;
|
|
117
|
+
}
|
|
118
|
+
export interface FromArgsResolver {
|
|
119
|
+
(ws: IWorkspace, args: Record<string, unknown>): z.ZodType;
|
|
69
120
|
}
|
package/dist/lib/types.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// ---- fromWorkspace marker ----------------------------------------------
|
|
2
|
+
/** Internal marker stamped on schemas produced by `fromWorkspace`. */
|
|
3
|
+
export const FROM_WORKSPACE = Symbol.for("workmark.fromWorkspace");
|
|
4
|
+
/** Marker for invocation-time (args-dependent) schemas — resolved per-call. */
|
|
5
|
+
export const FROM_ARGS = Symbol.for("workmark.fromArgs");
|
package/dist/lib/workspace.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { IProject, IWorkspace } from "./types.js";
|
|
1
|
+
import type { IProject, IWorkspace, Trait } from "./types.js";
|
|
2
|
+
import { TraitRegistry } from "./registry.js";
|
|
2
3
|
export declare class Workspace implements IWorkspace {
|
|
3
4
|
readonly root: string;
|
|
4
5
|
readonly projects: readonly IProject[];
|
|
6
|
+
/** Trait registry populated during load. Exposed for command-load phase. */
|
|
7
|
+
readonly traits: TraitRegistry;
|
|
5
8
|
private readonly byName;
|
|
6
|
-
constructor(root: string, projects: IProject[]);
|
|
9
|
+
constructor(root: string, projects: IProject[], traits: TraitRegistry);
|
|
7
10
|
get(name: string): IProject;
|
|
8
|
-
|
|
11
|
+
withTrait(trait: Trait | string): IProject[];
|
|
9
12
|
withTag(tag: string): IProject[];
|
|
10
13
|
}
|
|
11
14
|
export declare function loadWorkspace(from?: string): Promise<Workspace>;
|
package/dist/lib/workspace.js
CHANGED
|
@@ -2,17 +2,20 @@ import { dirname, join, relative } from "node:path";
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
4
|
import ignore from "ignore";
|
|
5
|
-
import { Project } from "./project.js";
|
|
5
|
+
import { Project, validateHas } from "./project.js";
|
|
6
|
+
import { TraitRegistry, installRegistry, uninstallRegistry } from "./registry.js";
|
|
6
7
|
import { jitiOptions } from "./jiti-options.js";
|
|
7
8
|
export class Workspace {
|
|
8
9
|
root;
|
|
9
10
|
projects;
|
|
11
|
+
/** Trait registry populated during load. Exposed for command-load phase. */
|
|
12
|
+
traits;
|
|
10
13
|
byName;
|
|
11
|
-
constructor(root, projects) {
|
|
14
|
+
constructor(root, projects, traits) {
|
|
12
15
|
this.root = root;
|
|
13
|
-
this.projects = Object.freeze(projects);
|
|
16
|
+
this.projects = Object.freeze([...projects]);
|
|
17
|
+
this.traits = traits;
|
|
14
18
|
this.byName = new Map(projects.map((p) => [p.name, p]));
|
|
15
|
-
// Validate unique names
|
|
16
19
|
if (this.byName.size !== projects.length) {
|
|
17
20
|
const seen = new Set();
|
|
18
21
|
for (const p of projects) {
|
|
@@ -28,14 +31,15 @@ export class Workspace {
|
|
|
28
31
|
throw new Error(`Unknown project: "${name}"`);
|
|
29
32
|
return p;
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
withTrait(trait) {
|
|
35
|
+
const name = typeof trait === "string" ? trait : trait.name;
|
|
36
|
+
return this.projects.filter((p) => p.hasTrait(name));
|
|
33
37
|
}
|
|
34
38
|
withTag(tag) {
|
|
35
39
|
return this.projects.filter((p) => p.tags.includes(tag));
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
|
-
|
|
42
|
+
// ---- Discovery ---------------------------------------------------------
|
|
39
43
|
function loadIgnore(root) {
|
|
40
44
|
const ig = ignore().add(["node_modules", "dist"]);
|
|
41
45
|
const gitignorePath = join(root, ".gitignore");
|
|
@@ -44,7 +48,6 @@ function loadIgnore(root) {
|
|
|
44
48
|
}
|
|
45
49
|
return ig;
|
|
46
50
|
}
|
|
47
|
-
/** Recursively find all wm.ts files, respecting .gitignore rules. */
|
|
48
51
|
function findWmFiles(root) {
|
|
49
52
|
const ig = loadIgnore(root);
|
|
50
53
|
const results = [];
|
|
@@ -63,47 +66,81 @@ function findWmFiles(root) {
|
|
|
63
66
|
walk(root);
|
|
64
67
|
return results;
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
function findTraitFiles(root) {
|
|
70
|
+
const traitsDir = join(root, ".wm", "traits");
|
|
71
|
+
if (!existsSync(traitsDir))
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
function walk(dir) {
|
|
75
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
76
|
+
const full = join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory())
|
|
78
|
+
walk(full);
|
|
79
|
+
else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
80
|
+
out.push(full);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
walk(traitsDir);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
67
87
|
function findRoot(from) {
|
|
68
88
|
let dir = from;
|
|
69
89
|
while (true) {
|
|
70
|
-
if (existsSync(join(dir, ".wm")))
|
|
90
|
+
if (existsSync(join(dir, ".wm")))
|
|
71
91
|
return dir;
|
|
72
|
-
}
|
|
73
92
|
const parent = dirname(dir);
|
|
74
93
|
if (parent === dir)
|
|
75
94
|
throw new Error("Could not find workspace root (.wm/ directory)");
|
|
76
95
|
dir = parent;
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
98
|
+
// ---- Load --------------------------------------------------------------
|
|
99
|
+
async function loadTraits(root, jiti) {
|
|
100
|
+
const registry = new TraitRegistry();
|
|
101
|
+
installRegistry(registry);
|
|
102
|
+
for (const file of findTraitFiles(root)) {
|
|
103
|
+
try {
|
|
104
|
+
await jiti.import(file);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
uninstallRegistry();
|
|
108
|
+
throw new Error(`Failed to import trait file ${file}: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Registry stays installed — command loading re-executes trait files
|
|
112
|
+
// under jiti moduleCache:false, and those defineTrait calls need it.
|
|
113
|
+
return registry;
|
|
114
|
+
}
|
|
115
|
+
async function loadProjects(root, jiti, registry) {
|
|
85
116
|
const projects = [];
|
|
86
|
-
for (const wmFile of
|
|
117
|
+
for (const wmFile of findWmFiles(root)) {
|
|
87
118
|
const dir = dirname(wmFile);
|
|
88
119
|
let exported;
|
|
89
120
|
try {
|
|
90
121
|
exported = await jiti.import(wmFile);
|
|
91
122
|
}
|
|
92
123
|
catch (err) {
|
|
93
|
-
|
|
94
|
-
console.error(`[workspace] Skipping ${wmFile}: ${err.message}`);
|
|
95
|
-
continue;
|
|
124
|
+
throw new Error(`Failed to import ${wmFile}: ${err.message}`);
|
|
96
125
|
}
|
|
97
|
-
// jiti may return { default: ... } when interopDefault doesn't fully unwrap
|
|
98
126
|
if (exported && typeof exported === "object" && "default" in exported) {
|
|
99
127
|
exported = exported.default;
|
|
100
128
|
}
|
|
101
129
|
const items = Array.isArray(exported) ? exported : [exported];
|
|
102
130
|
for (const item of items) {
|
|
103
131
|
if (item && typeof item === "object" && typeof item.name === "string") {
|
|
104
|
-
|
|
132
|
+
const def = item;
|
|
133
|
+
const traitData = validateHas(def, registry, wmFile);
|
|
134
|
+
projects.push(new Project(def, dir, traitData));
|
|
105
135
|
}
|
|
106
136
|
}
|
|
107
137
|
}
|
|
108
|
-
return
|
|
138
|
+
return projects;
|
|
139
|
+
}
|
|
140
|
+
export async function loadWorkspace(from) {
|
|
141
|
+
const root = from ?? process.env.WORKSPACE_ROOT ?? findRoot(process.cwd());
|
|
142
|
+
const jiti = createJiti(root, jitiOptions());
|
|
143
|
+
const registry = await loadTraits(root, jiti);
|
|
144
|
+
const projects = await loadProjects(root, jiti, registry);
|
|
145
|
+
return new Workspace(root, projects, registry);
|
|
109
146
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -114,7 +114,13 @@ async function main(): Promise<void> {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const args = parseArgs(rest, cmd.positional, cmd.inputSchema);
|
|
117
|
-
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await cmd.handler(args);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + "\n");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
118
124
|
|
|
119
125
|
for (const content of result.content) {
|
|
120
126
|
if (content.type === "text") {
|
|
@@ -127,6 +133,6 @@ async function main(): Promise<void> {
|
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
main().catch((err) => {
|
|
130
|
-
|
|
136
|
+
process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
|
|
131
137
|
process.exit(1);
|
|
132
138
|
});
|