@lovrabet/cli-framework 1.0.4-beta.2 → 1.0.4

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.
@@ -1,4 +1,4 @@
1
- export type { NormalizedServiceTree, NormalizedServiceTreeCommand, NormalizedServiceTreeFlag, NormalizedServiceTreeTarget, ServiceTreeAppBinding, ServiceTreeArg, ServiceTreeCommand, ServiceTreeCompileInput, ServiceTreeDatasetResolver, ServiceTreeDatasetResolverInput, ServiceTreeExecutionPlan, ServiceTreeFlag, ServiceTreeManifest, ServiceTreeMapTo, ServiceTreeMapToObject, ServiceTreeMapToRule, ServiceTreeMapTransform, ServiceTreeMatch, ServiceTreeServiceInfo, ServiceTreeTarget, ServiceTreeTargetCommand, ServiceTreeTargetKind, ServiceTreeValidationIssue, ServiceTreeValidationReport, ServiceTreeValueType, } from "./types.js";
1
+ export type { NormalizedServiceTree, NormalizedServiceTreeCommand, NormalizedServiceTreeFlag, NormalizedServiceTreeTarget, ServiceTreeAppBinding, ServiceTreeAppBindingInput, ServiceTreeArg, ServiceTreeArgInput, ServiceTreeAction, ServiceTreeCommand, ServiceTreeCompileInput, ServiceTreeDatasetResolver, ServiceTreeDatasetResolverInput, ServiceTreeExecutionPlan, ServiceTreeFlag, ServiceTreeFlagsInput, ServiceTreeManifest, ServiceTreeMapTo, ServiceTreeMapToObject, ServiceTreeMapToRule, ServiceTreeMapTransform, ServiceTreeMatch, ServiceTreeResource, ServiceTreeServiceInfo, ServiceTreeSimpleFlag, ServiceTreeTarget, ServiceTreeTargetCommand, ServiceTreeTargetKind, ServiceTreeValidationIssue, ServiceTreeValidationReport, ServiceTreeValueType, } from "./types.js";
2
2
  export { compileServiceTreeCommand } from "./compile.js";
3
3
  export { matchServiceTreeCommand } from "./match.js";
4
4
  export { normalizeServiceTreeManifest } from "./normalize.js";
@@ -1,35 +1,296 @@
1
1
  import { normalizeMapTo } from "./map-to.js";
2
2
  import { splitServiceTreePath, toKebabCase } from "./path.js";
3
3
  const DEFAULT_PROTOCOL = "lovrabet.service-tree/v1";
4
+ const DEFAULT_VERSION = "1.0.0";
5
+ const DEFAULT_APP_REF = "default";
4
6
  export function normalizeServiceTreeManifest(manifest) {
5
- const serviceCode = manifest.service.code;
7
+ const service = normalizeServiceInfo(manifest);
8
+ const serviceCode = service.code;
9
+ const appBindings = normalizeAppBindings(manifest);
10
+ const defaultAppRef = resolveDefaultAppRef(manifest, appBindings);
11
+ const commands = [
12
+ ...normalizeLegacyCommands(manifest, serviceCode, defaultAppRef),
13
+ ...normalizeResourceCommands(manifest, serviceCode, defaultAppRef),
14
+ ];
6
15
  return {
7
16
  protocol: manifest.protocol || DEFAULT_PROTOCOL,
8
- version: manifest.version,
9
- service: { ...manifest.service },
10
- appBindings: { ...(manifest.appBindings || {}) },
11
- commands: manifest.commands.map((command) => {
12
- const path = splitServiceTreePath(command.path);
13
- const fullPath = path[0] === serviceCode ? path : [serviceCode, ...path];
14
- const flags = (command.flags || []).map((flag) => ({
15
- ...flag,
16
- cliName: flag.cliName ? toKebabCase(flag.cliName) : toKebabCase(flag.name),
17
+ version: manifest.version || DEFAULT_VERSION,
18
+ service,
19
+ appBindings,
20
+ commands,
21
+ };
22
+ }
23
+ function normalizeServiceInfo(manifest) {
24
+ if (typeof manifest.service === "string") {
25
+ return {
26
+ code: manifest.service,
27
+ name: manifest.name,
28
+ description: manifest.description,
29
+ };
30
+ }
31
+ return {
32
+ ...manifest.service,
33
+ name: manifest.service.name || manifest.name,
34
+ description: manifest.service.description || manifest.description,
35
+ };
36
+ }
37
+ function normalizeAppBindings(manifest) {
38
+ const bindings = {};
39
+ if (manifest.appBindings) {
40
+ for (const [key, value] of Object.entries(manifest.appBindings)) {
41
+ bindings[key] = normalizeAppBinding(value);
42
+ }
43
+ }
44
+ if (manifest.apps) {
45
+ for (const [key, value] of Object.entries(manifest.apps)) {
46
+ bindings[key] = normalizeAppBinding(value);
47
+ }
48
+ }
49
+ if (manifest.app !== undefined && !bindings[DEFAULT_APP_REF]) {
50
+ bindings[DEFAULT_APP_REF] = normalizeAppBinding(manifest.app);
51
+ }
52
+ return bindings;
53
+ }
54
+ function normalizeAppBinding(input) {
55
+ if (typeof input !== "string")
56
+ return { ...input };
57
+ return input.startsWith("app-")
58
+ ? { appcode: input }
59
+ : { app: input };
60
+ }
61
+ function resolveDefaultAppRef(manifest, appBindings) {
62
+ if (manifest.app !== undefined)
63
+ return DEFAULT_APP_REF;
64
+ const keys = Object.keys(appBindings);
65
+ return keys.length === 1 ? keys[0] : undefined;
66
+ }
67
+ function normalizeLegacyCommands(manifest, serviceCode, defaultAppRef) {
68
+ return (manifest.commands || []).map((command) => normalizeCommand(command, serviceCode, defaultAppRef, manifest.defaults));
69
+ }
70
+ function normalizeResourceCommands(manifest, serviceCode, defaultAppRef) {
71
+ return flattenResources({
72
+ resources: manifest.resources || {},
73
+ serviceCode,
74
+ defaultAppRef,
75
+ path: [],
76
+ inherited: {
77
+ appRef: defaultAppRef,
78
+ defaults: manifest.defaults || {},
79
+ },
80
+ });
81
+ }
82
+ function flattenResources(input) {
83
+ const commands = [];
84
+ for (const [resourceCode, resource] of Object.entries(input.resources)) {
85
+ const path = [...input.path, resourceCode];
86
+ const inherited = {
87
+ appRef: resource.appRef || resource.app || input.inherited.appRef,
88
+ datasetCode: resource.datasetCode || input.inherited.datasetCode,
89
+ datatable: resource.datatable || resource.table || input.inherited.datatable,
90
+ defaults: {
91
+ ...input.inherited.defaults,
92
+ ...(resource.defaults || {}),
93
+ ...(resource.params || {}),
94
+ },
95
+ };
96
+ for (const [actionCode, action] of Object.entries(resource.actions || {})) {
97
+ commands.push(normalizeActionCommand({
98
+ serviceCode: input.serviceCode,
99
+ defaultAppRef: input.defaultAppRef,
100
+ path: [...path, actionCode],
101
+ resource,
102
+ action,
103
+ inherited,
17
104
  }));
18
- return {
105
+ }
106
+ if (resource.resources) {
107
+ commands.push(...flattenResources({
108
+ resources: resource.resources,
109
+ serviceCode: input.serviceCode,
110
+ defaultAppRef: input.defaultAppRef,
19
111
  path,
20
- fullPath,
21
- cliPath: fullPath.join(" "),
22
- description: command.description,
23
- risk: command.risk || "read",
24
- args: command.args || [],
25
- flags,
26
- target: { ...command.target },
27
- baseParams: {
28
- ...(command.target.params || {}),
29
- ...(command.params || {}),
30
- },
31
- mapTo: normalizeMapTo(command.mapTo, flags),
32
- };
33
- }),
112
+ inherited,
113
+ }));
114
+ }
115
+ }
116
+ return commands;
117
+ }
118
+ function normalizeActionCommand(input) {
119
+ const action = input.action;
120
+ const target = normalizeActionTarget(action, input.inherited);
121
+ const args = normalizeArgs(action.args);
122
+ const flags = normalizeFlags(action.flags);
123
+ const mapTo = normalizeMapInput(action.mapTo || action.map, args, flags);
124
+ return buildNormalizedCommand({
125
+ serviceCode: input.serviceCode,
126
+ path: input.path,
127
+ description: action.description,
128
+ risk: action.risk,
129
+ args,
130
+ flags,
131
+ target,
132
+ params: {
133
+ ...input.inherited.defaults,
134
+ ...(action.defaults || {}),
135
+ ...(action.params || {}),
136
+ },
137
+ mapTo,
138
+ });
139
+ }
140
+ function normalizeCommand(command, serviceCode, defaultAppRef, serviceDefaults) {
141
+ const args = normalizeArgs(command.args);
142
+ const flags = normalizeFlags(command.flags);
143
+ const mapTo = normalizeMapInput(command.mapTo || command.map, args, flags);
144
+ const target = {
145
+ ...command.target,
146
+ appRef: command.target.appRef || defaultAppRef,
34
147
  };
148
+ return buildNormalizedCommand({
149
+ serviceCode,
150
+ path: splitServiceTreePath(command.path),
151
+ description: command.description,
152
+ risk: command.risk,
153
+ args,
154
+ flags,
155
+ target,
156
+ params: {
157
+ ...(serviceDefaults || {}),
158
+ ...(command.defaults || {}),
159
+ ...(command.params || {}),
160
+ },
161
+ mapTo,
162
+ });
163
+ }
164
+ function buildNormalizedCommand(input) {
165
+ const fullPath = input.path[0] === input.serviceCode
166
+ ? input.path
167
+ : [input.serviceCode, ...input.path];
168
+ return {
169
+ path: input.path,
170
+ fullPath,
171
+ cliPath: fullPath.join(" "),
172
+ description: input.description,
173
+ risk: input.risk || inferRisk(input.target),
174
+ args: input.args,
175
+ flags: input.flags,
176
+ target: { ...input.target },
177
+ baseParams: {
178
+ ...(input.target.params || {}),
179
+ ...input.params,
180
+ },
181
+ mapTo: normalizeMapTo(input.mapTo, input.flags),
182
+ };
183
+ }
184
+ function normalizeActionTarget(action, inherited) {
185
+ if (typeof action.target === "object") {
186
+ return {
187
+ ...action.target,
188
+ appRef: action.target.appRef || action.appRef || action.app || inherited.appRef,
189
+ datasetCode: action.target.datasetCode || action.datasetCode || inherited.datasetCode,
190
+ datatable: action.target.datatable || action.datatable || action.table || inherited.datatable,
191
+ sqlCode: action.target.sqlCode || action.sqlCode,
192
+ bffCode: action.target.bffCode || action.bffCode,
193
+ bffId: action.target.bffId || action.bffId,
194
+ scriptName: action.target.scriptName || action.scriptName,
195
+ };
196
+ }
197
+ const parsed = parseTargetShorthand(action.target || action.action, action.kind, action.command);
198
+ return {
199
+ kind: parsed.kind,
200
+ command: parsed.command,
201
+ appRef: action.appRef || action.app || inherited.appRef,
202
+ datasetCode: action.datasetCode || inherited.datasetCode,
203
+ datatable: action.datatable || action.table || inherited.datatable,
204
+ sqlCode: action.sqlCode,
205
+ bffCode: action.bffCode,
206
+ bffId: action.bffId,
207
+ scriptName: action.scriptName,
208
+ };
209
+ }
210
+ function parseTargetShorthand(value, kind, command) {
211
+ if (typeof value === "string" && value.includes(".")) {
212
+ const [targetKind, targetCommand] = value.split(".", 2);
213
+ return {
214
+ kind: targetKind,
215
+ command: targetCommand,
216
+ };
217
+ }
218
+ return {
219
+ kind: kind || "data",
220
+ command: command || (typeof value === "string" ? value : "filter"),
221
+ };
222
+ }
223
+ function normalizeArgs(args) {
224
+ return (args || []).map((arg) => (typeof arg === "string" ? { name: arg, required: true } : { ...arg }));
225
+ }
226
+ function normalizeFlags(flags) {
227
+ if (!flags)
228
+ return [];
229
+ if (Array.isArray(flags)) {
230
+ return flags.map((flag) => ({
231
+ ...flag,
232
+ cliName: flag.cliName ? toKebabCase(flag.cliName) : toKebabCase(flag.name),
233
+ }));
234
+ }
235
+ return Object.entries(flags).map(([name, value]) => {
236
+ if (typeof value === "string") {
237
+ return {
238
+ name,
239
+ cliName: toKebabCase(name),
240
+ type: "string",
241
+ mapTo: value,
242
+ };
243
+ }
244
+ return {
245
+ name,
246
+ cliName: value.cliName ? toKebabCase(value.cliName) : toKebabCase(name),
247
+ type: value.type || "string",
248
+ description: value.description,
249
+ required: value.required,
250
+ default: value.default,
251
+ enum: value.enum,
252
+ mapTo: {
253
+ target: value.target || value.to || "",
254
+ operator: value.operator || value.op,
255
+ transform: value.transform,
256
+ omitEmpty: value.omitEmpty,
257
+ },
258
+ };
259
+ });
260
+ }
261
+ function normalizeMapInput(mapTo, args, flags) {
262
+ if (!mapTo || Array.isArray(mapTo))
263
+ return mapTo;
264
+ const argNames = new Set(args.map((arg) => arg.name));
265
+ const flagNames = new Set(flags.flatMap((flag) => [flag.name, flag.cliName]));
266
+ const normalized = {};
267
+ for (const [source, target] of Object.entries(mapTo)) {
268
+ normalized[normalizeMapSource(source, argNames, flagNames)] = target;
269
+ }
270
+ return normalized;
271
+ }
272
+ function normalizeMapSource(source, argNames, flagNames) {
273
+ if (source === "const"
274
+ || source.startsWith("flags.")
275
+ || source.startsWith("args.")
276
+ || source.startsWith("const.")
277
+ || source.startsWith("context.")
278
+ || source.startsWith("ctx.")) {
279
+ return source;
280
+ }
281
+ if (argNames.has(source))
282
+ return `args.${source}`;
283
+ if (flagNames.has(source))
284
+ return `flags.${source}`;
285
+ return `flags.${source}`;
286
+ }
287
+ function inferRisk(target) {
288
+ if (target.kind !== "data")
289
+ return "read";
290
+ if (target.command === "delete")
291
+ return "high-risk-write";
292
+ if (target.command === "create" || target.command === "batchCreate" || target.command === "update") {
293
+ return "write";
294
+ }
295
+ return "read";
35
296
  }
@@ -14,11 +14,13 @@ export interface ServiceTreeAppBinding {
14
14
  appCode?: string;
15
15
  env?: string;
16
16
  }
17
+ export type ServiceTreeAppBindingInput = string | ServiceTreeAppBinding;
17
18
  export interface ServiceTreeArg {
18
19
  name: string;
19
20
  description?: string;
20
21
  required?: boolean;
21
22
  }
23
+ export type ServiceTreeArgInput = string | ServiceTreeArg;
22
24
  export interface ServiceTreeMapToRule {
23
25
  source?: string;
24
26
  target: string;
@@ -39,6 +41,21 @@ export interface ServiceTreeFlag {
39
41
  enum?: string[];
40
42
  mapTo?: string | Omit<ServiceTreeMapToRule, "source">;
41
43
  }
44
+ export interface ServiceTreeSimpleFlag {
45
+ to?: string;
46
+ target?: string;
47
+ op?: string;
48
+ operator?: string;
49
+ type?: ServiceTreeValueType;
50
+ description?: string;
51
+ required?: boolean;
52
+ default?: unknown;
53
+ enum?: string[];
54
+ cliName?: string;
55
+ transform?: ServiceTreeMapTransform;
56
+ omitEmpty?: boolean;
57
+ }
58
+ export type ServiceTreeFlagsInput = ServiceTreeFlag[] | Record<string, string | ServiceTreeSimpleFlag>;
42
59
  export interface ServiceTreeTarget {
43
60
  kind: ServiceTreeTargetKind;
44
61
  command: ServiceTreeTargetCommand;
@@ -55,18 +72,62 @@ export interface ServiceTreeCommand {
55
72
  path: string | string[];
56
73
  description?: string;
57
74
  risk?: Risk;
58
- args?: ServiceTreeArg[];
59
- flags?: ServiceTreeFlag[];
75
+ args?: ServiceTreeArgInput[];
76
+ flags?: ServiceTreeFlagsInput;
60
77
  target: ServiceTreeTarget;
61
78
  params?: Record<string, unknown>;
79
+ defaults?: Record<string, unknown>;
80
+ map?: ServiceTreeMapTo;
81
+ mapTo?: ServiceTreeMapTo;
82
+ }
83
+ export interface ServiceTreeAction {
84
+ description?: string;
85
+ risk?: Risk;
86
+ action?: ServiceTreeTargetCommand | `${ServiceTreeTargetKind}.${ServiceTreeTargetCommand}`;
87
+ target?: ServiceTreeTarget | `${ServiceTreeTargetKind}.${ServiceTreeTargetCommand}`;
88
+ kind?: ServiceTreeTargetKind;
89
+ command?: ServiceTreeTargetCommand;
90
+ appRef?: string;
91
+ app?: string;
92
+ datasetCode?: string;
93
+ datatable?: string;
94
+ table?: string;
95
+ sqlCode?: string;
96
+ bffCode?: string;
97
+ bffId?: string | number;
98
+ scriptName?: string;
99
+ args?: ServiceTreeArgInput[];
100
+ flags?: ServiceTreeFlagsInput;
101
+ defaults?: Record<string, unknown>;
102
+ params?: Record<string, unknown>;
103
+ map?: ServiceTreeMapTo;
62
104
  mapTo?: ServiceTreeMapTo;
63
105
  }
106
+ export interface ServiceTreeResource {
107
+ name?: string;
108
+ description?: string;
109
+ appRef?: string;
110
+ app?: string;
111
+ datasetCode?: string;
112
+ datatable?: string;
113
+ table?: string;
114
+ defaults?: Record<string, unknown>;
115
+ params?: Record<string, unknown>;
116
+ actions?: Record<string, ServiceTreeAction>;
117
+ resources?: Record<string, ServiceTreeResource>;
118
+ }
64
119
  export interface ServiceTreeManifest {
65
120
  protocol?: string;
66
- version: string;
67
- service: ServiceTreeServiceInfo;
68
- appBindings?: Record<string, ServiceTreeAppBinding>;
69
- commands: ServiceTreeCommand[];
121
+ version?: string;
122
+ service: string | ServiceTreeServiceInfo;
123
+ name?: string;
124
+ description?: string;
125
+ app?: ServiceTreeAppBindingInput;
126
+ apps?: Record<string, ServiceTreeAppBindingInput>;
127
+ appBindings?: Record<string, ServiceTreeAppBindingInput>;
128
+ defaults?: Record<string, unknown>;
129
+ commands?: ServiceTreeCommand[];
130
+ resources?: Record<string, ServiceTreeResource>;
70
131
  }
71
132
  export interface NormalizedServiceTreeFlag extends ServiceTreeFlag {
72
133
  cliName: string;
@@ -1,4 +1,5 @@
1
- import { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, splitServiceTreePath, toKebabCase, } from "./path.js";
1
+ import { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, } from "./path.js";
2
+ import { normalizeServiceTreeManifest } from "./normalize.js";
2
3
  const DATA_COMMANDS = new Set(["filter", "getOne", "aggregate", "create", "batchCreate", "update", "delete"]);
3
4
  const SQL_COMMANDS = new Set(["exec"]);
4
5
  const BFF_COMMANDS = new Set(["exec"]);
@@ -9,35 +10,54 @@ export function validateServiceTreeManifest(manifest) {
9
10
  push(issues, "error", "$", "Manifest must be a JSON object.");
10
11
  return buildReport(issues);
11
12
  }
12
- if (typeof manifest.version !== "string" || !manifest.version.trim()) {
13
- push(issues, "error", "$.version", "version is required.");
13
+ if (typeof manifest.service === "string") {
14
+ if (!isServiceCode(manifest.service)) {
15
+ push(issues, "error", "$.service", "service must be lower kebab-case.");
16
+ }
14
17
  }
15
- if (!isRecord(manifest.service)) {
18
+ else if (!isRecord(manifest.service)) {
16
19
  push(issues, "error", "$.service", "service is required.");
17
20
  }
18
21
  else if (typeof manifest.service.code !== "string" || !isServiceCode(manifest.service.code)) {
19
22
  push(issues, "error", "$.service.code", "service.code must be lower kebab-case.");
20
23
  }
21
- if (!Array.isArray(manifest.commands) || manifest.commands.length === 0) {
22
- push(issues, "error", "$.commands", "commands must be a non-empty array.");
24
+ let hasShapeError = false;
25
+ if (manifest.commands !== undefined && !Array.isArray(manifest.commands)) {
26
+ push(issues, "error", "$.commands", "commands must be an array.");
27
+ hasShapeError = true;
28
+ }
29
+ if (manifest.resources !== undefined && !isRecord(manifest.resources)) {
30
+ push(issues, "error", "$.resources", "resources must be an object.");
31
+ hasShapeError = true;
32
+ }
33
+ if (hasShapeError) {
34
+ return buildReport(issues);
35
+ }
36
+ if (!Array.isArray(manifest.commands) && !isRecord(manifest.resources)) {
37
+ push(issues, "error", "$", "commands or resources is required.");
38
+ return buildReport(issues);
39
+ }
40
+ let normalized;
41
+ try {
42
+ normalized = normalizeServiceTreeManifest(manifest);
43
+ }
44
+ catch (error) {
45
+ push(issues, "error", "$", error instanceof Error ? error.message : String(error));
46
+ return buildReport(issues);
47
+ }
48
+ if (normalized.commands.length === 0) {
49
+ push(issues, "error", "$", "Service tree must define at least one command.");
23
50
  return buildReport(issues);
24
51
  }
25
- const serviceCode = isRecord(manifest.service) && typeof manifest.service.code === "string"
26
- ? manifest.service.code
27
- : "";
28
52
  const seenPaths = new Set();
29
- manifest.commands.forEach((command, index) => {
30
- validateCommand(command, index, serviceCode, seenPaths, issues);
53
+ normalized.commands.forEach((command, index) => {
54
+ validateCommand(command, index, seenPaths, issues);
31
55
  });
32
56
  return buildReport(issues);
33
57
  }
34
- function validateCommand(command, index, serviceCode, seenPaths, issues) {
58
+ function validateCommand(command, index, seenPaths, issues) {
35
59
  const base = `$.commands[${index}]`;
36
- if (!isRecord(command)) {
37
- push(issues, "error", base, "Command must be an object.");
38
- return;
39
- }
40
- const path = splitServiceTreePath(command.path || []);
60
+ const path = command.path;
41
61
  if (path.length === 0) {
42
62
  push(issues, "error", `${base}.path`, "path is required.");
43
63
  }
@@ -46,8 +66,7 @@ function validateCommand(command, index, serviceCode, seenPaths, issues) {
46
66
  push(issues, "error", `${base}.path[${partIndex}]`, "Command path segments must be lower kebab-case.");
47
67
  }
48
68
  }
49
- const fullPath = path[0] === serviceCode ? path : [serviceCode, ...path];
50
- const fullKey = fullPath.join(" ");
69
+ const fullKey = command.fullPath.join(" ");
51
70
  if (seenPaths.has(fullKey)) {
52
71
  push(issues, "error", `${base}.path`, `Duplicate command path: ${fullKey}.`);
53
72
  }
@@ -105,6 +124,7 @@ function validateFlags(flags, path, issues) {
105
124
  return;
106
125
  }
107
126
  const seen = new Set();
127
+ const seenCliNames = new Set();
108
128
  flags.forEach((flag, index) => {
109
129
  const flagPath = `${path}[${index}]`;
110
130
  if (!isRecord(flag)) {
@@ -119,9 +139,11 @@ function validateFlags(flags, path, issues) {
119
139
  push(issues, "error", `${flagPath}.name`, `Duplicate flag name: ${flag.name}.`);
120
140
  }
121
141
  seen.add(flag.name);
122
- const cliName = typeof flag.cliName === "string" ? flag.cliName : toKebabCase(flag.name);
123
- if (cliName !== toKebabCase(cliName)) {
124
- push(issues, "error", `${flagPath}.cliName`, "flag.cliName must be kebab-case.");
142
+ if (typeof flag.cliName === "string") {
143
+ if (seenCliNames.has(flag.cliName)) {
144
+ push(issues, "error", `${flagPath}.cliName`, `Duplicate CLI flag name: ${flag.cliName}.`);
145
+ }
146
+ seenCliNames.add(flag.cliName);
125
147
  }
126
148
  if (typeof flag.type !== "string" || !VALUE_TYPES.has(flag.type)) {
127
149
  push(issues, "error", `${flagPath}.type`, "flag.type must be string, number, boolean, or json.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrabet/cli-framework",
3
- "version": "1.0.4-beta.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",