cli4ai 0.9.1 → 1.0.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/LICENSE +109 -0
- package/README.md +95 -177
- package/package.json +2 -2
- package/src/commands/init.ts +1 -1
- package/src/commands/update.ts +23 -4
- package/src/lib/cli.ts +1 -1
- package/src/commands/init.test.ts +0 -211
- package/src/core/config.test.ts +0 -188
- package/src/core/link.test.ts +0 -238
- package/src/core/lockfile.test.ts +0 -337
- package/src/core/manifest.test.ts +0 -327
- package/src/core/routine-engine.test.ts +0 -139
- package/src/core/scheduler.test.ts +0 -291
- package/src/core/secrets.test.ts +0 -79
- package/src/mcp/adapter.test.ts +0 -132
- package/src/mcp/config-gen.test.ts +0 -214
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for init command
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import { initCommand } from './init.js';
|
|
10
|
-
|
|
11
|
-
describe('init command', () => {
|
|
12
|
-
let tempDir: string;
|
|
13
|
-
let originalCwd: string;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-init-test-'));
|
|
17
|
-
originalCwd = process.cwd();
|
|
18
|
-
process.chdir(tempDir);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
process.chdir(originalCwd);
|
|
23
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('with name argument (creates subdirectory)', () => {
|
|
27
|
-
test('creates cli4ai.json in subdirectory', async () => {
|
|
28
|
-
await initCommand('test-tool', {});
|
|
29
|
-
|
|
30
|
-
expect(existsSync(join(tempDir, 'test-tool', 'cli4ai.json'))).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('creates run.ts entry file in subdirectory', async () => {
|
|
34
|
-
await initCommand('test-tool', {});
|
|
35
|
-
|
|
36
|
-
expect(existsSync(join(tempDir, 'test-tool', 'run.ts'))).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('creates README.md and package.json', async () => {
|
|
40
|
-
await initCommand('test-tool', {});
|
|
41
|
-
|
|
42
|
-
expect(existsSync(join(tempDir, 'test-tool', 'README.md'))).toBe(true);
|
|
43
|
-
expect(existsSync(join(tempDir, 'test-tool', 'package.json'))).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('creates the subdirectory', async () => {
|
|
47
|
-
await initCommand('new-project', {});
|
|
48
|
-
|
|
49
|
-
expect(existsSync(join(tempDir, 'new-project'))).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('without name argument (uses current directory)', () => {
|
|
54
|
-
test('creates cli4ai.json in current directory', async () => {
|
|
55
|
-
await initCommand(undefined, {});
|
|
56
|
-
|
|
57
|
-
expect(existsSync(join(tempDir, 'cli4ai.json'))).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('creates run.ts entry file', async () => {
|
|
61
|
-
await initCommand(undefined, {});
|
|
62
|
-
|
|
63
|
-
expect(existsSync(join(tempDir, 'run.ts'))).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('uses directory name as package name', async () => {
|
|
67
|
-
await initCommand(undefined, {});
|
|
68
|
-
|
|
69
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'cli4ai.json'), 'utf-8'));
|
|
70
|
-
// Name is normalized from the temp dir name
|
|
71
|
-
expect(manifest.name).toBeDefined();
|
|
72
|
-
expect(typeof manifest.name).toBe('string');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('manifest content', () => {
|
|
77
|
-
test('manifest has correct name', async () => {
|
|
78
|
-
await initCommand('my-cool-tool', {});
|
|
79
|
-
|
|
80
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'my-cool-tool', 'cli4ai.json'), 'utf-8'));
|
|
81
|
-
expect(manifest.name).toBe('my-cool-tool');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('manifest has default version', async () => {
|
|
85
|
-
await initCommand('tool', {});
|
|
86
|
-
|
|
87
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
88
|
-
expect(manifest.version).toBe('1.0.0');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('manifest has default runtime node', async () => {
|
|
92
|
-
await initCommand('tool', {});
|
|
93
|
-
|
|
94
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
95
|
-
expect(manifest.runtime).toBe('node');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('manifest has entry pointing to run.ts', async () => {
|
|
99
|
-
await initCommand('tool', {});
|
|
100
|
-
|
|
101
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
102
|
-
expect(manifest.entry).toBe('run.ts');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('manifest has hello command defined', async () => {
|
|
106
|
-
await initCommand('tool', {});
|
|
107
|
-
|
|
108
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
109
|
-
expect(manifest.commands?.hello).toBeDefined();
|
|
110
|
-
expect(manifest.commands?.hello?.description).toBe('Say hello');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('manifest includes dependencies', async () => {
|
|
114
|
-
await initCommand('tool', {});
|
|
115
|
-
|
|
116
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
117
|
-
expect(manifest.dependencies).toBeDefined();
|
|
118
|
-
expect(manifest.dependencies['@cli4ai/lib']).toBeDefined();
|
|
119
|
-
expect(manifest.dependencies['commander']).toBeDefined();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('entry file content', () => {
|
|
124
|
-
test('run.ts starts with tsx shebang', async () => {
|
|
125
|
-
await initCommand('tool', {});
|
|
126
|
-
|
|
127
|
-
const content = readFileSync(join(tempDir, 'tool', 'run.ts'), 'utf-8');
|
|
128
|
-
expect(content.startsWith('#!/usr/bin/env npx tsx')).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('run.ts has hello command', async () => {
|
|
132
|
-
await initCommand('tool', {});
|
|
133
|
-
|
|
134
|
-
const content = readFileSync(join(tempDir, 'tool', 'run.ts'), 'utf-8');
|
|
135
|
-
expect(content).toContain('hello');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('run.ts uses cli4ai SDK helpers', async () => {
|
|
139
|
-
await initCommand('tool', {});
|
|
140
|
-
|
|
141
|
-
const content = readFileSync(join(tempDir, 'tool', 'run.ts'), 'utf-8');
|
|
142
|
-
expect(content).toContain("@cli4ai/lib");
|
|
143
|
-
expect(content).toContain('output(');
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('runtime option', () => {
|
|
148
|
-
test('respects runtime option', async () => {
|
|
149
|
-
await initCommand('tool', { runtime: 'bun' });
|
|
150
|
-
|
|
151
|
-
const manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
152
|
-
expect(manifest.runtime).toBe('bun');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('both runtimes use run.ts with tsx', async () => {
|
|
156
|
-
// Node runtime (default)
|
|
157
|
-
await initCommand('tool', { runtime: 'node' });
|
|
158
|
-
let manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
159
|
-
expect(manifest.entry).toBe('run.ts');
|
|
160
|
-
expect(existsSync(join(tempDir, 'tool', 'run.ts'))).toBe(true);
|
|
161
|
-
|
|
162
|
-
// Bun runtime
|
|
163
|
-
rmSync(join(tempDir, 'tool'), { recursive: true, force: true });
|
|
164
|
-
await initCommand('tool', { runtime: 'bun' });
|
|
165
|
-
manifest = JSON.parse(readFileSync(join(tempDir, 'tool', 'cli4ai.json'), 'utf-8'));
|
|
166
|
-
expect(manifest.entry).toBe('run.ts');
|
|
167
|
-
expect(existsSync(join(tempDir, 'tool', 'run.ts'))).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('test scaffolding', () => {
|
|
172
|
-
test('creates vitest.config.ts', async () => {
|
|
173
|
-
await initCommand('tool', {});
|
|
174
|
-
expect(existsSync(join(tempDir, 'tool', 'vitest.config.ts'))).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('creates run.test.ts with vitest imports', async () => {
|
|
178
|
-
await initCommand('tool', {});
|
|
179
|
-
const content = readFileSync(join(tempDir, 'tool', 'run.test.ts'), 'utf-8');
|
|
180
|
-
expect(content).toContain("from 'vitest'");
|
|
181
|
-
expect(content).toContain('describe(');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('package.json has vitest dev dependency', async () => {
|
|
185
|
-
await initCommand('tool', {});
|
|
186
|
-
const pkg = JSON.parse(readFileSync(join(tempDir, 'tool', 'package.json'), 'utf-8'));
|
|
187
|
-
expect(pkg.devDependencies).toBeDefined();
|
|
188
|
-
expect(pkg.devDependencies['vitest']).toBeDefined();
|
|
189
|
-
expect(pkg.devDependencies['tsx']).toBeDefined();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('package.json has test script', async () => {
|
|
193
|
-
await initCommand('tool', {});
|
|
194
|
-
const pkg = JSON.parse(readFileSync(join(tempDir, 'tool', 'package.json'), 'utf-8'));
|
|
195
|
-
expect(pkg.scripts?.test).toBe('vitest run');
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe('tsconfig.json', () => {
|
|
200
|
-
test('creates tsconfig.json', async () => {
|
|
201
|
-
await initCommand('tool', {});
|
|
202
|
-
expect(existsSync(join(tempDir, 'tool', 'tsconfig.json'))).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('tsconfig uses NodeNext module resolution', async () => {
|
|
206
|
-
await initCommand('tool', {});
|
|
207
|
-
const tsconfig = JSON.parse(readFileSync(join(tempDir, 'tool', 'tsconfig.json'), 'utf-8'));
|
|
208
|
-
expect(tsconfig.compilerOptions.moduleResolution).toBe('NodeNext');
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
});
|
package/src/core/config.test.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for config.ts
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import {
|
|
10
|
-
ensureLocalDir,
|
|
11
|
-
getGlobalPackages,
|
|
12
|
-
getLocalPackages,
|
|
13
|
-
findPackage,
|
|
14
|
-
LOCAL_DIR,
|
|
15
|
-
LOCAL_PACKAGES_DIR,
|
|
16
|
-
DEFAULT_CONFIG
|
|
17
|
-
} from './config.js';
|
|
18
|
-
|
|
19
|
-
describe('config', () => {
|
|
20
|
-
let tempDir: string;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-config-test-'));
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('DEFAULT_CONFIG', () => {
|
|
31
|
-
test('has expected defaults', () => {
|
|
32
|
-
expect(DEFAULT_CONFIG.registry).toBe('https://registry.cli4ai.com');
|
|
33
|
-
expect(DEFAULT_CONFIG.localRegistries).toEqual([]);
|
|
34
|
-
expect(DEFAULT_CONFIG.defaultRuntime).toBe('node');
|
|
35
|
-
expect(DEFAULT_CONFIG.mcp.transport).toBe('stdio');
|
|
36
|
-
expect(DEFAULT_CONFIG.telemetry).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('ensureLocalDir', () => {
|
|
41
|
-
test('creates .cli4ai directory', () => {
|
|
42
|
-
ensureLocalDir(tempDir);
|
|
43
|
-
|
|
44
|
-
expect(existsSync(join(tempDir, LOCAL_DIR))).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('creates packages subdirectory', () => {
|
|
48
|
-
ensureLocalDir(tempDir);
|
|
49
|
-
|
|
50
|
-
expect(existsSync(join(tempDir, LOCAL_PACKAGES_DIR))).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('is idempotent', () => {
|
|
54
|
-
ensureLocalDir(tempDir);
|
|
55
|
-
ensureLocalDir(tempDir);
|
|
56
|
-
|
|
57
|
-
expect(existsSync(join(tempDir, LOCAL_PACKAGES_DIR))).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('getLocalPackages', () => {
|
|
62
|
-
test('returns empty array when no packages dir', () => {
|
|
63
|
-
const packages = getLocalPackages(tempDir);
|
|
64
|
-
expect(packages).toEqual([]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('returns empty array when packages dir empty', () => {
|
|
68
|
-
ensureLocalDir(tempDir);
|
|
69
|
-
|
|
70
|
-
const packages = getLocalPackages(tempDir);
|
|
71
|
-
expect(packages).toEqual([]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('returns installed packages', () => {
|
|
75
|
-
ensureLocalDir(tempDir);
|
|
76
|
-
|
|
77
|
-
// Create a mock package
|
|
78
|
-
const pkgDir = join(tempDir, LOCAL_PACKAGES_DIR, 'test-tool');
|
|
79
|
-
mkdirSync(pkgDir);
|
|
80
|
-
writeFileSync(
|
|
81
|
-
join(pkgDir, 'cli4ai.json'),
|
|
82
|
-
JSON.stringify({
|
|
83
|
-
name: 'test-tool',
|
|
84
|
-
version: '1.0.0',
|
|
85
|
-
entry: 'run.ts'
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const packages = getLocalPackages(tempDir);
|
|
90
|
-
|
|
91
|
-
expect(packages).toHaveLength(1);
|
|
92
|
-
expect(packages[0].name).toBe('test-tool');
|
|
93
|
-
expect(packages[0].version).toBe('1.0.0');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('ignores directories without manifest', () => {
|
|
97
|
-
ensureLocalDir(tempDir);
|
|
98
|
-
|
|
99
|
-
// Create a directory without cli4ai.json
|
|
100
|
-
const invalidDir = join(tempDir, LOCAL_PACKAGES_DIR, 'invalid');
|
|
101
|
-
mkdirSync(invalidDir);
|
|
102
|
-
writeFileSync(join(invalidDir, 'readme.txt'), 'not a package');
|
|
103
|
-
|
|
104
|
-
const packages = getLocalPackages(tempDir);
|
|
105
|
-
expect(packages).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('ignores files in packages dir', () => {
|
|
109
|
-
ensureLocalDir(tempDir);
|
|
110
|
-
|
|
111
|
-
// Create a file instead of directory
|
|
112
|
-
writeFileSync(
|
|
113
|
-
join(tempDir, LOCAL_PACKAGES_DIR, 'somefile.txt'),
|
|
114
|
-
'not a package'
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const packages = getLocalPackages(tempDir);
|
|
118
|
-
expect(packages).toEqual([]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('handles invalid JSON gracefully', () => {
|
|
122
|
-
ensureLocalDir(tempDir);
|
|
123
|
-
|
|
124
|
-
const pkgDir = join(tempDir, LOCAL_PACKAGES_DIR, 'bad-json');
|
|
125
|
-
mkdirSync(pkgDir);
|
|
126
|
-
writeFileSync(join(pkgDir, 'cli4ai.json'), 'invalid json');
|
|
127
|
-
|
|
128
|
-
const packages = getLocalPackages(tempDir);
|
|
129
|
-
expect(packages).toEqual([]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('returns multiple packages', () => {
|
|
133
|
-
ensureLocalDir(tempDir);
|
|
134
|
-
|
|
135
|
-
// Create multiple packages
|
|
136
|
-
for (const name of ['pkg-a', 'pkg-b', 'pkg-c']) {
|
|
137
|
-
const pkgDir = join(tempDir, LOCAL_PACKAGES_DIR, name);
|
|
138
|
-
mkdirSync(pkgDir);
|
|
139
|
-
writeFileSync(
|
|
140
|
-
join(pkgDir, 'cli4ai.json'),
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
name,
|
|
143
|
-
version: '1.0.0',
|
|
144
|
-
entry: 'run.ts'
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const packages = getLocalPackages(tempDir);
|
|
150
|
-
expect(packages).toHaveLength(3);
|
|
151
|
-
expect(packages.map(p => p.name).sort()).toEqual(['pkg-a', 'pkg-b', 'pkg-c']);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('findPackage', () => {
|
|
156
|
-
test('finds local package', () => {
|
|
157
|
-
ensureLocalDir(tempDir);
|
|
158
|
-
|
|
159
|
-
const pkgDir = join(tempDir, LOCAL_PACKAGES_DIR, 'local-tool');
|
|
160
|
-
mkdirSync(pkgDir);
|
|
161
|
-
writeFileSync(
|
|
162
|
-
join(pkgDir, 'cli4ai.json'),
|
|
163
|
-
JSON.stringify({
|
|
164
|
-
name: 'local-tool',
|
|
165
|
-
version: '1.0.0',
|
|
166
|
-
entry: 'run.ts'
|
|
167
|
-
})
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const pkg = findPackage('local-tool', tempDir);
|
|
171
|
-
|
|
172
|
-
expect(pkg).not.toBeNull();
|
|
173
|
-
expect(pkg?.name).toBe('local-tool');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('returns null when package not found', () => {
|
|
177
|
-
const pkg = findPackage('nonexistent', tempDir);
|
|
178
|
-
expect(pkg).toBeNull();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('searches without project dir', () => {
|
|
182
|
-
// Should not throw, just search global
|
|
183
|
-
const pkg = findPackage('test');
|
|
184
|
-
// May or may not find depending on global state
|
|
185
|
-
expect(pkg === null || typeof pkg === 'object').toBe(true);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
});
|
package/src/core/link.test.ts
DELETED
|
@@ -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
|
-
});
|