cli4ai 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli4ai",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The package manager for AI CLI tools - cli4ai.com",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bin.ts CHANGED
@@ -92,10 +92,8 @@ function checkUpdatesInBackground(): void {
92
92
  const packages = getNpmGlobalPackages();
93
93
  if (packages.length > 0) {
94
94
  // Spawn a background process to check updates and cache results
95
- const child = Bun.spawn(['sh', '-c', `
96
- # Quick npm check (with timeout)
97
- timeout 3 npm view cli4ai version 2>/dev/null > /tmp/.cli4ai-update-check 2>&1 &
98
- `], {
95
+ const { spawn } = require('child_process');
96
+ const child = spawn('sh', ['-c', 'timeout 3 npm view cli4ai version 2>/dev/null > /tmp/.cli4ai-update-check 2>&1 &'], {
99
97
  detached: true,
100
98
  stdio: ['ignore', 'ignore', 'ignore']
101
99
  });
@@ -18,6 +18,7 @@ import {
18
18
  } from '../core/config.js';
19
19
  import { lockPackage, computeDirectoryIntegrity, type LockedPackage } from '../core/lockfile.js';
20
20
  import { linkPackageDirect, isBinInPath, getPathInstructions } from '../core/link.js';
21
+ import { getRegistryIntegrity, verifyPackageIntegrity } from '../core/registry.js';
21
22
 
22
23
  interface AddOptions {
23
24
  local?: boolean;
@@ -217,11 +218,10 @@ async function downloadFromNpm(packageName: string, targetDir: string): Promise<
217
218
  /**
218
219
  * Check if a CLI tool is available
219
220
  */
220
- async function checkCliTool(name: string): Promise<boolean> {
221
+ function checkCliTool(name: string): boolean {
221
222
  try {
222
- const proc = Bun.spawn(['which', name], { stdout: 'pipe', stderr: 'pipe' });
223
- await proc.exited;
224
- return proc.exitCode === 0;
223
+ const result = spawnSync('which', [name], { stdio: 'pipe' });
224
+ return result.status === 0;
225
225
  } catch {
226
226
  return false;
227
227
  }
@@ -369,7 +369,7 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
369
369
  for (const peer of plan.peerDependencies) {
370
370
  // Check if it's a CLI tool (not a cli4ai package reference)
371
371
  if (!peer.version.includes('cli4ai')) {
372
- const available = await checkCliTool(peer.name);
372
+ const available = checkCliTool(peer.name);
373
373
  if (!available) {
374
374
  peerWarnings.push(`${peer.name} is not installed on your system`);
375
375
  }
@@ -417,6 +417,27 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
417
417
  await installNpmDependencies(result.path, plan.manifest.dependencies);
418
418
  }
419
419
 
420
+ // SECURITY: Verify integrity against registry
421
+ let integrity: string | undefined;
422
+ try {
423
+ const verification = await verifyPackageIntegrity(result.name, result.path);
424
+ integrity = verification.actual;
425
+
426
+ if (verification.expected) {
427
+ if (verification.valid) {
428
+ log(` integrity: ${integrity.slice(0, 20)}... ✓`);
429
+ } else {
430
+ log(` ⚠️ Integrity mismatch!`);
431
+ log(` Expected: ${verification.expected.slice(0, 30)}...`);
432
+ log(` Actual: ${verification.actual.slice(0, 30)}...`);
433
+ }
434
+ } else {
435
+ log(` integrity: ${integrity.slice(0, 20)}...`);
436
+ }
437
+ } catch (err) {
438
+ log(` warning: could not compute integrity hash`);
439
+ }
440
+
420
441
  // Link to PATH for global installs
421
442
  if (options.global) {
422
443
  result.binPath = linkPackageDirect(plan.manifest, result.path);
@@ -424,15 +445,6 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
424
445
  } else {
425
446
  log(`✓ ${result.name}@${result.version}`);
426
447
 
427
- // SECURITY: Compute integrity hash for the installed package
428
- let integrity: string | undefined;
429
- try {
430
- integrity = computeDirectoryIntegrity(result.path);
431
- log(` integrity: ${integrity.slice(0, 20)}...`);
432
- } catch (err) {
433
- log(` warning: could not compute integrity hash`);
434
- }
435
-
436
448
  // Update lockfile (only for local/project installs)
437
449
  const lockedPkg: LockedPackage = {
438
450
  name: result.name,
@@ -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
+ }