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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Package Registry
3
+ *
4
+ * Fetches and caches package metadata from cli4ai.com registry.
5
+ * Provides integrity hashes for verification.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+ import { readdirSync, readFileSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ const REGISTRY_URL = 'https://cli4ai.com/registry/packages.json';
13
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
14
+
15
+ interface RegistryPackage {
16
+ name: string;
17
+ version: string;
18
+ description: string;
19
+ integrity: string;
20
+ readme: string;
21
+ commands: Record<string, { description: string; args?: Array<{ name: string; required: boolean }> }>;
22
+ dependencies: Record<string, string>;
23
+ env: Record<string, { required: boolean; description: string }>;
24
+ category: string;
25
+ keywords: string[];
26
+ author: string;
27
+ license: string;
28
+ runtime: string;
29
+ mcp: boolean;
30
+ repository: string;
31
+ homepage: string;
32
+ npm: string;
33
+ updatedAt: string;
34
+ }
35
+
36
+ interface Registry {
37
+ version: number;
38
+ generatedAt: string;
39
+ packages: RegistryPackage[];
40
+ }
41
+
42
+ // In-memory cache
43
+ let cachedRegistry: Registry | null = null;
44
+ let cacheTimestamp = 0;
45
+
46
+ /**
47
+ * Fetch registry from cli4ai.com
48
+ */
49
+ export async function fetchRegistry(): Promise<Registry | null> {
50
+ // Return cached if fresh
51
+ if (cachedRegistry && Date.now() - cacheTimestamp < CACHE_TTL) {
52
+ return cachedRegistry;
53
+ }
54
+
55
+ try {
56
+ const response = await fetch(REGISTRY_URL, {
57
+ headers: { 'Accept': 'application/json' },
58
+ signal: AbortSignal.timeout(10000), // 10 second timeout
59
+ });
60
+
61
+ if (!response.ok) {
62
+ return null;
63
+ }
64
+
65
+ const registry = await response.json() as Registry;
66
+ cachedRegistry = registry;
67
+ cacheTimestamp = Date.now();
68
+ return registry;
69
+ } catch {
70
+ // Return cached even if stale, or null if no cache
71
+ return cachedRegistry;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get package info from registry
77
+ */
78
+ export async function getRegistryPackage(name: string): Promise<RegistryPackage | null> {
79
+ const registry = await fetchRegistry();
80
+ if (!registry) return null;
81
+
82
+ return registry.packages.find(p => p.name === name) || null;
83
+ }
84
+
85
+ /**
86
+ * Get integrity hash for a package from registry
87
+ */
88
+ export async function getRegistryIntegrity(name: string): Promise<string | null> {
89
+ const pkg = await getRegistryPackage(name);
90
+ return pkg?.integrity || null;
91
+ }
92
+
93
+ /**
94
+ * Get all package names from registry
95
+ */
96
+ export async function getRegistryPackageNames(): Promise<string[]> {
97
+ const registry = await fetchRegistry();
98
+ if (!registry) return [];
99
+ return registry.packages.map(p => p.name);
100
+ }
101
+
102
+ /**
103
+ * Compute SHA-512 integrity hash for a directory
104
+ * Same algorithm as used by generate-registry.ts
105
+ */
106
+ export function computeDirectoryIntegrity(dirPath: string): string {
107
+ const hash = createHash('sha512');
108
+
109
+ function processDir(dir: string) {
110
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
111
+ a.name.localeCompare(b.name)
112
+ );
113
+
114
+ for (const entry of entries) {
115
+ const fullPath = join(dir, entry.name);
116
+
117
+ // Skip node_modules and hidden files
118
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
119
+ continue;
120
+ }
121
+
122
+ if (entry.isDirectory()) {
123
+ processDir(fullPath);
124
+ } else if (entry.isFile()) {
125
+ // Include relative path in hash for structure integrity
126
+ const relativePath = fullPath.slice(dirPath.length + 1);
127
+ hash.update(relativePath);
128
+ hash.update(readFileSync(fullPath));
129
+ }
130
+ }
131
+ }
132
+
133
+ processDir(dirPath);
134
+ return `sha512-${hash.digest('base64')}`;
135
+ }
136
+
137
+ /**
138
+ * Verify package integrity against registry
139
+ */
140
+ export async function verifyPackageIntegrity(
141
+ packageName: string,
142
+ packagePath: string
143
+ ): Promise<{ valid: boolean; expected: string | null; actual: string }> {
144
+ const expected = await getRegistryIntegrity(packageName);
145
+ const actual = computeDirectoryIntegrity(packagePath);
146
+
147
+ if (!expected) {
148
+ // Package not in registry, can't verify
149
+ return { valid: true, expected: null, actual };
150
+ }
151
+
152
+ return {
153
+ valid: expected === actual,
154
+ expected,
155
+ actual,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Clear the registry cache
161
+ */
162
+ export function clearRegistryCache(): void {
163
+ cachedRegistry = null;
164
+ cacheTimestamp = 0;
165
+ }
@@ -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
- });
@@ -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
- });