runspec-node 0.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.
Files changed (64) hide show
  1. package/bin/runspec.js +4 -0
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +384 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/errors.d.ts +25 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +146 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/finder.d.ts +6 -0
  11. package/dist/finder.d.ts.map +1 -0
  12. package/dist/finder.js +91 -0
  13. package/dist/finder.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +22 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/inference.d.ts +10 -0
  19. package/dist/inference.d.ts.map +1 -0
  20. package/dist/inference.js +69 -0
  21. package/dist/inference.js.map +1 -0
  22. package/dist/loader.d.ts +3 -0
  23. package/dist/loader.d.ts.map +1 -0
  24. package/dist/loader.js +142 -0
  25. package/dist/loader.js.map +1 -0
  26. package/dist/models.d.ts +57 -0
  27. package/dist/models.d.ts.map +1 -0
  28. package/dist/models.js +3 -0
  29. package/dist/models.js.map +1 -0
  30. package/dist/parser.d.ts +10 -0
  31. package/dist/parser.d.ts.map +1 -0
  32. package/dist/parser.js +251 -0
  33. package/dist/parser.js.map +1 -0
  34. package/dist/serve.d.ts +2 -0
  35. package/dist/serve.d.ts.map +1 -0
  36. package/dist/serve.js +199 -0
  37. package/dist/serve.js.map +1 -0
  38. package/dist/types.d.ts +6 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +121 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/validator.d.ts +5 -0
  43. package/dist/validator.d.ts.map +1 -0
  44. package/dist/validator.js +56 -0
  45. package/dist/validator.js.map +1 -0
  46. package/jest.config.js +8 -0
  47. package/package.json +36 -0
  48. package/src/cli.ts +378 -0
  49. package/src/errors.ts +126 -0
  50. package/src/finder.ts +59 -0
  51. package/src/index.ts +6 -0
  52. package/src/inference.ts +77 -0
  53. package/src/loader.ts +120 -0
  54. package/src/models.ts +61 -0
  55. package/src/parser.ts +239 -0
  56. package/src/serve.ts +196 -0
  57. package/src/types.ts +96 -0
  58. package/src/validator.ts +64 -0
  59. package/tests/test_inference.test.ts +153 -0
  60. package/tests/test_integration.test.ts +197 -0
  61. package/tests/test_loader.test.ts +169 -0
  62. package/tests/test_types.test.ts +110 -0
  63. package/tests/test_validator.test.ts +120 -0
  64. package/tsconfig.json +18 -0
@@ -0,0 +1,110 @@
1
+ import { coerce, registerType, listTypes } from '../src/types';
2
+ import type { ArgSpec } from '../src/models';
3
+
4
+ function spec(overrides: Partial<ArgSpec> = {}): ArgSpec {
5
+ return { name: 'test', ...overrides } as ArgSpec;
6
+ }
7
+
8
+ // ── str ───────────────────────────────────────────────────────────────────────
9
+
10
+ test('coerces string', () => {
11
+ expect(coerce('hello', spec({ type: 'str' }))).toBe('hello');
12
+ });
13
+
14
+ test('coerces number to string', () => {
15
+ expect(coerce(42, spec({ type: 'str' }))).toBe('42');
16
+ });
17
+
18
+ // ── int ───────────────────────────────────────────────────────────────────────
19
+
20
+ test('coerces integer string', () => {
21
+ expect(coerce('42', spec({ type: 'int' }))).toBe(42);
22
+ });
23
+
24
+ test('coerces number to int', () => {
25
+ expect(coerce(10, spec({ type: 'int' }))).toBe(10);
26
+ });
27
+
28
+ test('rejects float string as int', () => {
29
+ expect(() => coerce('3.14', spec({ type: 'int' }))).toThrow();
30
+ });
31
+
32
+ test('int respects range', () => {
33
+ expect(() => coerce('50', spec({ type: 'int', range: [1, 32] }))).toThrow();
34
+ });
35
+
36
+ test('int within range passes', () => {
37
+ expect(coerce('4', spec({ type: 'int', range: [1, 32] }))).toBe(4);
38
+ });
39
+
40
+ // ── float ─────────────────────────────────────────────────────────────────────
41
+
42
+ test('coerces float string', () => {
43
+ expect(coerce('3.14', spec({ type: 'float' }))).toBeCloseTo(3.14);
44
+ });
45
+
46
+ test('rejects invalid float', () => {
47
+ expect(() => coerce('abc', spec({ type: 'float' }))).toThrow();
48
+ });
49
+
50
+ // ── bool ─────────────────────────────────────────────────────────────────────
51
+
52
+ test('coerces true string', () => {
53
+ expect(coerce('true', spec({ type: 'bool' }))).toBe(true);
54
+ });
55
+
56
+ test('coerces yes string', () => {
57
+ expect(coerce('yes', spec({ type: 'bool' }))).toBe(true);
58
+ });
59
+
60
+ test('coerces false string', () => {
61
+ expect(coerce('false', spec({ type: 'bool' }))).toBe(false);
62
+ });
63
+
64
+ test('coerces boolean directly', () => {
65
+ expect(coerce(true, spec({ type: 'bool' }))).toBe(true);
66
+ });
67
+
68
+ test('rejects invalid bool', () => {
69
+ expect(() => coerce('maybe', spec({ type: 'bool' }))).toThrow();
70
+ });
71
+
72
+ // ── flag ─────────────────────────────────────────────────────────────────────
73
+
74
+ test('flag true when boolean true', () => {
75
+ expect(coerce(true, spec({ type: 'flag' }))).toBe(true);
76
+ });
77
+
78
+ test('flag false when boolean false', () => {
79
+ expect(coerce(false, spec({ type: 'flag' }))).toBe(false);
80
+ });
81
+
82
+ // ── path ─────────────────────────────────────────────────────────────────────
83
+
84
+ test('coerces path to absolute', () => {
85
+ const result = coerce('/tmp/data', spec({ type: 'path' })) as string;
86
+ expect(result).toBe('/tmp/data');
87
+ expect(result.startsWith('/')).toBe(true);
88
+ });
89
+
90
+ // ── choice ───────────────────────────────────────────────────────────────────
91
+
92
+ test('accepts valid choice', () => {
93
+ expect(coerce('json', spec({ type: 'choice', options: ['json', 'csv'] }))).toBe('json');
94
+ });
95
+
96
+ test('rejects invalid choice', () => {
97
+ expect(() => coerce('xml', spec({ type: 'choice', options: ['json', 'csv'] }))).toThrow();
98
+ });
99
+
100
+ // ── custom types ──────────────────────────────────────────────────────────────
101
+
102
+ test('registerType adds custom coercer', () => {
103
+ registerType('upper', (v) => String(v).toUpperCase());
104
+ expect(coerce('hello', spec({ type: 'upper' }))).toBe('HELLO');
105
+ expect(listTypes()).toContain('upper');
106
+ });
107
+
108
+ test('unknown type throws', () => {
109
+ expect(() => coerce('x', spec({ type: 'nonexistent-xyz' }))).toThrow();
110
+ });
@@ -0,0 +1,120 @@
1
+ import { validateArgs, validateGroups, raiseIfErrors } from '../src/validator';
2
+ import { RunSpecError } from '../src/errors';
3
+ import type { ArgSpec, GroupSpec } from '../src/models';
4
+
5
+ function argSpec(overrides: Partial<ArgSpec> = {}): ArgSpec {
6
+ return { name: 'test', ...overrides } as ArgSpec;
7
+ }
8
+
9
+ function groupSpec(overrides: Partial<GroupSpec> = {}): GroupSpec {
10
+ return { name: 'g', args: [], ...overrides } as GroupSpec;
11
+ }
12
+
13
+ // ── validateArgs ──────────────────────────────────────────────────────────────
14
+
15
+ test('passes when required arg present', () => {
16
+ const errors = validateArgs({ name: 'Alice' }, { name: argSpec({ name: 'name', required: true }) });
17
+ expect(errors).toHaveLength(0);
18
+ });
19
+
20
+ test('errors when required arg missing', () => {
21
+ const errors = validateArgs({ name: undefined }, { name: argSpec({ name: 'name', required: true }) });
22
+ expect(errors).toHaveLength(1);
23
+ expect(errors[0]).toContain('--name');
24
+ });
25
+
26
+ test('passes when optional arg missing', () => {
27
+ const errors = validateArgs({ verbose: undefined }, { verbose: argSpec({ name: 'verbose', required: false }) });
28
+ expect(errors).toHaveLength(0);
29
+ });
30
+
31
+ test('error message includes type', () => {
32
+ const errors = validateArgs(
33
+ { input: undefined },
34
+ { input: argSpec({ name: 'input', required: true, type: 'path' }) },
35
+ );
36
+ expect(errors[0]).toContain('path');
37
+ });
38
+
39
+ test('error message includes env tip when env set', () => {
40
+ const errors = validateArgs(
41
+ { apiKey: undefined },
42
+ { apiKey: argSpec({ name: 'api-key', required: true, env: 'API_KEY' }) },
43
+ );
44
+ expect(errors[0]).toContain('API_KEY');
45
+ });
46
+
47
+ // ── validateGroups ────────────────────────────────────────────────────────────
48
+
49
+ test('exclusive: passes when one provided', () => {
50
+ const errors = validateGroups({ format: 'json', raw: undefined }, { g: groupSpec({ args: ['format', 'raw'], exclusive: true }) });
51
+ expect(errors).toHaveLength(0);
52
+ });
53
+
54
+ test('exclusive: errors when two provided', () => {
55
+ const errors = validateGroups({ format: 'json', raw: true }, { g: groupSpec({ args: ['format', 'raw'], exclusive: true }) });
56
+ expect(errors).toHaveLength(1);
57
+ expect(errors[0]).toContain('Conflicting');
58
+ });
59
+
60
+ test('inclusive: passes when all provided', () => {
61
+ const errors = validateGroups(
62
+ { apiKey: 'x', apiEndpoint: 'y' },
63
+ { g: groupSpec({ args: ['apiKey', 'apiEndpoint'], inclusive: true }) },
64
+ );
65
+ expect(errors).toHaveLength(0);
66
+ });
67
+
68
+ test('inclusive: errors when partial', () => {
69
+ const errors = validateGroups(
70
+ { apiKey: 'x', apiEndpoint: undefined },
71
+ { g: groupSpec({ args: ['apiKey', 'apiEndpoint'], inclusive: true }) },
72
+ );
73
+ expect(errors).toHaveLength(1);
74
+ expect(errors[0]).toContain('Incomplete');
75
+ });
76
+
77
+ test('atLeastOne: passes when one provided', () => {
78
+ const errors = validateGroups({ a: 'x', b: undefined }, { g: groupSpec({ args: ['a', 'b'], atLeastOne: true }) });
79
+ expect(errors).toHaveLength(0);
80
+ });
81
+
82
+ test('atLeastOne: errors when none provided', () => {
83
+ const errors = validateGroups({ a: undefined, b: undefined }, { g: groupSpec({ args: ['a', 'b'], atLeastOne: true }) });
84
+ expect(errors).toHaveLength(1);
85
+ });
86
+
87
+ test('exactlyOne: passes when exactly one provided', () => {
88
+ const errors = validateGroups({ a: 'x', b: undefined }, { g: groupSpec({ args: ['a', 'b'], exactlyOne: true }) });
89
+ expect(errors).toHaveLength(0);
90
+ });
91
+
92
+ test('exactlyOne: errors when none provided', () => {
93
+ const errors = validateGroups({ a: undefined, b: undefined }, { g: groupSpec({ args: ['a', 'b'], exactlyOne: true }) });
94
+ expect(errors).toHaveLength(1);
95
+ });
96
+
97
+ test('exactlyOne: errors when two provided', () => {
98
+ const errors = validateGroups({ a: 'x', b: 'y' }, { g: groupSpec({ args: ['a', 'b'], exactlyOne: true }) });
99
+ expect(errors).toHaveLength(1);
100
+ });
101
+
102
+ // ── raiseIfErrors ─────────────────────────────────────────────────────────────
103
+
104
+ test('raiseIfErrors does not throw on empty list', () => {
105
+ expect(() => raiseIfErrors([])).not.toThrow();
106
+ });
107
+
108
+ test('raiseIfErrors throws RunSpecError with all messages', () => {
109
+ expect(() => raiseIfErrors(['error one', 'error two'])).toThrow(RunSpecError);
110
+ });
111
+
112
+ test('raiseIfErrors message joins errors', () => {
113
+ try {
114
+ raiseIfErrors(['first', 'second']);
115
+ fail('should have thrown');
116
+ } catch (e) {
117
+ expect((e as Error).message).toContain('first');
118
+ expect((e as Error).message).toContain('second');
119
+ }
120
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "strict": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "esModuleInterop": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["src"],
17
+ "exclude": ["node_modules", "dist", "tests"]
18
+ }