@happyvertical/utils 0.79.0 → 0.80.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.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/cli/parse-args.ts","../src/cli/parse-flags.ts","../src/shared/code/sandbox.ts","../src/web.ts","../src/index.ts"],"sourcesContent":["/**\n * CLI Argument Parsing Utility\n *\n * Provides robust parsing for multi-word commands, options, and arguments.\n * Supports Node.js util.parseArgs for option parsing with fallback.\n */\n\nimport { basename } from 'node:path';\nimport { parseArgs as nodeParseArgs } from 'node:util';\n\n/**\n * Command option configuration\n */\nexport interface OptionConfig {\n type: 'string' | 'boolean' | 'number';\n description: string;\n default?: any;\n short?: string;\n}\n\n/**\n * Command definition\n */\nexport interface Command {\n name: string;\n description: string;\n aliases?: string[];\n options?: Record<string, OptionConfig>;\n args?: string[];\n handler?: (args: any, options: any) => Promise<void>;\n}\n\n/**\n * Parsed command line arguments\n */\nexport interface ParsedArgs {\n command?: string;\n args: string[];\n options: Record<string, any>;\n}\n\n/**\n * Parse command line arguments with support for multi-word commands\n *\n * Handles:\n * - Multi-word commands (up to 3 words: \"gnode create\", \"foo bar baz\")\n * - Command aliases\n * - Positional arguments\n * - Options using Node.js util.parseArgs\n * - Global flags (--help, --version)\n * - Automatic removal of node/script paths\n *\n * @param argv - Process argv or custom argument array\n * @param commands - Array of command definitions to match against\n * @param builtInCommands - Optional map of additional built-in commands\n * @returns Parsed arguments with command, args, and options\n *\n * @example\n * ```typescript\n * const parsed = parseCliArgs(\n * ['node', 'cli.js', 'gnode', 'create', 'my-site', '--template=town'],\n * commands\n * );\n * // { command: 'gnode create', args: ['my-site'], options: { template: 'town' } }\n * ```\n */\nexport function parseCliArgs(\n argv: string[],\n commands: Command[],\n builtInCommands: Record<string, Command> = {},\n): ParsedArgs {\n // Remove node and script name if present\n // Be precise: only remove if it's actually the node executable or a .js file\n // Use basename to avoid matching commands like \"gnode\" which end with \"node\"\n let args = argv;\n\n // Check if first arg is node executable (check basename to avoid false matches)\n if (args.length > 0 && basename(args[0]) === 'node') {\n args = args.slice(1);\n }\n\n // Check if first arg is a .js file (script name)\n if (args.length > 0 && args[0].endsWith('.js')) {\n args = args.slice(1);\n }\n\n if (args.length === 0) {\n return { args: [], options: {} };\n }\n\n // Handle global --help flag (check for -h later to avoid conflict with command options)\n if (args.includes('--help')) {\n return { command: 'help', args: [], options: {} };\n }\n\n if (args.includes('--version')) {\n return { command: 'version', args: [], options: {} };\n }\n\n // Try to match multi-word commands (longest match wins)\n let matchedCommand: Command | undefined;\n let commandName: string | undefined;\n let commandWordCount = 0;\n\n // Try up to 3 words for command name (e.g., \"foo bar baz\")\n for (let i = Math.min(3, args.length); i > 0; i--) {\n const possibleCommand = args.slice(0, i).join(' ');\n const found =\n builtInCommands[possibleCommand] ||\n commands.find(\n (cmd) =>\n cmd.name === possibleCommand ||\n cmd.aliases?.includes(possibleCommand),\n );\n\n if (found) {\n matchedCommand = found;\n commandName = possibleCommand;\n commandWordCount = i;\n break;\n }\n }\n\n // If no multi-word match, try single word\n if (!commandName && args.length > 0) {\n commandName = args[0];\n commandWordCount = 1;\n matchedCommand = commands.find(\n (cmd) =>\n cmd.name === commandName ||\n cmd.aliases?.includes(commandName as string),\n );\n }\n\n // If no command matched, check for global short flags\n if (!matchedCommand) {\n if (args.includes('-h')) {\n return { command: 'help', args: [], options: {} };\n }\n if (args.includes('-v')) {\n return { command: 'version', args: [], options: {} };\n }\n // Return unknown command with args filtered\n return {\n command: commandName,\n args: args.slice(1).filter((arg) => !arg.startsWith('-')),\n options: {},\n };\n }\n\n // Build parseArgs config from command definition\n const parseConfig: any = {\n args: args.slice(commandWordCount),\n options: {},\n strict: false, // Allow unknown options\n allowPositionals: true, // Required for mixing positional args and options\n };\n\n // Track which options are numbers for post-processing\n const numberOptions: Set<string> = new Set();\n\n if (matchedCommand.options) {\n for (const [name, option] of Object.entries(matchedCommand.options)) {\n // Node.js parseArgs only supports 'string' and 'boolean'\n // Treat 'number' as 'string' and convert after parsing\n const nodeType = option.type === 'boolean' ? 'boolean' : 'string';\n\n if (option.type === 'number') {\n numberOptions.add(name);\n }\n\n parseConfig.options[name] = {\n type: nodeType,\n };\n\n // Handle default value - convert number defaults to string for parseArgs\n if (option.default !== undefined) {\n parseConfig.options[name].default =\n option.type === 'number' ? String(option.default) : option.default;\n }\n\n if (option.short) {\n parseConfig.options[name].short = option.short;\n }\n }\n }\n\n try {\n const parsed = nodeParseArgs(parseConfig);\n // Cast to Record<string, unknown> since we'll be adding number values\n // during post-processing of 'number' type options\n const options: Record<string, unknown> = parsed.values || {};\n\n // Post-process number options: convert strings to numbers\n for (const name of numberOptions) {\n if (name in options && options[name] !== undefined) {\n const rawValue = options[name];\n // Empty string should be treated as invalid, not as 0\n if (rawValue === '') {\n throw new Error(`Invalid number for --${name}: \"${rawValue}\"`);\n }\n const value = Number(rawValue);\n if (Number.isNaN(value)) {\n throw new Error(`Invalid number for --${name}: \"${rawValue}\"`);\n }\n options[name] = value;\n }\n }\n\n return {\n command: commandName,\n args: parsed.positionals || [],\n options,\n };\n } catch (error) {\n // Re-throw validation errors (invalid number conversion)\n if (error instanceof Error && error.message.startsWith('Invalid number')) {\n throw error;\n }\n // Fallback for parse errors - extract positional args manually\n return {\n command: commandName,\n args: args.slice(commandWordCount).filter((arg) => !arg.startsWith('-')),\n options: {},\n };\n }\n}\n","/**\n * Lightweight flag/positional splitter for one-shot scripts.\n *\n * Distinct from `parseCliArgs`, which models multi-word commands and typed\n * options. `parseFlagArgs` is the bare-bones alternative for scripts that\n * just need `{ flags, positionals }` from `process.argv.slice(2)`.\n *\n * Flag forms supported:\n * --foo -> flags.foo = true\n * --foo bar -> flags.foo = 'bar'\n * --foo=bar -> flags.foo = 'bar'\n * --foo-bar baz -> flags.fooBar = 'baz' (kebab -> camel)\n * -- -> stop parsing flags; rest are positionals\n *\n * Anything not starting with `--` becomes a positional.\n */\nexport interface ParsedFlagArgs {\n flags: Record<string, string | true>;\n positionals: string[];\n}\n\nexport function parseFlagArgs(args: string[]): ParsedFlagArgs {\n const flags: Record<string, string | true> = {};\n const positionals: string[] = [];\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n if (arg === '--') {\n positionals.push(...args.slice(index + 1));\n break;\n }\n if (!arg.startsWith('--')) {\n positionals.push(arg);\n continue;\n }\n\n const [rawKey, inlineValue] = arg.slice(2).split(/=(.*)/su, 2);\n if (rawKey === '') {\n // `--=value` or just `--` followed by garbage — the empty key\n // would be unreachable to any sensible caller. Throw rather than\n // silently producing `flags[''] = value`.\n throw new Error(\n `Invalid flag token \"${arg}\": flag name is empty. Use --key=value or --key value.`,\n );\n }\n const key = rawKey.replace(/-([a-z])/gu, (_match, letter: string) =>\n letter.toUpperCase(),\n );\n if (inlineValue !== undefined) {\n flags[key] = inlineValue;\n continue;\n }\n\n const next = args[index + 1];\n if (next !== undefined && !next.startsWith('--')) {\n flags[key] = next;\n index += 1;\n } else {\n flags[key] = true;\n }\n }\n\n return { flags, positionals };\n}\n","/**\n * Sandbox creation and safe code execution utilities\n *\n * Provides secure execution of generated code in isolated VM contexts with\n * controlled globals, timeouts, and resource constraints.\n */\n\nimport * as vm from 'node:vm';\n\n/**\n * Options for creating a sandbox execution context\n */\nexport interface SandboxOptions {\n /**\n * Global variables to make available in the sandbox\n * These will be accessible as global variables in the executed code\n */\n globals?: Record<string, any>;\n\n /**\n * Maximum execution time in milliseconds\n * Default: 5000ms (5 seconds)\n */\n timeout?: number;\n\n /**\n * Allowed built-in JavaScript objects\n * Default: ['Array', 'Object', 'JSON', 'Math', 'Date', 'String', 'Number', 'Boolean', 'RegExp']\n */\n allowedBuiltins?: string[];\n\n /**\n * Whether to allow console access (useful for debugging)\n * Default: false (console will be undefined unless provided in globals)\n */\n allowConsole?: boolean;\n}\n\n/**\n * Options for executing code in a sandbox\n */\nexport interface ExecuteOptions {\n /**\n * Maximum execution time in milliseconds\n * Overrides the sandbox-level timeout if provided\n */\n timeout?: number;\n\n /**\n * Filename to use in error messages and stack traces\n * Default: 'generated-code.js'\n */\n filename?: string;\n\n /**\n * Whether to capture and return the last expression value\n * Default: true\n */\n captureResult?: boolean;\n}\n\n/**\n * Default safe built-in objects available in the sandbox\n */\nconst DEFAULT_BUILTINS = [\n 'Array',\n 'Object',\n 'JSON',\n 'Math',\n 'Date',\n 'String',\n 'Number',\n 'Boolean',\n 'RegExp',\n 'Set',\n 'Map',\n 'WeakSet',\n 'WeakMap',\n 'Symbol',\n 'Promise',\n];\n\n/**\n * Creates a secure sandbox execution context with controlled globals\n *\n * @param options - Configuration for the sandbox\n * @returns A VM context that can be used with executeCode()\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: {\n * cheerio: require('cheerio'),\n * data: { foo: 'bar' }\n * },\n * timeout: 5000,\n * allowedBuiltins: ['Array', 'Object', 'JSON']\n * });\n *\n * const result = executeCode('data.foo', sandbox);\n * // Returns: \"bar\"\n * ```\n */\nexport function createSandbox(options: SandboxOptions = {}): vm.Context {\n const {\n globals = {},\n allowedBuiltins = DEFAULT_BUILTINS,\n allowConsole = false,\n } = options;\n\n // Start with an empty object with null prototype to avoid inheriting anything\n const sandbox: Record<string, any> = Object.create(null);\n\n // Add allowed built-ins\n for (const builtin of allowedBuiltins) {\n if (builtin in globalThis) {\n sandbox[builtin] = (globalThis as any)[builtin];\n }\n }\n\n // Add console if allowed (otherwise it remains undefined)\n if (allowConsole) {\n sandbox.console = console;\n }\n\n // Add user-provided globals\n Object.assign(sandbox, globals);\n\n // Create and return the context\n return vm.createContext(sandbox);\n}\n\n/**\n * Executes code in a sandbox with timeout and error handling\n *\n * @param code - The JavaScript code to execute\n * @param sandbox - The VM context created by createSandbox()\n * @param options - Execution options\n * @returns The result of the code execution\n * @throws {Error} If code execution fails or times out\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: { x: 10, y: 20 }\n * });\n *\n * const result = executeCode('x + y', sandbox);\n * // Returns: 30\n *\n * // With a function\n * const funcResult = executeCode(`\n * function add(a, b) {\n * return a + b;\n * }\n * add(x, y);\n * `, sandbox);\n * // Returns: 30\n * ```\n */\nexport function executeCode<T = any>(\n code: string,\n sandbox: vm.Context,\n options: ExecuteOptions = {},\n): T {\n const {\n timeout = 5000,\n filename = 'generated-code.js',\n captureResult = true,\n } = options;\n\n try {\n // For simple expressions, wrap in return. For complex code, execute directly\n let wrappedCode: string;\n\n if (captureResult) {\n // Check if code has multiple statements or function definitions\n const hasMultipleStatements =\n code.includes(';') ||\n (code.includes('\\n') && code.trim().split('\\n').length > 1);\n const hasFunctionDef =\n /function\\s+\\w+|const\\s+\\w+\\s*=\\s*function|const\\s+\\w+\\s*=\\s*\\(/i.test(\n code,\n );\n\n if (hasMultipleStatements || hasFunctionDef) {\n // Execute code as-is (will return last expression)\n wrappedCode = code;\n } else {\n // Simple expression - wrap in return\n wrappedCode = `(function() { return (${code}); })();`;\n }\n } else {\n wrappedCode = code;\n }\n\n const result = vm.runInContext(wrappedCode, sandbox, {\n timeout,\n filename,\n displayErrors: true,\n });\n\n return result as T;\n } catch (error) {\n // Enhanced error message with context\n if (error instanceof Error) {\n const message = `Code execution failed: ${error.message}`;\n const enhancedError = new Error(message);\n enhancedError.stack = error.stack;\n throw enhancedError;\n }\n throw error;\n }\n}\n\n/**\n * Executes async code in a sandbox with timeout and error handling\n *\n * @param code - The JavaScript code to execute (can contain async/await)\n * @param sandbox - The VM context created by createSandbox()\n * @param options - Execution options\n * @returns Promise resolving to the result of the code execution\n * @throws {Error} If code execution fails or times out\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: {\n * fetch: require('node-fetch')\n * }\n * });\n *\n * const result = await executeCodeAsync(`\n * const response = await fetch('https://api.example.com/data');\n * const data = await response.json();\n * data;\n * `, sandbox);\n * ```\n */\nexport async function executeCodeAsync<T = any>(\n code: string,\n sandbox: vm.Context,\n options: ExecuteOptions = {},\n): Promise<T> {\n const {\n timeout = 5000,\n filename = 'generated-code.js',\n captureResult = true,\n } = options;\n\n try {\n // For async code, wrap in async function with smart return handling\n let wrappedCode: string;\n\n if (captureResult) {\n // For multi-line code, wrap in async IIFE and return last expression\n // Split into statements and make last one a return\n const trimmedCode = code.trim();\n const lines = trimmedCode.split('\\n');\n\n if (lines.length > 1 || trimmedCode.includes(';')) {\n // Multi-line or multiple statements - find last expression\n const statements = trimmedCode\n .split('\\n')\n .filter((line) => line.trim());\n const lastLine = statements[statements.length - 1];\n const otherLines = statements.slice(0, -1);\n\n // If last line is already a return statement, keep code as-is\n if (lastLine.trim().startsWith('return ')) {\n wrappedCode = `(async function() {\n ${trimmedCode}\n })();`;\n } else {\n // Make last line a return (remove trailing semicolon if present)\n const lastExpression = lastLine.trim().replace(/;$/, '');\n wrappedCode = `(async function() {\n ${otherLines.join('\\n')}\n return ${lastExpression};\n })();`;\n }\n } else {\n // Single expression - wrap in return\n wrappedCode = `(async function() {\n return (${trimmedCode});\n })();`;\n }\n } else {\n wrappedCode = `(async function() {\n ${code}\n })();`;\n }\n\n const result = await vm.runInContext(wrappedCode, sandbox, {\n timeout,\n filename,\n displayErrors: true,\n });\n\n return result as T;\n } catch (error) {\n // Enhanced error message with context\n if (error instanceof Error) {\n const message = `Async code execution failed: ${error.message}`;\n const enhancedError = new Error(message);\n enhancedError.stack = error.stack;\n throw enhancedError;\n }\n throw error;\n }\n}\n\n/**\n * Convenience function to create a sandbox and execute code in one step\n *\n * @param code - The JavaScript code to execute\n * @param options - Combined sandbox and execution options\n * @returns The result of the code execution\n *\n * @example\n * ```typescript\n * const result = executeInSandbox('Math.sqrt(16)', {\n * globals: { x: 10 },\n * timeout: 1000\n * });\n * // Returns: 4\n * ```\n */\nexport function executeInSandbox<T = any>(\n code: string,\n options: SandboxOptions & ExecuteOptions = {},\n): T {\n const sandbox = createSandbox(options);\n return executeCode<T>(code, sandbox, options);\n}\n\n/**\n * Convenience function to create a sandbox and execute async code in one step\n *\n * @param code - The JavaScript code to execute (can contain async/await)\n * @param options - Combined sandbox and execution options\n * @returns Promise resolving to the result of the code execution\n *\n * @example\n * ```typescript\n * const result = await executeInSandboxAsync(`\n * const data = await Promise.resolve({ value: 42 });\n * data.value;\n * `, {\n * timeout: 2000\n * });\n * // Returns: 42\n * ```\n */\nexport async function executeInSandboxAsync<T = any>(\n code: string,\n options: SandboxOptions & ExecuteOptions = {},\n): Promise<T> {\n const sandbox = createSandbox(options);\n return executeCodeAsync<T>(code, sandbox, options);\n}\n","/**\n * Web and URL utility functions\n *\n * General-purpose utilities for working with URLs and web content.\n * Used by scrapers, note systems, and content processors.\n */\n\nimport { createHash } from 'node:crypto';\n\n/**\n * Normalize URL for consistent key storage\n * Removes tracking params, sorts query string, lowercases, etc.\n *\n * @param url - The URL to normalize\n * @returns Normalized URL string\n */\nexport function normalizeUrl(url: string): string {\n const parsed = new URL(url);\n\n // Lowercase scheme and host\n parsed.protocol = parsed.protocol.toLowerCase();\n parsed.hostname = parsed.hostname.toLowerCase();\n\n // Remove www prefix\n parsed.hostname = parsed.hostname.replace(/^www\\./, '');\n\n // Remove default ports\n if (\n (parsed.protocol === 'http:' && parsed.port === '80') ||\n (parsed.protocol === 'https:' && parsed.port === '443')\n ) {\n parsed.port = '';\n }\n\n // Remove fragment\n parsed.hash = '';\n\n // Sort and filter query params\n const params = new URLSearchParams(parsed.search);\n const filtered = new URLSearchParams();\n const trackingParams = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_content',\n 'utm_term',\n 'fbclid',\n 'gclid',\n 'msclkid',\n '_ga',\n 'mc_cid',\n 'mc_eid',\n ];\n\n Array.from(params.keys())\n .sort()\n .forEach((key) => {\n if (!trackingParams.includes(key)) {\n filtered.set(key, params.get(key)!);\n }\n });\n\n parsed.search = filtered.toString();\n\n return parsed.toString();\n}\n\n/**\n * Generate hierarchical scope from URL for organized note storage\n *\n * @param url - The URL to generate scope from\n * @param baseScope - Base scope prefix (default: 'discovery/parser')\n * @returns Hierarchical scope string\n *\n * @example\n * generateScopeFromUrl('https://cityofboston.gov/meetings/minutes')\n * // Returns: 'discovery/parser/cityofboston.gov/meetings'\n */\nexport function generateScopeFromUrl(\n url: string,\n baseScope = 'discovery/parser',\n): string {\n const parsed = new URL(normalizeUrl(url));\n const domain = parsed.hostname;\n const pathParts = parsed.pathname.split('/').filter((p) => p);\n\n // Use first path segment as page type (e.g., 'meetings', 'documents')\n const pageType = pathParts[0] || 'index';\n\n return `${baseScope}/${domain}/${pageType}`;\n}\n\n/**\n * Hash page content for change detection\n * Uses SHA-256 to create a unique fingerprint of the HTML\n *\n * @param html - Page HTML content\n * @returns SHA-256 hash as hex string\n */\nexport function hashPageContent(html: string): string {\n return createHash('sha256').update(html).digest('hex');\n}\n","export * from './cli/index';\nexport * from './config/env-config';\nexport * from './shared/index';\nexport * from './web';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"names":["i","nodeParseArgs","p"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/cli/parse-args.ts","../src/cli/parse-flags.ts","../src/shared/code/sandbox.ts","../src/web.ts","../src/index.ts"],"sourcesContent":["/**\n * CLI Argument Parsing Utility\n *\n * Provides robust parsing for multi-word commands, options, and arguments.\n * Supports Node.js util.parseArgs for option parsing with fallback.\n */\n\nimport { basename } from 'node:path';\nimport { parseArgs as nodeParseArgs } from 'node:util';\n\n/**\n * Command option configuration\n */\nexport interface OptionConfig {\n type: 'string' | 'boolean' | 'number';\n description: string;\n default?: any;\n short?: string;\n}\n\n/**\n * Command definition\n */\nexport interface Command {\n name: string;\n description: string;\n aliases?: string[];\n options?: Record<string, OptionConfig>;\n args?: string[];\n handler?: (args: any, options: any) => Promise<void>;\n}\n\n/**\n * Parsed command line arguments\n */\nexport interface ParsedArgs {\n command?: string;\n args: string[];\n options: Record<string, any>;\n}\n\n/**\n * Parse command line arguments with support for multi-word commands\n *\n * Handles:\n * - Multi-word commands (up to 3 words: \"gnode create\", \"foo bar baz\")\n * - Command aliases\n * - Positional arguments\n * - Options using Node.js util.parseArgs\n * - Global flags (--help, --version)\n * - Automatic removal of node/script paths\n *\n * @param argv - Process argv or custom argument array\n * @param commands - Array of command definitions to match against\n * @param builtInCommands - Optional map of additional built-in commands\n * @returns Parsed arguments with command, args, and options\n *\n * @example\n * ```typescript\n * const parsed = parseCliArgs(\n * ['node', 'cli.js', 'gnode', 'create', 'my-site', '--template=town'],\n * commands\n * );\n * // { command: 'gnode create', args: ['my-site'], options: { template: 'town' } }\n * ```\n */\nexport function parseCliArgs(\n argv: string[],\n commands: Command[],\n builtInCommands: Record<string, Command> = {},\n): ParsedArgs {\n // Remove node and script name if present\n // Be precise: only remove if it's actually the node executable or a .js file\n // Use basename to avoid matching commands like \"gnode\" which end with \"node\"\n let args = argv;\n\n // Check if first arg is node executable (check basename to avoid false matches)\n if (args.length > 0 && basename(args[0]) === 'node') {\n args = args.slice(1);\n }\n\n // Check if first arg is a .js file (script name)\n if (args.length > 0 && args[0].endsWith('.js')) {\n args = args.slice(1);\n }\n\n if (args.length === 0) {\n return { args: [], options: {} };\n }\n\n // Handle global --help flag (check for -h later to avoid conflict with command options)\n if (args.includes('--help')) {\n return { command: 'help', args: [], options: {} };\n }\n\n if (args.includes('--version')) {\n return { command: 'version', args: [], options: {} };\n }\n\n // Try to match multi-word commands (longest match wins)\n let matchedCommand: Command | undefined;\n let commandName: string | undefined;\n let commandWordCount = 0;\n\n // Try up to 3 words for command name (e.g., \"foo bar baz\")\n for (let i = Math.min(3, args.length); i > 0; i--) {\n const possibleCommand = args.slice(0, i).join(' ');\n const found =\n builtInCommands[possibleCommand] ||\n commands.find(\n (cmd) =>\n cmd.name === possibleCommand ||\n cmd.aliases?.includes(possibleCommand),\n );\n\n if (found) {\n matchedCommand = found;\n commandName = possibleCommand;\n commandWordCount = i;\n break;\n }\n }\n\n // If no multi-word match, try single word\n if (!commandName && args.length > 0) {\n commandName = args[0];\n commandWordCount = 1;\n matchedCommand = commands.find(\n (cmd) =>\n cmd.name === commandName ||\n cmd.aliases?.includes(commandName as string),\n );\n }\n\n // If no command matched, check for global short flags\n if (!matchedCommand) {\n if (args.includes('-h')) {\n return { command: 'help', args: [], options: {} };\n }\n if (args.includes('-v')) {\n return { command: 'version', args: [], options: {} };\n }\n // Return unknown command with args filtered\n return {\n command: commandName,\n args: args.slice(1).filter((arg) => !arg.startsWith('-')),\n options: {},\n };\n }\n\n // Build parseArgs config from command definition\n const parseConfig: any = {\n args: args.slice(commandWordCount),\n options: {},\n strict: false, // Allow unknown options\n allowPositionals: true, // Required for mixing positional args and options\n };\n\n // Track which options are numbers for post-processing\n const numberOptions: Set<string> = new Set();\n\n if (matchedCommand.options) {\n for (const [name, option] of Object.entries(matchedCommand.options)) {\n // Node.js parseArgs only supports 'string' and 'boolean'\n // Treat 'number' as 'string' and convert after parsing\n const nodeType = option.type === 'boolean' ? 'boolean' : 'string';\n\n if (option.type === 'number') {\n numberOptions.add(name);\n }\n\n parseConfig.options[name] = {\n type: nodeType,\n };\n\n // Handle default value - convert number defaults to string for parseArgs\n if (option.default !== undefined) {\n parseConfig.options[name].default =\n option.type === 'number' ? String(option.default) : option.default;\n }\n\n if (option.short) {\n parseConfig.options[name].short = option.short;\n }\n }\n }\n\n try {\n const parsed = nodeParseArgs(parseConfig);\n // Cast to Record<string, unknown> since we'll be adding number values\n // during post-processing of 'number' type options\n const options: Record<string, unknown> = parsed.values || {};\n\n // Post-process number options: convert strings to numbers\n for (const name of numberOptions) {\n if (name in options && options[name] !== undefined) {\n const rawValue = options[name];\n // Empty string should be treated as invalid, not as 0\n if (rawValue === '') {\n throw new Error(`Invalid number for --${name}: \"${rawValue}\"`);\n }\n const value = Number(rawValue);\n if (Number.isNaN(value)) {\n throw new Error(`Invalid number for --${name}: \"${rawValue}\"`);\n }\n options[name] = value;\n }\n }\n\n return {\n command: commandName,\n args: parsed.positionals || [],\n options,\n };\n } catch (error) {\n // Re-throw validation errors (invalid number conversion)\n if (error instanceof Error && error.message.startsWith('Invalid number')) {\n throw error;\n }\n // Fallback for parse errors - extract positional args manually\n return {\n command: commandName,\n args: args.slice(commandWordCount).filter((arg) => !arg.startsWith('-')),\n options: {},\n };\n }\n}\n","/**\n * Lightweight flag/positional splitter for one-shot scripts.\n *\n * Distinct from `parseCliArgs`, which models multi-word commands and typed\n * options. `parseFlagArgs` is the bare-bones alternative for scripts that\n * just need `{ flags, positionals }` from `process.argv.slice(2)`.\n *\n * Flag forms supported:\n * --foo -> flags.foo = true\n * --foo bar -> flags.foo = 'bar'\n * --foo=bar -> flags.foo = 'bar'\n * --foo-bar baz -> flags.fooBar = 'baz' (kebab -> camel)\n * -- -> stop parsing flags; rest are positionals\n *\n * Anything not starting with `--` becomes a positional.\n */\nexport interface ParsedFlagArgs {\n flags: Record<string, string | true>;\n positionals: string[];\n}\n\nexport function parseFlagArgs(args: string[]): ParsedFlagArgs {\n const flags: Record<string, string | true> = {};\n const positionals: string[] = [];\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n if (arg === '--') {\n positionals.push(...args.slice(index + 1));\n break;\n }\n if (!arg.startsWith('--')) {\n positionals.push(arg);\n continue;\n }\n\n const [rawKey, inlineValue] = arg.slice(2).split(/=(.*)/su, 2);\n if (rawKey === '') {\n // `--=value` or just `--` followed by garbage — the empty key\n // would be unreachable to any sensible caller. Throw rather than\n // silently producing `flags[''] = value`.\n throw new Error(\n `Invalid flag token \"${arg}\": flag name is empty. Use --key=value or --key value.`,\n );\n }\n const key = rawKey.replace(/-([a-z])/gu, (_match, letter: string) =>\n letter.toUpperCase(),\n );\n if (inlineValue !== undefined) {\n flags[key] = inlineValue;\n continue;\n }\n\n const next = args[index + 1];\n if (next !== undefined && !next.startsWith('--')) {\n flags[key] = next;\n index += 1;\n } else {\n flags[key] = true;\n }\n }\n\n return { flags, positionals };\n}\n","/**\n * Sandbox creation and safe code execution utilities\n *\n * Provides secure execution of generated code in isolated VM contexts with\n * controlled globals, timeouts, and resource constraints.\n */\n\nimport * as vm from 'node:vm';\n\n/**\n * Options for creating a sandbox execution context\n */\nexport interface SandboxOptions {\n /**\n * Global variables to make available in the sandbox\n * These will be accessible as global variables in the executed code\n */\n globals?: Record<string, any>;\n\n /**\n * Maximum execution time in milliseconds\n * Default: 5000ms (5 seconds)\n */\n timeout?: number;\n\n /**\n * Allowed built-in JavaScript objects\n * Default: ['Array', 'Object', 'JSON', 'Math', 'Date', 'String', 'Number', 'Boolean', 'RegExp']\n */\n allowedBuiltins?: string[];\n\n /**\n * Whether to allow console access (useful for debugging)\n * Default: false (console will be undefined unless provided in globals)\n */\n allowConsole?: boolean;\n}\n\n/**\n * Options for executing code in a sandbox\n */\nexport interface ExecuteOptions {\n /**\n * Maximum execution time in milliseconds\n * Overrides the sandbox-level timeout if provided\n */\n timeout?: number;\n\n /**\n * Filename to use in error messages and stack traces\n * Default: 'generated-code.js'\n */\n filename?: string;\n\n /**\n * Whether to capture and return the last expression value\n * Default: true\n */\n captureResult?: boolean;\n}\n\n/**\n * Default safe built-in objects available in the sandbox\n */\nconst DEFAULT_BUILTINS = [\n 'Array',\n 'Object',\n 'JSON',\n 'Math',\n 'Date',\n 'String',\n 'Number',\n 'Boolean',\n 'RegExp',\n 'Set',\n 'Map',\n 'WeakSet',\n 'WeakMap',\n 'Symbol',\n 'Promise',\n];\n\n/**\n * Creates a secure sandbox execution context with controlled globals\n *\n * @param options - Configuration for the sandbox\n * @returns A VM context that can be used with executeCode()\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: {\n * cheerio: require('cheerio'),\n * data: { foo: 'bar' }\n * },\n * timeout: 5000,\n * allowedBuiltins: ['Array', 'Object', 'JSON']\n * });\n *\n * const result = executeCode('data.foo', sandbox);\n * // Returns: \"bar\"\n * ```\n */\nexport function createSandbox(options: SandboxOptions = {}): vm.Context {\n const {\n globals = {},\n allowedBuiltins = DEFAULT_BUILTINS,\n allowConsole = false,\n } = options;\n\n // Start with an empty object with null prototype to avoid inheriting anything\n const sandbox: Record<string, any> = Object.create(null);\n\n // Add allowed built-ins\n for (const builtin of allowedBuiltins) {\n if (builtin in globalThis) {\n sandbox[builtin] = (globalThis as any)[builtin];\n }\n }\n\n // Add console if allowed (otherwise it remains undefined)\n if (allowConsole) {\n sandbox.console = console;\n }\n\n // Add user-provided globals\n Object.assign(sandbox, globals);\n\n // Create and return the context\n return vm.createContext(sandbox);\n}\n\n/**\n * Executes code in a sandbox with timeout and error handling\n *\n * @param code - The JavaScript code to execute\n * @param sandbox - The VM context created by createSandbox()\n * @param options - Execution options\n * @returns The result of the code execution\n * @throws {Error} If code execution fails or times out\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: { x: 10, y: 20 }\n * });\n *\n * const result = executeCode('x + y', sandbox);\n * // Returns: 30\n *\n * // With a function\n * const funcResult = executeCode(`\n * function add(a, b) {\n * return a + b;\n * }\n * add(x, y);\n * `, sandbox);\n * // Returns: 30\n * ```\n */\nexport function executeCode<T = any>(\n code: string,\n sandbox: vm.Context,\n options: ExecuteOptions = {},\n): T {\n const {\n timeout = 5000,\n filename = 'generated-code.js',\n captureResult = true,\n } = options;\n\n try {\n // For simple expressions, wrap in return. For complex code, execute directly\n let wrappedCode: string;\n\n if (captureResult) {\n // Check if code has multiple statements or function definitions\n const hasMultipleStatements =\n code.includes(';') ||\n (code.includes('\\n') && code.trim().split('\\n').length > 1);\n const hasFunctionDef =\n /function\\s+\\w+|const\\s+\\w+\\s*=\\s*function|const\\s+\\w+\\s*=\\s*\\(/i.test(\n code,\n );\n\n if (hasMultipleStatements || hasFunctionDef) {\n // Execute code as-is (will return last expression)\n wrappedCode = code;\n } else {\n // Simple expression - wrap in return\n wrappedCode = `(function() { return (${code}); })();`;\n }\n } else {\n wrappedCode = code;\n }\n\n const result = vm.runInContext(wrappedCode, sandbox, {\n timeout,\n filename,\n displayErrors: true,\n });\n\n return result as T;\n } catch (error) {\n // Enhanced error message with context\n if (error instanceof Error) {\n const message = `Code execution failed: ${error.message}`;\n const enhancedError = new Error(message);\n enhancedError.stack = error.stack;\n throw enhancedError;\n }\n throw error;\n }\n}\n\n/**\n * Executes async code in a sandbox with timeout and error handling\n *\n * @param code - The JavaScript code to execute (can contain async/await)\n * @param sandbox - The VM context created by createSandbox()\n * @param options - Execution options\n * @returns Promise resolving to the result of the code execution\n * @throws {Error} If code execution fails or times out\n *\n * @example\n * ```typescript\n * const sandbox = createSandbox({\n * globals: {\n * fetch: require('node-fetch')\n * }\n * });\n *\n * const result = await executeCodeAsync(`\n * const response = await fetch('https://api.example.com/data');\n * const data = await response.json();\n * data;\n * `, sandbox);\n * ```\n */\nexport async function executeCodeAsync<T = any>(\n code: string,\n sandbox: vm.Context,\n options: ExecuteOptions = {},\n): Promise<T> {\n const {\n timeout = 5000,\n filename = 'generated-code.js',\n captureResult = true,\n } = options;\n\n try {\n // For async code, wrap in async function with smart return handling\n let wrappedCode: string;\n\n if (captureResult) {\n // For multi-line code, wrap in async IIFE and return last expression\n // Split into statements and make last one a return\n const trimmedCode = code.trim();\n const lines = trimmedCode.split('\\n');\n\n if (lines.length > 1 || trimmedCode.includes(';')) {\n // Multi-line or multiple statements - find last expression\n const statements = trimmedCode\n .split('\\n')\n .filter((line) => line.trim());\n const lastLine = statements[statements.length - 1];\n const otherLines = statements.slice(0, -1);\n\n // If last line is already a return statement, keep code as-is\n if (lastLine.trim().startsWith('return ')) {\n wrappedCode = `(async function() {\n ${trimmedCode}\n })();`;\n } else {\n // Make last line a return (remove trailing semicolon if present)\n const lastExpression = lastLine.trim().replace(/;$/, '');\n wrappedCode = `(async function() {\n ${otherLines.join('\\n')}\n return ${lastExpression};\n })();`;\n }\n } else {\n // Single expression - wrap in return\n wrappedCode = `(async function() {\n return (${trimmedCode});\n })();`;\n }\n } else {\n wrappedCode = `(async function() {\n ${code}\n })();`;\n }\n\n const result = await vm.runInContext(wrappedCode, sandbox, {\n timeout,\n filename,\n displayErrors: true,\n });\n\n return result as T;\n } catch (error) {\n // Enhanced error message with context\n if (error instanceof Error) {\n const message = `Async code execution failed: ${error.message}`;\n const enhancedError = new Error(message);\n enhancedError.stack = error.stack;\n throw enhancedError;\n }\n throw error;\n }\n}\n\n/**\n * Convenience function to create a sandbox and execute code in one step\n *\n * @param code - The JavaScript code to execute\n * @param options - Combined sandbox and execution options\n * @returns The result of the code execution\n *\n * @example\n * ```typescript\n * const result = executeInSandbox('Math.sqrt(16)', {\n * globals: { x: 10 },\n * timeout: 1000\n * });\n * // Returns: 4\n * ```\n */\nexport function executeInSandbox<T = any>(\n code: string,\n options: SandboxOptions & ExecuteOptions = {},\n): T {\n const sandbox = createSandbox(options);\n return executeCode<T>(code, sandbox, options);\n}\n\n/**\n * Convenience function to create a sandbox and execute async code in one step\n *\n * @param code - The JavaScript code to execute (can contain async/await)\n * @param options - Combined sandbox and execution options\n * @returns Promise resolving to the result of the code execution\n *\n * @example\n * ```typescript\n * const result = await executeInSandboxAsync(`\n * const data = await Promise.resolve({ value: 42 });\n * data.value;\n * `, {\n * timeout: 2000\n * });\n * // Returns: 42\n * ```\n */\nexport async function executeInSandboxAsync<T = any>(\n code: string,\n options: SandboxOptions & ExecuteOptions = {},\n): Promise<T> {\n const sandbox = createSandbox(options);\n return executeCodeAsync<T>(code, sandbox, options);\n}\n","/**\n * Web and URL utility functions\n *\n * General-purpose utilities for working with URLs and web content.\n * Used by scrapers, note systems, and content processors.\n */\n\nimport { createHash } from 'node:crypto';\n\n/**\n * Normalize URL for consistent key storage\n * Removes tracking params, sorts query string, lowercases, etc.\n *\n * @param url - The URL to normalize\n * @returns Normalized URL string\n */\nexport function normalizeUrl(url: string): string {\n const parsed = new URL(url);\n\n // Lowercase scheme and host\n parsed.protocol = parsed.protocol.toLowerCase();\n parsed.hostname = parsed.hostname.toLowerCase();\n\n // Remove www prefix\n parsed.hostname = parsed.hostname.replace(/^www\\./, '');\n\n // Remove default ports\n if (\n (parsed.protocol === 'http:' && parsed.port === '80') ||\n (parsed.protocol === 'https:' && parsed.port === '443')\n ) {\n parsed.port = '';\n }\n\n // Remove fragment\n parsed.hash = '';\n\n // Sort and filter query params\n const params = new URLSearchParams(parsed.search);\n const filtered = new URLSearchParams();\n const trackingParams = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_content',\n 'utm_term',\n 'fbclid',\n 'gclid',\n 'msclkid',\n '_ga',\n 'mc_cid',\n 'mc_eid',\n ];\n\n Array.from(params.keys())\n .sort()\n .forEach((key) => {\n if (!trackingParams.includes(key)) {\n filtered.set(key, params.get(key)!);\n }\n });\n\n parsed.search = filtered.toString();\n\n return parsed.toString();\n}\n\n/**\n * Generate hierarchical scope from URL for organized note storage\n *\n * @param url - The URL to generate scope from\n * @param baseScope - Base scope prefix (default: 'discovery/parser')\n * @returns Hierarchical scope string\n *\n * @example\n * generateScopeFromUrl('https://cityofboston.gov/meetings/minutes')\n * // Returns: 'discovery/parser/cityofboston.gov/meetings'\n */\nexport function generateScopeFromUrl(\n url: string,\n baseScope = 'discovery/parser',\n): string {\n const parsed = new URL(normalizeUrl(url));\n const domain = parsed.hostname;\n const pathParts = parsed.pathname.split('/').filter((p) => p);\n\n // Use first path segment as page type (e.g., 'meetings', 'documents')\n const pageType = pathParts[0] || 'index';\n\n return `${baseScope}/${domain}/${pageType}`;\n}\n\n/**\n * Hash page content for change detection\n * Uses SHA-256 to create a unique fingerprint of the HTML\n *\n * @param html - Page HTML content\n * @returns SHA-256 hash as hex string\n */\nexport function hashPageContent(html: string): string {\n return createHash('sha256').update(html).digest('hex');\n}\n","export * from './cli/index';\nexport * from './config/env-config';\nexport * from './shared/index';\nexport * from './web';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
package/metadata.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "name": "@happyvertical/utils",
3
3
  "path": "packages/utils",
4
4
  "position": {
5
- "index": 29,
6
- "count": 31
5
+ "index": 30,
6
+ "count": 32
7
7
  },
8
8
  "description": "Foundation utilities for ID generation, date parsing, URL handling, string conversion, error handling, and logging",
9
9
  "provides": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/utils",
3
- "version": "0.79.0",
3
+ "version": "0.80.1",
4
4
  "description": "Foundation utilities for ID generation, date parsing, URL handling, string conversion, error handling, and logging",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,10 +28,10 @@
28
28
  "devDependencies": {
29
29
  "@types/node": "25.0.10",
30
30
  "@types/pluralize": "^0.0.33",
31
- "typescript": "^5.9.3",
32
- "vite": "7.3.2",
31
+ "typescript": "5.9.3",
32
+ "vite": "8.1.4",
33
33
  "vite-plugin-dts": "4.5.4",
34
- "vitest": "^4.1.5"
34
+ "vitest": "4.1.10"
35
35
  },
36
36
  "bin": {
37
37
  "have-utils-context": "./dist/cli/claude-context.js"