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 +1 -1
- package/src/bin.ts +2 -4
- package/src/commands/add.ts +26 -14
- package/src/core/registry.ts +165 -0
package/package.json
CHANGED
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
|
|
96
|
-
|
|
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
|
});
|
package/src/commands/add.ts
CHANGED
|
@@ -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
|
-
|
|
221
|
+
function checkCliTool(name: string): boolean {
|
|
221
222
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
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 =
|
|
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
|
+
}
|