@ldlework/workmark 1.1.1 → 1.3.0
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/dist/cli.js +5 -40
- package/dist/lib/jiti-options.d.ts +1 -1
- package/dist/lib/jiti-options.js +1 -1
- package/dist/lib/load.js +1 -1
- package/dist/lib/parse.d.ts +13 -0
- package/dist/lib/parse.js +113 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/workspace.js +13 -13
- package/package.json +2 -2
- package/src/cli.ts +5 -45
- package/src/lib/jiti-options.ts +1 -1
- package/src/lib/load.ts +1 -1
- package/src/lib/parse.ts +155 -0
- package/src/lib/types.ts +2 -2
- package/src/lib/workspace.ts +13 -13
package/dist/cli.js
CHANGED
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadCommands } from "./lib/load.js";
|
|
3
3
|
import { loadWorkspace } from "./lib/workspace.js";
|
|
4
|
-
|
|
5
|
-
// Arg parsing
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
function parseValue(raw) {
|
|
8
|
-
if (raw === "true")
|
|
9
|
-
return true;
|
|
10
|
-
if (raw === "false")
|
|
11
|
-
return false;
|
|
12
|
-
const n = Number(raw);
|
|
13
|
-
if (!Number.isNaN(n) && raw !== "")
|
|
14
|
-
return n;
|
|
15
|
-
return raw;
|
|
16
|
-
}
|
|
17
|
-
function parseArgs(argv, positionalNames) {
|
|
18
|
-
const args = {};
|
|
19
|
-
let posIdx = 0;
|
|
20
|
-
for (let i = 0; i < argv.length; i++) {
|
|
21
|
-
const token = argv[i];
|
|
22
|
-
if (token.startsWith("--")) {
|
|
23
|
-
const key = token.slice(2);
|
|
24
|
-
const next = argv[i + 1];
|
|
25
|
-
if (next === undefined || next.startsWith("--")) {
|
|
26
|
-
args[key] = true;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
args[key] = parseValue(next);
|
|
30
|
-
i++;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else if (posIdx < positionalNames.length) {
|
|
34
|
-
args[positionalNames[posIdx]] = parseValue(token);
|
|
35
|
-
posIdx++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return args;
|
|
39
|
-
}
|
|
4
|
+
import { parseArgs } from "./lib/parse.js";
|
|
40
5
|
// ---------------------------------------------------------------------------
|
|
41
6
|
// Help
|
|
42
7
|
// ---------------------------------------------------------------------------
|
|
43
8
|
function printHelp(commands) {
|
|
44
|
-
console.log("Usage:
|
|
9
|
+
console.log("Usage: wm <command> [args...]\n");
|
|
45
10
|
// Group by group name
|
|
46
11
|
const groups = new Map();
|
|
47
12
|
for (const cmd of commands) {
|
|
@@ -78,7 +43,7 @@ function printHelp(commands) {
|
|
|
78
43
|
}
|
|
79
44
|
function printCommandHelp(cmd) {
|
|
80
45
|
const pos = cmd.positional.map((p) => `<${p}>`).join(" ");
|
|
81
|
-
console.log(`Usage:
|
|
46
|
+
console.log(`Usage: wm ${cmd.name} ${pos}\n`);
|
|
82
47
|
console.log(cmd.description);
|
|
83
48
|
const schema = cmd.inputSchema;
|
|
84
49
|
const props = schema.properties;
|
|
@@ -123,7 +88,7 @@ async function main() {
|
|
|
123
88
|
const cmd = cmdMap.get(command);
|
|
124
89
|
if (!cmd) {
|
|
125
90
|
console.error(`Unknown command: ${command}`);
|
|
126
|
-
console.error(`Run '
|
|
91
|
+
console.error(`Run 'wm --help' for available commands.`);
|
|
127
92
|
process.exit(1);
|
|
128
93
|
}
|
|
129
94
|
// Per-command help
|
|
@@ -131,7 +96,7 @@ async function main() {
|
|
|
131
96
|
printCommandHelp(cmd);
|
|
132
97
|
return;
|
|
133
98
|
}
|
|
134
|
-
const args = parseArgs(rest, cmd.positional);
|
|
99
|
+
const args = parseArgs(rest, cmd.positional, cmd.inputSchema);
|
|
135
100
|
const result = await cmd.handler(args);
|
|
136
101
|
for (const content of result.content) {
|
|
137
102
|
if (content.type === "text") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JitiOptions } from "jiti";
|
|
2
2
|
/**
|
|
3
|
-
* Build jiti options that let
|
|
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
|
*/
|
package/dist/lib/jiti-options.js
CHANGED
|
@@ -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
|
|
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
|
@@ -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, ".
|
|
75
|
+
const commandsDir = join(workspace.root, ".wm", "commands");
|
|
76
76
|
if (!existsSync(commandsDir))
|
|
77
77
|
return [];
|
|
78
78
|
const jiti = createJiti(commandsDir, jitiOptions());
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parsing, in three phases:
|
|
3
|
+
*
|
|
4
|
+
* 1. tokenize — turn argv into a structured token stream (flags vs. positionals)
|
|
5
|
+
* 2. dispatch — bind tokens to parameter names using positional order and
|
|
6
|
+
* schema-declared array flags (repeats accumulate)
|
|
7
|
+
* 3. coerce — convert raw strings to JSON Schema-declared types
|
|
8
|
+
*
|
|
9
|
+
* Each phase is independently testable and has one job. Coercion is driven by
|
|
10
|
+
* the command's JSON Schema rather than heuristics, so `z.string()` fields
|
|
11
|
+
* always stay strings and `z.array(...)` fields always come out as arrays.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseArgs(argv: string[], positional: string[], schema: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parsing, in three phases:
|
|
3
|
+
*
|
|
4
|
+
* 1. tokenize — turn argv into a structured token stream (flags vs. positionals)
|
|
5
|
+
* 2. dispatch — bind tokens to parameter names using positional order and
|
|
6
|
+
* schema-declared array flags (repeats accumulate)
|
|
7
|
+
* 3. coerce — convert raw strings to JSON Schema-declared types
|
|
8
|
+
*
|
|
9
|
+
* Each phase is independently testable and has one job. Coercion is driven by
|
|
10
|
+
* the command's JSON Schema rather than heuristics, so `z.string()` fields
|
|
11
|
+
* always stay strings and `z.array(...)` fields always come out as arrays.
|
|
12
|
+
*/
|
|
13
|
+
// --- 1. tokenize ---------------------------------------------------------
|
|
14
|
+
function tokenize(argv) {
|
|
15
|
+
const tokens = [];
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const tok = argv[i];
|
|
18
|
+
if (!tok.startsWith("--")) {
|
|
19
|
+
tokens.push({ kind: "positional", value: tok });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const eq = tok.indexOf("=");
|
|
23
|
+
if (eq >= 0) {
|
|
24
|
+
tokens.push({ kind: "flag", key: tok.slice(2, eq), value: tok.slice(eq + 1) });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const key = tok.slice(2);
|
|
28
|
+
const next = argv[i + 1];
|
|
29
|
+
if (next === undefined || next.startsWith("--")) {
|
|
30
|
+
tokens.push({ kind: "flag", key });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
tokens.push({ kind: "flag", key, value: next });
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return tokens;
|
|
38
|
+
}
|
|
39
|
+
// --- 2. dispatch ---------------------------------------------------------
|
|
40
|
+
function isArrayField(schema, name) {
|
|
41
|
+
return schema.properties?.[name]?.type === "array";
|
|
42
|
+
}
|
|
43
|
+
function appendToArray(out, key, value) {
|
|
44
|
+
const prev = out[key];
|
|
45
|
+
out[key] = Array.isArray(prev) ? [...prev, value] : [value];
|
|
46
|
+
}
|
|
47
|
+
function assignFlag(out, schema, key, value) {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
out[key] = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isArrayField(schema, key)) {
|
|
53
|
+
appendToArray(out, key, value);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
out[key] = value;
|
|
57
|
+
}
|
|
58
|
+
function dispatch(tokens, positional, schema) {
|
|
59
|
+
const out = {};
|
|
60
|
+
let posIdx = 0;
|
|
61
|
+
for (const tok of tokens) {
|
|
62
|
+
if (tok.kind === "flag") {
|
|
63
|
+
assignFlag(out, schema, tok.key, tok.value);
|
|
64
|
+
}
|
|
65
|
+
else if (posIdx < positional.length) {
|
|
66
|
+
out[positional[posIdx]] = tok.value;
|
|
67
|
+
posIdx++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
// --- 3. coerce -----------------------------------------------------------
|
|
73
|
+
function coerceScalar(raw, type) {
|
|
74
|
+
switch (type) {
|
|
75
|
+
case "number":
|
|
76
|
+
case "integer": {
|
|
77
|
+
const n = Number(raw);
|
|
78
|
+
return Number.isNaN(n) ? raw : n;
|
|
79
|
+
}
|
|
80
|
+
case "boolean":
|
|
81
|
+
if (raw === "true")
|
|
82
|
+
return true;
|
|
83
|
+
if (raw === "false")
|
|
84
|
+
return false;
|
|
85
|
+
return raw;
|
|
86
|
+
default:
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function coerceField(value, prop) {
|
|
91
|
+
if (prop === undefined)
|
|
92
|
+
return value;
|
|
93
|
+
if (typeof value === "boolean")
|
|
94
|
+
return value;
|
|
95
|
+
if (prop.type === "array") {
|
|
96
|
+
const items = Array.isArray(value) ? value : [value];
|
|
97
|
+
return items.map((item) => coerceScalar(String(item), prop.items?.type));
|
|
98
|
+
}
|
|
99
|
+
return coerceScalar(String(value), prop.type);
|
|
100
|
+
}
|
|
101
|
+
function coerce(raw, schema) {
|
|
102
|
+
const out = {};
|
|
103
|
+
const props = schema.properties ?? {};
|
|
104
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
105
|
+
out[key] = coerceField(value, props[key]);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
// --- orchestrator --------------------------------------------------------
|
|
110
|
+
export function parseArgs(argv, positional, schema) {
|
|
111
|
+
const s = schema;
|
|
112
|
+
return coerce(dispatch(tokenize(argv), positional, s), s);
|
|
113
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
|
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[];
|
package/dist/lib/workspace.js
CHANGED
|
@@ -44,8 +44,8 @@ function loadIgnore(root) {
|
|
|
44
44
|
}
|
|
45
45
|
return ig;
|
|
46
46
|
}
|
|
47
|
-
/** Recursively find all
|
|
48
|
-
function
|
|
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 === "
|
|
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 .
|
|
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, ".
|
|
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 (.
|
|
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
|
|
83
|
-
const
|
|
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
|
|
87
|
-
const dir = dirname(
|
|
86
|
+
for (const wmFile of wmFiles) {
|
|
87
|
+
const dir = dirname(wmFile);
|
|
88
88
|
let exported;
|
|
89
89
|
try {
|
|
90
|
-
exported = await jiti.import(
|
|
90
|
+
exported = await jiti.import(wmFile);
|
|
91
91
|
}
|
|
92
92
|
catch (err) {
|
|
93
|
-
// Log import failures — helps debug bad
|
|
94
|
-
console.error(`[workspace] Skipping ${
|
|
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
package/src/cli.ts
CHANGED
|
@@ -2,55 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { loadCommands } from "./lib/load.js";
|
|
4
4
|
import { loadWorkspace } from "./lib/workspace.js";
|
|
5
|
+
import { parseArgs } from "./lib/parse.js";
|
|
5
6
|
import type { ResolvedCommand } from "./lib/types.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Arg parsing
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
function parseValue(raw: string): unknown {
|
|
13
|
-
if (raw === "true") return true;
|
|
14
|
-
if (raw === "false") return false;
|
|
15
|
-
const n = Number(raw);
|
|
16
|
-
if (!Number.isNaN(n) && raw !== "") return n;
|
|
17
|
-
return raw;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseArgs(
|
|
21
|
-
argv: string[],
|
|
22
|
-
positionalNames: string[],
|
|
23
|
-
): Record<string, unknown> {
|
|
24
|
-
const args: Record<string, unknown> = {};
|
|
25
|
-
let posIdx = 0;
|
|
26
|
-
|
|
27
|
-
for (let i = 0; i < argv.length; i++) {
|
|
28
|
-
const token = argv[i];
|
|
29
|
-
|
|
30
|
-
if (token.startsWith("--")) {
|
|
31
|
-
const key = token.slice(2);
|
|
32
|
-
const next = argv[i + 1];
|
|
33
|
-
if (next === undefined || next.startsWith("--")) {
|
|
34
|
-
args[key] = true;
|
|
35
|
-
} else {
|
|
36
|
-
args[key] = parseValue(next);
|
|
37
|
-
i++;
|
|
38
|
-
}
|
|
39
|
-
} else if (posIdx < positionalNames.length) {
|
|
40
|
-
args[positionalNames[posIdx]] = parseValue(token);
|
|
41
|
-
posIdx++;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return args;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
8
|
// ---------------------------------------------------------------------------
|
|
49
9
|
// Help
|
|
50
10
|
// ---------------------------------------------------------------------------
|
|
51
11
|
|
|
52
12
|
function printHelp(commands: ResolvedCommand[]): void {
|
|
53
|
-
console.log("Usage:
|
|
13
|
+
console.log("Usage: wm <command> [args...]\n");
|
|
54
14
|
|
|
55
15
|
// Group by group name
|
|
56
16
|
const groups = new Map<string, ResolvedCommand[]>();
|
|
@@ -91,7 +51,7 @@ function printHelp(commands: ResolvedCommand[]): void {
|
|
|
91
51
|
|
|
92
52
|
function printCommandHelp(cmd: ResolvedCommand): void {
|
|
93
53
|
const pos = cmd.positional.map((p) => `<${p}>`).join(" ");
|
|
94
|
-
console.log(`Usage:
|
|
54
|
+
console.log(`Usage: wm ${cmd.name} ${pos}\n`);
|
|
95
55
|
console.log(cmd.description);
|
|
96
56
|
|
|
97
57
|
const schema = cmd.inputSchema as {
|
|
@@ -143,7 +103,7 @@ async function main(): Promise<void> {
|
|
|
143
103
|
const cmd = cmdMap.get(command);
|
|
144
104
|
if (!cmd) {
|
|
145
105
|
console.error(`Unknown command: ${command}`);
|
|
146
|
-
console.error(`Run '
|
|
106
|
+
console.error(`Run 'wm --help' for available commands.`);
|
|
147
107
|
process.exit(1);
|
|
148
108
|
}
|
|
149
109
|
|
|
@@ -153,7 +113,7 @@ async function main(): Promise<void> {
|
|
|
153
113
|
return;
|
|
154
114
|
}
|
|
155
115
|
|
|
156
|
-
const args = parseArgs(rest, cmd.positional);
|
|
116
|
+
const args = parseArgs(rest, cmd.positional, cmd.inputSchema);
|
|
157
117
|
const result = await cmd.handler(args);
|
|
158
118
|
|
|
159
119
|
for (const content of result.content) {
|
package/src/lib/jiti-options.ts
CHANGED
|
@@ -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
|
|
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
|
@@ -87,7 +87,7 @@ function resolve(def: CommandDef, workspace: IWorkspace, group: string, sourceFi
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export async function loadCommands(workspace: IWorkspace): Promise<ResolvedCommand[]> {
|
|
90
|
-
const commandsDir = join(workspace.root, ".
|
|
90
|
+
const commandsDir = join(workspace.root, ".wm", "commands");
|
|
91
91
|
if (!existsSync(commandsDir)) return [];
|
|
92
92
|
|
|
93
93
|
const jiti = createJiti(commandsDir, jitiOptions());
|
package/src/lib/parse.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parsing, in three phases:
|
|
3
|
+
*
|
|
4
|
+
* 1. tokenize — turn argv into a structured token stream (flags vs. positionals)
|
|
5
|
+
* 2. dispatch — bind tokens to parameter names using positional order and
|
|
6
|
+
* schema-declared array flags (repeats accumulate)
|
|
7
|
+
* 3. coerce — convert raw strings to JSON Schema-declared types
|
|
8
|
+
*
|
|
9
|
+
* Each phase is independently testable and has one job. Coercion is driven by
|
|
10
|
+
* the command's JSON Schema rather than heuristics, so `z.string()` fields
|
|
11
|
+
* always stay strings and `z.array(...)` fields always come out as arrays.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Token =
|
|
15
|
+
| { kind: "flag"; key: string; value?: string }
|
|
16
|
+
| { kind: "positional"; value: string };
|
|
17
|
+
|
|
18
|
+
type SchemaProp = {
|
|
19
|
+
type?: string;
|
|
20
|
+
items?: { type?: string; enum?: unknown[] };
|
|
21
|
+
enum?: unknown[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SchemaObject = {
|
|
25
|
+
properties?: Record<string, SchemaProp>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// --- 1. tokenize ---------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function tokenize(argv: string[]): Token[] {
|
|
31
|
+
const tokens: Token[] = [];
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const tok = argv[i];
|
|
34
|
+
if (!tok.startsWith("--")) {
|
|
35
|
+
tokens.push({ kind: "positional", value: tok });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const eq = tok.indexOf("=");
|
|
40
|
+
if (eq >= 0) {
|
|
41
|
+
tokens.push({ kind: "flag", key: tok.slice(2, eq), value: tok.slice(eq + 1) });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const key = tok.slice(2);
|
|
46
|
+
const next = argv[i + 1];
|
|
47
|
+
if (next === undefined || next.startsWith("--")) {
|
|
48
|
+
tokens.push({ kind: "flag", key });
|
|
49
|
+
} else {
|
|
50
|
+
tokens.push({ kind: "flag", key, value: next });
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- 2. dispatch ---------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function isArrayField(schema: SchemaObject, name: string): boolean {
|
|
60
|
+
return schema.properties?.[name]?.type === "array";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendToArray(out: Record<string, unknown>, key: string, value: string): void {
|
|
64
|
+
const prev = out[key];
|
|
65
|
+
out[key] = Array.isArray(prev) ? [...prev, value] : [value];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assignFlag(
|
|
69
|
+
out: Record<string, unknown>,
|
|
70
|
+
schema: SchemaObject,
|
|
71
|
+
key: string,
|
|
72
|
+
value: string | undefined,
|
|
73
|
+
): void {
|
|
74
|
+
if (value === undefined) {
|
|
75
|
+
out[key] = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isArrayField(schema, key)) {
|
|
79
|
+
appendToArray(out, key, value);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
out[key] = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dispatch(
|
|
86
|
+
tokens: Token[],
|
|
87
|
+
positional: string[],
|
|
88
|
+
schema: SchemaObject,
|
|
89
|
+
): Record<string, unknown> {
|
|
90
|
+
const out: Record<string, unknown> = {};
|
|
91
|
+
let posIdx = 0;
|
|
92
|
+
|
|
93
|
+
for (const tok of tokens) {
|
|
94
|
+
if (tok.kind === "flag") {
|
|
95
|
+
assignFlag(out, schema, tok.key, tok.value);
|
|
96
|
+
} else if (posIdx < positional.length) {
|
|
97
|
+
out[positional[posIdx]] = tok.value;
|
|
98
|
+
posIdx++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- 3. coerce -----------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function coerceScalar(raw: string, type?: string): unknown {
|
|
107
|
+
switch (type) {
|
|
108
|
+
case "number":
|
|
109
|
+
case "integer": {
|
|
110
|
+
const n = Number(raw);
|
|
111
|
+
return Number.isNaN(n) ? raw : n;
|
|
112
|
+
}
|
|
113
|
+
case "boolean":
|
|
114
|
+
if (raw === "true") return true;
|
|
115
|
+
if (raw === "false") return false;
|
|
116
|
+
return raw;
|
|
117
|
+
default:
|
|
118
|
+
return raw;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function coerceField(value: unknown, prop: SchemaProp | undefined): unknown {
|
|
123
|
+
if (prop === undefined) return value;
|
|
124
|
+
if (typeof value === "boolean") return value;
|
|
125
|
+
|
|
126
|
+
if (prop.type === "array") {
|
|
127
|
+
const items = Array.isArray(value) ? value : [value];
|
|
128
|
+
return items.map((item) => coerceScalar(String(item), prop.items?.type));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return coerceScalar(String(value), prop.type);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function coerce(
|
|
135
|
+
raw: Record<string, unknown>,
|
|
136
|
+
schema: SchemaObject,
|
|
137
|
+
): Record<string, unknown> {
|
|
138
|
+
const out: Record<string, unknown> = {};
|
|
139
|
+
const props = schema.properties ?? {};
|
|
140
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
141
|
+
out[key] = coerceField(value, props[key]);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- orchestrator --------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export function parseArgs(
|
|
149
|
+
argv: string[],
|
|
150
|
+
positional: string[],
|
|
151
|
+
schema: Record<string, unknown>,
|
|
152
|
+
): Record<string, unknown> {
|
|
153
|
+
const s = schema as SchemaObject;
|
|
154
|
+
return coerce(dispatch(tokenize(argv), positional, s), s);
|
|
155
|
+
}
|
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
|
|
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
|
|
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[];
|
package/src/lib/workspace.ts
CHANGED
|
@@ -52,8 +52,8 @@ function loadIgnore(root: string): Ignore {
|
|
|
52
52
|
return ig;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** Recursively find all
|
|
56
|
-
function
|
|
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 === "
|
|
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 .
|
|
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, ".
|
|
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 (.
|
|
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
|
|
93
|
-
const
|
|
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
|
|
99
|
-
const dir = dirname(
|
|
98
|
+
for (const wmFile of wmFiles) {
|
|
99
|
+
const dir = dirname(wmFile);
|
|
100
100
|
let exported: unknown;
|
|
101
101
|
try {
|
|
102
|
-
exported = await jiti.import(
|
|
102
|
+
exported = await jiti.import(wmFile);
|
|
103
103
|
} catch (err) {
|
|
104
|
-
// Log import failures — helps debug bad
|
|
105
|
-
console.error(`[workspace] Skipping ${
|
|
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
|
|