@ldlework/workmark 1.1.1 → 1.2.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 CHANGED
@@ -1,42 +1,7 @@
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
  // ---------------------------------------------------------------------------
@@ -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") {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldlework/workmark",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/lib/load.js",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -2,49 +2,9 @@
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
  // ---------------------------------------------------------------------------
@@ -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) {
@@ -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
+ }