cli4ai 0.9.2 → 1.0.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.
@@ -1,327 +0,0 @@
1
- /**
2
- * Tests for manifest.ts
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
- import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
7
- import { join } from 'path';
8
- import { tmpdir } from 'os';
9
- import {
10
- validateManifest,
11
- loadManifestOrThrow,
12
- tryLoadManifest,
13
- findManifest,
14
- createManifest,
15
- MANIFEST_FILENAME,
16
- ManifestValidationError,
17
- ManifestNotFoundError,
18
- ManifestParseError
19
- } from './manifest.js';
20
-
21
- describe('manifest', () => {
22
- let tempDir: string;
23
-
24
- beforeEach(() => {
25
- tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-manifest-test-'));
26
- });
27
-
28
- afterEach(() => {
29
- rmSync(tempDir, { recursive: true, force: true });
30
- });
31
-
32
- describe('validateManifest', () => {
33
- test('accepts valid minimal manifest', () => {
34
- const manifest = validateManifest({
35
- name: 'my-tool',
36
- version: '1.0.0',
37
- entry: 'run.ts'
38
- });
39
-
40
- expect(manifest.name).toBe('my-tool');
41
- expect(manifest.version).toBe('1.0.0');
42
- expect(manifest.entry).toBe('run.ts');
43
- });
44
-
45
- test('accepts valid full manifest', () => {
46
- const manifest = validateManifest({
47
- name: 'github',
48
- version: '2.1.0',
49
- entry: 'run.ts',
50
- description: 'GitHub CLI wrapper',
51
- author: 'cliforai',
52
- license: 'MIT',
53
- runtime: 'bun',
54
- commands: {
55
- notifs: { description: 'Your notifications' },
56
- repos: {
57
- description: 'List repos',
58
- args: [{ name: 'user', required: false }]
59
- }
60
- },
61
- env: {
62
- GITHUB_TOKEN: { required: false, description: 'Token' }
63
- },
64
- mcp: { enabled: true }
65
- });
66
-
67
- expect(manifest.name).toBe('github');
68
- expect(manifest.commands?.notifs.description).toBe('Your notifications');
69
- expect(manifest.mcp?.enabled).toBe(true);
70
- });
71
-
72
- test('accepts prerelease versions', () => {
73
- const manifest = validateManifest({
74
- name: 'tool',
75
- version: '1.0.0-beta.1',
76
- entry: 'run.ts'
77
- });
78
-
79
- expect(manifest.version).toBe('1.0.0-beta.1');
80
- });
81
-
82
- test('accepts all valid runtimes', () => {
83
- for (const runtime of ['bun', 'node'] as const) {
84
- const manifest = validateManifest({
85
- name: 'tool',
86
- version: '1.0.0',
87
- entry: 'run.ts',
88
- runtime
89
- });
90
- expect(manifest.runtime).toBe(runtime);
91
- }
92
- });
93
-
94
- test('rejects null manifest', () => {
95
- expect(() => validateManifest(null)).toThrow();
96
- });
97
-
98
- test('rejects non-object manifest', () => {
99
- expect(() => validateManifest('string')).toThrow();
100
- expect(() => validateManifest(123)).toThrow();
101
- expect(() => validateManifest([])).toThrow();
102
- });
103
-
104
- test('rejects missing name', () => {
105
- expect(() => validateManifest({
106
- version: '1.0.0',
107
- entry: 'run.ts'
108
- })).toThrow();
109
- });
110
-
111
- test('rejects invalid name format', () => {
112
- expect(() => validateManifest({
113
- name: 'Invalid-Name',
114
- version: '1.0.0',
115
- entry: 'run.ts'
116
- })).toThrow();
117
-
118
- expect(() => validateManifest({
119
- name: '123invalid',
120
- version: '1.0.0',
121
- entry: 'run.ts'
122
- })).toThrow();
123
-
124
- expect(() => validateManifest({
125
- name: 'has_underscore',
126
- version: '1.0.0',
127
- entry: 'run.ts'
128
- })).toThrow();
129
- });
130
-
131
- test('rejects missing version', () => {
132
- expect(() => validateManifest({
133
- name: 'tool',
134
- entry: 'run.ts'
135
- })).toThrow();
136
- });
137
-
138
- test('rejects invalid version format', () => {
139
- expect(() => validateManifest({
140
- name: 'tool',
141
- version: 'invalid',
142
- entry: 'run.ts'
143
- })).toThrow();
144
-
145
- expect(() => validateManifest({
146
- name: 'tool',
147
- version: '1.0',
148
- entry: 'run.ts'
149
- })).toThrow();
150
- });
151
-
152
- test('rejects missing entry', () => {
153
- expect(() => validateManifest({
154
- name: 'tool',
155
- version: '1.0.0'
156
- })).toThrow();
157
- });
158
-
159
- test('rejects empty entry', () => {
160
- expect(() => validateManifest({
161
- name: 'tool',
162
- version: '1.0.0',
163
- entry: ''
164
- })).toThrow();
165
- });
166
-
167
- test('rejects invalid runtime', () => {
168
- expect(() => validateManifest({
169
- name: 'tool',
170
- version: '1.0.0',
171
- entry: 'run.ts',
172
- runtime: 'python'
173
- })).toThrow();
174
- });
175
-
176
- test('rejects commands without description', () => {
177
- expect(() => validateManifest({
178
- name: 'tool',
179
- version: '1.0.0',
180
- entry: 'run.ts',
181
- commands: {
182
- mycommand: {}
183
- }
184
- })).toThrow();
185
- });
186
- });
187
-
188
- describe('loadManifestOrThrow', () => {
189
- test('loads valid manifest from directory', () => {
190
- writeFileSync(
191
- join(tempDir, MANIFEST_FILENAME),
192
- JSON.stringify({
193
- name: 'test-tool',
194
- version: '1.0.0',
195
- entry: 'run.ts'
196
- })
197
- );
198
-
199
- const manifest = loadManifestOrThrow(tempDir);
200
- expect(manifest.name).toBe('test-tool');
201
- });
202
-
203
- test('throws ManifestNotFoundError when manifest not found', () => {
204
- expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestNotFoundError);
205
- });
206
-
207
- test('throws ManifestParseError on invalid JSON', () => {
208
- writeFileSync(join(tempDir, MANIFEST_FILENAME), 'not json');
209
- expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestParseError);
210
- });
211
-
212
- test('throws ManifestValidationError on invalid manifest content', () => {
213
- writeFileSync(
214
- join(tempDir, MANIFEST_FILENAME),
215
- JSON.stringify({ name: 'Invalid' }) // Missing version, entry
216
- );
217
- expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestValidationError);
218
- });
219
- });
220
-
221
- describe('tryLoadManifest', () => {
222
- test('returns manifest when valid', () => {
223
- writeFileSync(
224
- join(tempDir, MANIFEST_FILENAME),
225
- JSON.stringify({
226
- name: 'test-tool',
227
- version: '1.0.0',
228
- entry: 'run.ts'
229
- })
230
- );
231
-
232
- const manifest = tryLoadManifest(tempDir);
233
- expect(manifest).not.toBeNull();
234
- expect(manifest?.name).toBe('test-tool');
235
- });
236
-
237
- test('returns null when not found', () => {
238
- const manifest = tryLoadManifest(tempDir);
239
- expect(manifest).toBeNull();
240
- });
241
-
242
- test('returns null on invalid JSON', () => {
243
- writeFileSync(join(tempDir, MANIFEST_FILENAME), 'not json');
244
- const manifest = tryLoadManifest(tempDir);
245
- expect(manifest).toBeNull();
246
- });
247
-
248
- test('returns null on invalid manifest', () => {
249
- writeFileSync(
250
- join(tempDir, MANIFEST_FILENAME),
251
- JSON.stringify({ name: 'Invalid' })
252
- );
253
- const manifest = tryLoadManifest(tempDir);
254
- expect(manifest).toBeNull();
255
- });
256
- });
257
-
258
- describe('findManifest', () => {
259
- test('finds manifest in current directory', () => {
260
- writeFileSync(
261
- join(tempDir, MANIFEST_FILENAME),
262
- JSON.stringify({
263
- name: 'found-tool',
264
- version: '1.0.0',
265
- entry: 'run.ts'
266
- })
267
- );
268
-
269
- const result = findManifest(tempDir);
270
- expect(result).not.toBeNull();
271
- expect(result?.manifest.name).toBe('found-tool');
272
- expect(result?.dir).toBe(tempDir);
273
- });
274
-
275
- test('finds manifest in parent directory', () => {
276
- const childDir = join(tempDir, 'child');
277
- mkdirSync(childDir);
278
-
279
- writeFileSync(
280
- join(tempDir, MANIFEST_FILENAME),
281
- JSON.stringify({
282
- name: 'parent-tool',
283
- version: '1.0.0',
284
- entry: 'run.ts'
285
- })
286
- );
287
-
288
- const result = findManifest(childDir);
289
- expect(result).not.toBeNull();
290
- expect(result?.manifest.name).toBe('parent-tool');
291
- expect(result?.dir).toBe(tempDir);
292
- });
293
-
294
- test('returns null when no manifest found', () => {
295
- const result = findManifest(tempDir);
296
- expect(result).toBeNull();
297
- });
298
- });
299
-
300
- describe('createManifest', () => {
301
- test('creates minimal manifest', () => {
302
- const manifest = createManifest('my-tool');
303
-
304
- expect(manifest.name).toBe('my-tool');
305
- expect(manifest.version).toBe('1.0.0');
306
- expect(manifest.entry).toBe('run.ts');
307
- expect(manifest.runtime).toBe('node');
308
- });
309
-
310
- test('normalizes name', () => {
311
- const manifest = createManifest('My Tool Name');
312
- expect(manifest.name).toBe('my-tool-name');
313
- });
314
-
315
- test('accepts options', () => {
316
- const manifest = createManifest('tool', {
317
- version: '2.0.0',
318
- description: 'Custom description',
319
- runtime: 'node'
320
- });
321
-
322
- expect(manifest.version).toBe('2.0.0');
323
- expect(manifest.description).toBe('Custom description');
324
- expect(manifest.runtime).toBe('node');
325
- });
326
- });
327
- });
@@ -1,139 +0,0 @@
1
- /**
2
- * Tests for routine-engine.ts
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
- import { mkdtempSync, rmSync, writeFileSync } from 'fs';
7
- import { join } from 'path';
8
- import { tmpdir } from 'os';
9
- import {
10
- dryRunRoutine,
11
- loadRoutineDefinition,
12
- runRoutine,
13
- RoutineParseError,
14
- RoutineTemplateError,
15
- RoutineValidationError,
16
- type RoutineDefinition
17
- } from './routine-engine.js';
18
-
19
- describe('routine-engine', () => {
20
- let tempDir: string;
21
-
22
- beforeEach(() => {
23
- tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-routine-engine-test-'));
24
- });
25
-
26
- afterEach(() => {
27
- rmSync(tempDir, { recursive: true, force: true });
28
- });
29
-
30
- test('loadRoutineDefinition throws RoutineParseError on invalid JSON', () => {
31
- const path = join(tempDir, 'bad.routine.json');
32
- writeFileSync(path, '{not json');
33
- expect(() => loadRoutineDefinition(path)).toThrow(RoutineParseError);
34
- });
35
-
36
- test('loadRoutineDefinition throws RoutineValidationError on duplicate step ids', () => {
37
- const path = join(tempDir, 'dup.routine.json');
38
- writeFileSync(
39
- path,
40
- JSON.stringify({
41
- version: 1,
42
- name: 'dup',
43
- steps: [
44
- { id: 'a', type: 'set', vars: {} },
45
- { id: 'a', type: 'set', vars: {} }
46
- ]
47
- })
48
- );
49
- expect(() => loadRoutineDefinition(path)).toThrow(RoutineValidationError);
50
- });
51
-
52
- test('runRoutine executes exec+set steps and renders object result templates', async () => {
53
- const def: RoutineDefinition = {
54
- version: 1,
55
- name: 'demo',
56
- vars: {
57
- lang: { default: 'rust' }
58
- },
59
- steps: [
60
- {
61
- id: 'hello',
62
- type: 'exec',
63
- cmd: 'bash',
64
- args: ['-lc', "echo '{\"x\":1,\"lang\":\"{{vars.lang}}\"}'"],
65
- capture: 'json'
66
- },
67
- {
68
- id: 'setvars',
69
- type: 'set',
70
- vars: {
71
- lang2: '{{steps.hello.json.lang}}',
72
- x: '{{steps.hello.json.x}}'
73
- }
74
- },
75
- {
76
- id: 'echo',
77
- type: 'exec',
78
- cmd: 'bash',
79
- args: ['-lc', "echo '{{vars.lang2}} {{vars.x}}'"],
80
- capture: 'text'
81
- }
82
- ],
83
- result: {
84
- lang: '{{vars.lang2}}',
85
- x: '{{vars.x}}',
86
- raw: '{{steps.echo.stdout}}'
87
- }
88
- };
89
-
90
- const summary = await runRoutine(def, { lang: 'typescript' }, tempDir);
91
- expect(summary.status).toBe('success');
92
- expect(summary.exitCode).toBe(0);
93
- expect(summary.result).toEqual({
94
- lang: 'typescript',
95
- x: 1,
96
- raw: 'typescript 1\n'
97
- });
98
- });
99
-
100
- test('runRoutine fails fast on missing template path', async () => {
101
- const def: RoutineDefinition = {
102
- version: 1,
103
- name: 'bad-template',
104
- steps: [
105
- {
106
- id: 'set',
107
- type: 'set',
108
- vars: {
109
- x: '{{steps.nope.json.0}}'
110
- }
111
- }
112
- ]
113
- };
114
-
115
- await expect(runRoutine(def, {}, tempDir)).rejects.toThrow(RoutineTemplateError);
116
- });
117
-
118
- test('dryRunRoutine is lenient for missing step paths', async () => {
119
- const def: RoutineDefinition = {
120
- version: 1,
121
- name: 'dry',
122
- steps: [
123
- {
124
- id: 'set',
125
- type: 'set',
126
- vars: {
127
- x: '{{steps.nope.json.0}}'
128
- }
129
- }
130
- ],
131
- result: { x: '{{vars.x}}' }
132
- };
133
-
134
- const plan = await dryRunRoutine(def, {}, tempDir);
135
- expect(plan.vars.x).toBe('{{steps.nope.json.0}}');
136
- expect(plan.result).toEqual({ x: '{{steps.nope.json.0}}' });
137
- });
138
- });
139
-