New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@cli4ai/lib

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cli4ai/lib - npm Package Compare versions

Comparing version
1.0.8
to
1.0.9
+2
-9
package.json
{
"name": "@cli4ai/lib",
"version": "1.0.8",
"version": "1.0.9",
"description": "Shared CLI framework for cli4ai tools",

@@ -16,8 +16,3 @@ "author": "cliforai",

},
"./cli.ts": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js",
"default": "./dist/cli.js"
},
"./browser.ts": {
"./browser": {
"types": "./dist/browser.d.ts",

@@ -56,4 +51,2 @@ "import": "./dist/browser.js",

"files": [
"cli.ts",
"browser.ts",
"dist"

@@ -60,0 +53,0 @@ ],

/**
* Shared browser utilities for packages that depend on @cli4ai/chrome
* Auto-launches managed browser if not running
*/
import puppeteer, { Browser, Page } from 'puppeteer';
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Persistent storage paths (same as chrome package)
const CLI4AI_DIR = join(homedir(), '.cli4ai');
const CHROME_DIR = join(CLI4AI_DIR, 'chrome');
const PROFILE_DIR = join(CHROME_DIR, 'profile');
const WS_FILE = join(CHROME_DIR, 'ws-endpoint');
const PID_FILE = join(CHROME_DIR, 'pid');
function ensureDirs() {
if (!existsSync(CLI4AI_DIR)) mkdirSync(CLI4AI_DIR, { recursive: true });
if (!existsSync(CHROME_DIR)) mkdirSync(CHROME_DIR, { recursive: true });
}
function isBrowserRunning(): boolean {
if (!existsSync(PID_FILE)) return false;
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
try {
process.kill(pid, 0);
return true;
} catch {
cleanup();
return false;
}
}
function cleanup() {
if (existsSync(WS_FILE)) unlinkSync(WS_FILE);
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
}
async function launchBrowser(headless: boolean): Promise<Browser> {
ensureDirs();
const browser = await puppeteer.launch({
headless: headless ? 'shell' : false,
userDataDir: PROFILE_DIR,
args: [
'--no-first-run',
'--no-default-browser-check',
'--disable-infobars',
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
],
defaultViewport: null,
ignoreDefaultArgs: ['--enable-automation'],
});
// Remove automation detection signals
const userAgent = process.platform === 'win32'
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
const pages = await browser.pages();
for (const page of pages) {
await page.setUserAgent(userAgent);
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});
}
const wsEndpoint = browser.wsEndpoint();
writeFileSync(WS_FILE, wsEndpoint);
const browserProcess = browser.process();
if (browserProcess?.pid) {
writeFileSync(PID_FILE, String(browserProcess.pid));
}
return browser;
}
/**
* Get or create browser instance
* Auto-launches if not running
*/
export async function getBrowser(headless = false): Promise<Browser> {
if (isBrowserRunning() && existsSync(WS_FILE)) {
try {
const ws = readFileSync(WS_FILE, 'utf-8').trim();
return await puppeteer.connect({ browserWSEndpoint: ws });
} catch {
cleanup();
}
}
return launchBrowser(headless);
}
/**
* Get a page from the browser
*/
export async function getPage(browser: Browser, newTab = false): Promise<Page> {
if (newTab) {
return browser.newPage();
}
const pages = await browser.pages();
return pages[pages.length - 1] || browser.newPage();
}
/**
* Execute a function with a page, handling browser connection
*/
export async function withPage<T>(
fn: (page: Page) => Promise<T>,
options: { headless?: boolean; newTab?: boolean } = {}
): Promise<T> {
const browser = await getBrowser(options.headless ?? false);
const page = await getPage(browser, options.newTab ?? false);
try {
return await fn(page);
} finally {
browser.disconnect();
}
}
/**
* Execute a function with a new tab
*/
export async function withNewPage<T>(
fn: (page: Page) => Promise<T>,
headless = false
): Promise<T> {
const browser = await getBrowser(headless);
const page = await browser.newPage();
try {
return await fn(page);
} finally {
await page.close();
browser.disconnect();
}
}
/**
* Check if browser is currently running
*/
export function isRunning(): boolean {
return isBrowserRunning();
}
export { Browser, Page };
-299
/**
* cli4ai - cli4ai.com
* Standardized CLI framework for AI agent tools
*/
import { Command } from 'commander';
import { resolve } from 'path';
import { existsSync, readFileSync } from 'fs';
const BRAND = 'cli4ai - cli4ai.com';
// ═══════════════════════════════════════════════════════════════════════════
// TYPES
// ═══════════════════════════════════════════════════════════════════════════
export interface CLIError {
error: string;
message: string;
[key: string]: unknown;
}
export type CommandFn = (...args: string[]) => Promise<void> | void;
// ═══════════════════════════════════════════════════════════════════════════
// ENV LOADING
// ═══════════════════════════════════════════════════════════════════════════
let envLoaded = false;
export interface LoadEnvOptions {
cwd?: string;
searchParents?: boolean;
maxDepth?: number;
}
/**
* Load .env file from disk (explicit opt-in).
*
* SECURITY NOTE: Prefer `cli4ai secrets set <key>` over loading .env files.
*/
export function loadEnv(options: LoadEnvOptions = {}): void {
if (envLoaded) return;
envLoaded = true;
const cwd = options.cwd ?? process.cwd();
const searchParents = options.searchParents ?? false;
const maxDepth = options.maxDepth ?? (searchParents ? 5 : 0);
// Start from current working directory (optionally walk parents)
let dir = cwd;
for (let i = 0; i <= maxDepth; i++) {
const envPath = resolve(dir, '.env');
if (existsSync(envPath)) {
try {
const content = readFileSync(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1).trim();
if (!process.env[key]) {
process.env[key] = value;
}
}
}
}
return;
} catch {}
}
if (!searchParents) return;
dir = resolve(dir, '..');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ENV VALIDATION
// ═══════════════════════════════════════════════════════════════════════════
// SECURITY NOTE: We intentionally do NOT auto-load .env files from the filesystem.
// This prevents supply chain attacks where malicious .env files in parent directories
// could inject credentials or override security settings.
//
// Use `cli4ai secrets set <key>` for secure credential storage, or set environment
// variables explicitly in your shell/CI environment.
/**
* Require environment variables to be set. Exits with parseable error if missing.
*/
export function requireEnv(...variables: string[]): void {
const missing = variables.filter(v => !process.env[v]);
if (missing.length > 0) {
outputError('ENV_MISSING', `Missing required environment variables: ${missing.join(', ')}`, {
variables: missing,
hint: 'Use "cli4ai secrets set <key>" to store securely, or set in your shell environment'
});
}
}
/**
* Get env var or exit with error
*/
export function env(name: string): string {
const value = process.env[name];
if (!value) {
outputError('ENV_MISSING', `Missing required environment variable: ${name}`, {
variables: [name],
hint: 'Use "cli4ai secrets set ' + name + '" to store securely, or set in your shell environment'
});
}
return value;
}
/**
* Get env var or return default value
*/
export function envOr(name: string, defaultValue: string): string {
return process.env[name] || defaultValue;
}
// ═══════════════════════════════════════════════════════════════════════════
// OUTPUT
// ═══════════════════════════════════════════════════════════════════════════
const EXIT_CODES: Record<string, number> = {
NOT_FOUND: 2,
INVALID_INPUT: 3,
ENV_MISSING: 4,
MANIFEST_ERROR: 4,
INSTALL_ERROR: 4,
AUTH_FAILED: 6,
NETWORK_ERROR: 7,
RATE_LIMITED: 8,
TIMEOUT: 9,
PARSE_ERROR: 10,
NPM_ERROR: 11,
};
function getExitCode(code: string): number {
return EXIT_CODES[code] ?? 1;
}
/**
* Output JSON data to stdout
*/
export function output(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}
/**
* Output error and exit. Format is parseable JSON.
*/
export function outputError(
code: string,
message: string,
details?: Record<string, unknown>
): never {
console.error(JSON.stringify({
error: code,
message,
...details
}));
process.exit(getExitCode(code));
}
/**
* Log to stderr (for progress messages)
*/
export function log(message: string): void {
console.error(message);
}
// ═══════════════════════════════════════════════════════════════════════════
// CLI CREATION
// ═══════════════════════════════════════════════════════════════════════════
/**
* Create a branded CLI program
*/
export function cli(name: string, version: string, description: string): Command {
const program = new Command()
.name(name)
.version(version, '-v, --version', 'Show version')
.description(description)
.addHelpText('beforeAll', `\n${BRAND}\n`)
.configureOutput({
writeErr: (str) => {
// Don't double-output errors
if (!str.includes('"error"')) {
process.stderr.write(str);
}
}
})
.exitOverride((err) => {
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
process.exit(0);
}
if (err.code === 'commander.missingArgument') {
outputError('INVALID_INPUT', err.message, { code: err.code });
}
if (err.code === 'commander.unknownCommand') {
outputError('INVALID_INPUT', err.message, { code: err.code });
}
// Don't exit for help - commander already handles it
if (err.code !== 'commander.help') {
process.exit(1);
}
});
// Add help command explicitly
program.addHelpCommand('help [command]', 'Show help for command');
return program;
}
// ═══════════════════════════════════════════════════════════════════════════
// ERROR CODES (for consistent error handling)
// ═══════════════════════════════════════════════════════════════════════════
export const ErrorCodes = {
ENV_MISSING: 'ENV_MISSING',
INVALID_INPUT: 'INVALID_INPUT',
NOT_FOUND: 'NOT_FOUND',
AUTH_FAILED: 'AUTH_FAILED',
API_ERROR: 'API_ERROR',
NETWORK_ERROR: 'NETWORK_ERROR',
RATE_LIMITED: 'RATE_LIMITED',
TIMEOUT: 'TIMEOUT',
PARSE_ERROR: 'PARSE_ERROR',
MANIFEST_ERROR: 'MANIFEST_ERROR',
INSTALL_ERROR: 'INSTALL_ERROR',
NPM_ERROR: 'NPM_ERROR',
} as const;
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY HELPERS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Parse a string as JSON, or return error
*/
export function parseJson<T>(str: string, context?: string): T {
try {
return JSON.parse(str);
} catch {
outputError('PARSE_ERROR', `Invalid JSON${context ? ` in ${context}` : ''}`, {
input: str.slice(0, 100)
});
}
}
/**
* Sleep for ms milliseconds
*/
export const sleep = (ms: number): Promise<void> =>
new Promise(r => setTimeout(r, ms));
/**
* Wrap an async action with error handling
*/
export function withErrorHandling<T extends unknown[]>(
fn: (...args: T) => Promise<void>
): (...args: T) => Promise<void> {
return async (...args: T) => {
try {
await fn(...args);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
outputError('API_ERROR', message);
}
};
}
/**
* Format bytes to human readable
*/
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
/**
* Format date to ISO string (date only)
*/
export function formatDate(date: Date | number | string): string {
const d = new Date(date);
return d.toISOString().slice(0, 10);
}
/**
* Format date to ISO string (datetime)
*/
export function formatDateTime(date: Date | number | string): string {
const d = new Date(date);
return d.toISOString().slice(0, 19).replace('T', ' ');
}