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,238 +0,0 @@
1
- /**
2
- * Tests for link.ts
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
- import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
7
- import { join, resolve } from 'path';
8
- import { tmpdir, homedir } from 'os';
9
- import {
10
- ensureBinDir,
11
- linkPackage,
12
- linkPackageDirect,
13
- unlinkPackage,
14
- isPackageLinked,
15
- getPathInstructions,
16
- isBinInPath,
17
- C4AI_BIN
18
- } from './link.js';
19
- import type { Manifest } from './manifest.js';
20
-
21
- describe('link', () => {
22
- let tempDir: string;
23
- let testBinDir: string;
24
-
25
- beforeEach(() => {
26
- tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-link-test-'));
27
- testBinDir = join(tempDir, 'bin');
28
- mkdirSync(testBinDir);
29
- });
30
-
31
- afterEach(() => {
32
- rmSync(tempDir, { recursive: true, force: true });
33
- });
34
-
35
- const createMockManifest = (name: string, version = '1.0.0'): Manifest => ({
36
- name,
37
- version,
38
- entry: 'run.ts',
39
- runtime: 'node'
40
- });
41
-
42
- describe('C4AI_BIN constant', () => {
43
- test('is in home directory', () => {
44
- expect(C4AI_BIN).toBe(resolve(homedir(), '.cli4ai', 'bin'));
45
- });
46
- });
47
-
48
- describe('ensureBinDir', () => {
49
- test('creates bin directory if not exists', () => {
50
- // This test affects real filesystem, so we just verify function doesn't throw
51
- expect(() => ensureBinDir()).not.toThrow();
52
- expect(existsSync(C4AI_BIN)).toBe(true);
53
- });
54
- });
55
-
56
- describe('linkPackage', () => {
57
- test('creates wrapper script', () => {
58
- const manifest = createMockManifest('test-tool');
59
- const packagePath = join(tempDir, 'test-tool');
60
-
61
- const binPath = linkPackage(manifest, packagePath);
62
-
63
- expect(existsSync(binPath)).toBe(true);
64
- const content = readFileSync(binPath, 'utf-8');
65
- expect(content).toContain('#!/bin/sh');
66
- // SECURITY: Package names are shell-escaped with single quotes
67
- expect(content).toContain("cli4ai run 'test-tool'");
68
- });
69
-
70
- test('makes script executable', () => {
71
- const manifest = createMockManifest('test-tool');
72
- const packagePath = join(tempDir, 'test-tool');
73
-
74
- const binPath = linkPackage(manifest, packagePath);
75
-
76
- const stat = statSync(binPath);
77
- // Check executable bit (0o755 = rwxr-xr-x)
78
- const isExecutable = (stat.mode & 0o111) !== 0;
79
- expect(isExecutable).toBe(true);
80
- });
81
-
82
- test('includes version in comment', () => {
83
- const manifest = createMockManifest('my-tool', '2.5.0');
84
- const packagePath = join(tempDir, 'my-tool');
85
-
86
- const binPath = linkPackage(manifest, packagePath);
87
-
88
- const content = readFileSync(binPath, 'utf-8');
89
- // SECURITY: Version is shell-escaped in comment
90
- expect(content).toContain("'my-tool'@'2.5.0'");
91
- });
92
- });
93
-
94
- describe('linkPackageDirect', () => {
95
- test('creates direct wrapper script', () => {
96
- const manifest = createMockManifest('direct-tool');
97
- const packagePath = join(tempDir, 'direct-tool');
98
-
99
- const binPath = linkPackageDirect(manifest, packagePath);
100
-
101
- expect(existsSync(binPath)).toBe(true);
102
- const content = readFileSync(binPath, 'utf-8');
103
- expect(content).toContain('#!/bin/sh');
104
- expect(content).toContain('npx tsx');
105
- expect(content).toContain('run.ts');
106
- });
107
-
108
- test('uses correct runtime from manifest (node)', () => {
109
- const manifest: Manifest = {
110
- name: 'node-tool',
111
- version: '1.0.0',
112
- entry: 'index.js',
113
- runtime: 'node'
114
- };
115
- const packagePath = join(tempDir, 'node-tool');
116
-
117
- const binPath = linkPackageDirect(manifest, packagePath);
118
-
119
- const content = readFileSync(binPath, 'utf-8');
120
- // Node.js doesn't use 'run' subcommand - it's just 'node <file>'
121
- // SECURITY: Paths are now shell-escaped with single quotes
122
- expect(content).toContain("exec node '");
123
- expect(content).not.toContain('node run');
124
- });
125
-
126
- test('defaults to node runtime with tsx for TypeScript', () => {
127
- const manifest: Manifest = {
128
- name: 'no-runtime',
129
- version: '1.0.0',
130
- entry: 'run.ts'
131
- // no runtime specified
132
- };
133
- const packagePath = join(tempDir, 'no-runtime');
134
-
135
- const binPath = linkPackageDirect(manifest, packagePath);
136
-
137
- const content = readFileSync(binPath, 'utf-8');
138
- expect(content).toContain('npx tsx');
139
- });
140
-
141
- test('includes full path to entry', () => {
142
- const manifest = createMockManifest('path-tool');
143
- const packagePath = join(tempDir, 'path-tool');
144
-
145
- const binPath = linkPackageDirect(manifest, packagePath);
146
-
147
- const content = readFileSync(binPath, 'utf-8');
148
- expect(content).toContain(resolve(packagePath, 'run.ts'));
149
- });
150
- });
151
-
152
- describe('unlinkPackage', () => {
153
- test('removes linked package', () => {
154
- const manifest = createMockManifest('unlink-test');
155
- const packagePath = join(tempDir, 'unlink-test');
156
-
157
- const binPath = linkPackage(manifest, packagePath);
158
- expect(existsSync(binPath)).toBe(true);
159
-
160
- const result = unlinkPackage('unlink-test');
161
-
162
- expect(result).toBe(true);
163
- expect(existsSync(binPath)).toBe(false);
164
- });
165
-
166
- test('returns false for non-existent package', () => {
167
- const result = unlinkPackage('nonexistent-package-name');
168
- expect(result).toBe(false);
169
- });
170
- });
171
-
172
- describe('isPackageLinked', () => {
173
- test('returns true for linked package', () => {
174
- const manifest = createMockManifest('linked-tool');
175
- const packagePath = join(tempDir, 'linked-tool');
176
-
177
- linkPackage(manifest, packagePath);
178
-
179
- expect(isPackageLinked('linked-tool')).toBe(true);
180
- });
181
-
182
- test('returns false for unlinked package', () => {
183
- expect(isPackageLinked('not-linked-tool')).toBe(false);
184
- });
185
- });
186
-
187
- describe('getPathInstructions', () => {
188
- test('returns instructions string', () => {
189
- const instructions = getPathInstructions();
190
-
191
- expect(instructions).toContain('export PATH=');
192
- expect(instructions).toContain(C4AI_BIN);
193
- expect(instructions).toContain('source');
194
- });
195
-
196
- test('detects zsh shell', () => {
197
- const originalShell = process.env.SHELL;
198
- process.env.SHELL = '/bin/zsh';
199
-
200
- const instructions = getPathInstructions();
201
-
202
- expect(instructions).toContain('.zshrc');
203
-
204
- process.env.SHELL = originalShell;
205
- });
206
-
207
- test('defaults to bash', () => {
208
- const originalShell = process.env.SHELL;
209
- process.env.SHELL = '/bin/bash';
210
-
211
- const instructions = getPathInstructions();
212
-
213
- expect(instructions).toContain('.bashrc');
214
-
215
- process.env.SHELL = originalShell;
216
- });
217
- });
218
-
219
- describe('isBinInPath', () => {
220
- test('returns true when bin in PATH', () => {
221
- const originalPath = process.env.PATH;
222
- process.env.PATH = `${C4AI_BIN}:${originalPath}`;
223
-
224
- expect(isBinInPath()).toBe(true);
225
-
226
- process.env.PATH = originalPath;
227
- });
228
-
229
- test('returns false when bin not in PATH', () => {
230
- const originalPath = process.env.PATH;
231
- process.env.PATH = '/usr/bin:/usr/local/bin';
232
-
233
- expect(isBinInPath()).toBe(false);
234
-
235
- process.env.PATH = originalPath;
236
- });
237
- });
238
- });
@@ -1,337 +0,0 @@
1
- /**
2
- * Tests for lockfile.ts
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
- import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs';
7
- import { join } from 'path';
8
- import { tmpdir } from 'os';
9
- import {
10
- getLockfilePath,
11
- lockfileExists,
12
- loadLockfile,
13
- saveLockfile,
14
- lockPackage,
15
- unlockPackage,
16
- getLockedPackage,
17
- getLockedPackages,
18
- isPackageLocked,
19
- clearLockfile,
20
- formatLockfile,
21
- LOCKFILE_NAME,
22
- LOCKFILE_VERSION,
23
- type Lockfile,
24
- type LockedPackage
25
- } from './lockfile.js';
26
-
27
- describe('lockfile', () => {
28
- let tempDir: string;
29
-
30
- beforeEach(() => {
31
- tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-lockfile-test-'));
32
- });
33
-
34
- afterEach(() => {
35
- rmSync(tempDir, { recursive: true, force: true });
36
- });
37
-
38
- describe('getLockfilePath', () => {
39
- test('returns correct path', () => {
40
- const path = getLockfilePath(tempDir);
41
- expect(path).toBe(join(tempDir, LOCKFILE_NAME));
42
- });
43
- });
44
-
45
- describe('lockfileExists', () => {
46
- test('returns false when no lockfile', () => {
47
- expect(lockfileExists(tempDir)).toBe(false);
48
- });
49
-
50
- test('returns true when lockfile exists', () => {
51
- writeFileSync(join(tempDir, LOCKFILE_NAME), '{}');
52
- expect(lockfileExists(tempDir)).toBe(true);
53
- });
54
- });
55
-
56
- describe('loadLockfile', () => {
57
- test('returns empty lockfile when none exists', () => {
58
- const lockfile = loadLockfile(tempDir);
59
-
60
- expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
61
- expect(lockfile.packages).toEqual({});
62
- });
63
-
64
- test('loads existing lockfile', () => {
65
- const existing: Lockfile = {
66
- lockfileVersion: 1,
67
- packages: {
68
- github: {
69
- name: 'github',
70
- version: '1.0.0',
71
- resolved: '/path/to/github'
72
- }
73
- }
74
- };
75
-
76
- writeFileSync(
77
- join(tempDir, LOCKFILE_NAME),
78
- JSON.stringify(existing)
79
- );
80
-
81
- const lockfile = loadLockfile(tempDir);
82
- expect(lockfile.packages.github.name).toBe('github');
83
- expect(lockfile.packages.github.version).toBe('1.0.0');
84
- });
85
-
86
- test('handles invalid JSON gracefully', () => {
87
- writeFileSync(join(tempDir, LOCKFILE_NAME), 'invalid json');
88
-
89
- const lockfile = loadLockfile(tempDir);
90
- expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
91
- expect(lockfile.packages).toEqual({});
92
- });
93
- });
94
-
95
- describe('saveLockfile', () => {
96
- test('saves lockfile to disk', () => {
97
- const lockfile: Lockfile = {
98
- lockfileVersion: 1,
99
- packages: {
100
- tool: {
101
- name: 'tool',
102
- version: '2.0.0',
103
- resolved: '/some/path'
104
- }
105
- }
106
- };
107
-
108
- saveLockfile(tempDir, lockfile);
109
-
110
- const content = readFileSync(join(tempDir, LOCKFILE_NAME), 'utf-8');
111
- const parsed = JSON.parse(content);
112
-
113
- expect(parsed.lockfileVersion).toBe(1);
114
- expect(parsed.packages.tool.version).toBe('2.0.0');
115
- });
116
-
117
- test('formats JSON with indentation', () => {
118
- const lockfile: Lockfile = {
119
- lockfileVersion: 1,
120
- packages: {}
121
- };
122
-
123
- saveLockfile(tempDir, lockfile);
124
-
125
- const content = readFileSync(join(tempDir, LOCKFILE_NAME), 'utf-8');
126
- expect(content).toContain(' '); // Has indentation
127
- expect(content.endsWith('\n')).toBe(true); // Trailing newline
128
- });
129
- });
130
-
131
- describe('lockPackage', () => {
132
- test('adds new package to lockfile', () => {
133
- const pkg: LockedPackage = {
134
- name: 'new-tool',
135
- version: '1.0.0',
136
- resolved: '/path/to/tool'
137
- };
138
-
139
- lockPackage(tempDir, pkg);
140
-
141
- const lockfile = loadLockfile(tempDir);
142
- expect(lockfile.packages['new-tool']).toEqual(pkg);
143
- });
144
-
145
- test('updates existing package', () => {
146
- lockPackage(tempDir, {
147
- name: 'tool',
148
- version: '1.0.0',
149
- resolved: '/old/path'
150
- });
151
-
152
- lockPackage(tempDir, {
153
- name: 'tool',
154
- version: '2.0.0',
155
- resolved: '/new/path'
156
- });
157
-
158
- const lockfile = loadLockfile(tempDir);
159
- expect(lockfile.packages.tool.version).toBe('2.0.0');
160
- expect(lockfile.packages.tool.resolved).toBe('/new/path');
161
- });
162
-
163
- test('preserves other packages when adding', () => {
164
- lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
165
- lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
166
- lockPackage(tempDir, { name: 'c', version: '1.0.0', resolved: '/c' });
167
-
168
- const lockfile = loadLockfile(tempDir);
169
- expect(Object.keys(lockfile.packages)).toEqual(['a', 'b', 'c']);
170
- });
171
- });
172
-
173
- describe('unlockPackage', () => {
174
- test('removes package from lockfile', () => {
175
- lockPackage(tempDir, { name: 'tool', version: '1.0.0', resolved: '/path' });
176
- unlockPackage(tempDir, 'tool');
177
-
178
- const lockfile = loadLockfile(tempDir);
179
- expect(lockfile.packages.tool).toBeUndefined();
180
- });
181
-
182
- test('handles non-existent package gracefully', () => {
183
- // Should not throw
184
- unlockPackage(tempDir, 'nonexistent');
185
-
186
- const lockfile = loadLockfile(tempDir);
187
- expect(lockfile.packages.nonexistent).toBeUndefined();
188
- });
189
-
190
- test('preserves other packages when removing', () => {
191
- lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
192
- lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
193
- lockPackage(tempDir, { name: 'c', version: '1.0.0', resolved: '/c' });
194
-
195
- unlockPackage(tempDir, 'b');
196
-
197
- const lockfile = loadLockfile(tempDir);
198
- expect(Object.keys(lockfile.packages)).toEqual(['a', 'c']);
199
- });
200
- });
201
-
202
- describe('getLockedPackage', () => {
203
- test('returns package when found', () => {
204
- lockPackage(tempDir, {
205
- name: 'github',
206
- version: '1.0.0',
207
- resolved: '/path'
208
- });
209
-
210
- const pkg = getLockedPackage(tempDir, 'github');
211
- expect(pkg).not.toBeNull();
212
- expect(pkg?.name).toBe('github');
213
- });
214
-
215
- test('returns null when not found', () => {
216
- const pkg = getLockedPackage(tempDir, 'nonexistent');
217
- expect(pkg).toBeNull();
218
- });
219
- });
220
-
221
- describe('getLockedPackages', () => {
222
- test('returns empty array when no packages', () => {
223
- const packages = getLockedPackages(tempDir);
224
- expect(packages).toEqual([]);
225
- });
226
-
227
- test('returns all packages', () => {
228
- lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
229
- lockPackage(tempDir, { name: 'b', version: '2.0.0', resolved: '/b' });
230
-
231
- const packages = getLockedPackages(tempDir);
232
- expect(packages).toHaveLength(2);
233
- expect(packages.map(p => p.name).sort()).toEqual(['a', 'b']);
234
- });
235
- });
236
-
237
- describe('isPackageLocked', () => {
238
- test('returns true when package is locked', () => {
239
- lockPackage(tempDir, { name: 'tool', version: '1.0.0', resolved: '/path' });
240
- expect(isPackageLocked(tempDir, 'tool')).toBe(true);
241
- });
242
-
243
- test('returns false when package not locked', () => {
244
- expect(isPackageLocked(tempDir, 'nonexistent')).toBe(false);
245
- });
246
- });
247
-
248
- describe('clearLockfile', () => {
249
- test('removes all packages', () => {
250
- lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
251
- lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
252
-
253
- clearLockfile(tempDir);
254
-
255
- const lockfile = loadLockfile(tempDir);
256
- expect(lockfile.packages).toEqual({});
257
- });
258
-
259
- test('preserves lockfile version', () => {
260
- clearLockfile(tempDir);
261
-
262
- const lockfile = loadLockfile(tempDir);
263
- expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
264
- });
265
- });
266
-
267
- describe('formatLockfile', () => {
268
- test('formats empty lockfile', () => {
269
- const lockfile: Lockfile = {
270
- lockfileVersion: 1,
271
- packages: {}
272
- };
273
-
274
- const output = formatLockfile(lockfile);
275
- expect(output).toContain('# cli4ai.lock');
276
- expect(output).toContain('lockfileVersion: 1');
277
- });
278
-
279
- test('formats packages alphabetically', () => {
280
- const lockfile: Lockfile = {
281
- lockfileVersion: 1,
282
- packages: {
283
- zebra: { name: 'zebra', version: '1.0.0', resolved: '/z' },
284
- apple: { name: 'apple', version: '2.0.0', resolved: '/a' },
285
- monkey: { name: 'monkey', version: '3.0.0', resolved: '/m' }
286
- }
287
- };
288
-
289
- const output = formatLockfile(lockfile);
290
- const appleIndex = output.indexOf('apple@');
291
- const monkeyIndex = output.indexOf('monkey@');
292
- const zebraIndex = output.indexOf('zebra@');
293
-
294
- expect(appleIndex).toBeLessThan(monkeyIndex);
295
- expect(monkeyIndex).toBeLessThan(zebraIndex);
296
- });
297
-
298
- test('includes integrity when present', () => {
299
- const lockfile: Lockfile = {
300
- lockfileVersion: 1,
301
- packages: {
302
- tool: {
303
- name: 'tool',
304
- version: '1.0.0',
305
- resolved: '/path',
306
- integrity: 'sha256-abc123'
307
- }
308
- }
309
- };
310
-
311
- const output = formatLockfile(lockfile);
312
- expect(output).toContain('integrity: sha256-abc123');
313
- });
314
-
315
- test('includes dependencies when present', () => {
316
- const lockfile: Lockfile = {
317
- lockfileVersion: 1,
318
- packages: {
319
- tool: {
320
- name: 'tool',
321
- version: '1.0.0',
322
- resolved: '/path',
323
- dependencies: {
324
- 'dep-a': '1.0.0',
325
- 'dep-b': '2.0.0'
326
- }
327
- }
328
- }
329
- };
330
-
331
- const output = formatLockfile(lockfile);
332
- expect(output).toContain('dependencies:');
333
- expect(output).toContain('dep-a: "1.0.0"');
334
- expect(output).toContain('dep-b: "2.0.0"');
335
- });
336
- });
337
- });