UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

224 lines • 9.07 kB
/** * Which modules make up the app's sandbox kit: `components` — files, globs, directories or package * specifiers whose exports are the kit — or, without it, the first shadcn-style `components/ui` * folder the project has; and the `wrapper` module whose default export wraps the model's App. */ import { existsSync, readdirSync, statSync } from 'node:fs'; import { createRequire } from 'node:module'; import { isAbsolute, join, relative, resolve, sep } from 'node:path'; /** The options `genuiSandboxKit()` / `genuiKitHook()` took before 0.75, and what replaced each. */ const LEGACY_OPTIONS = { entry: '`entry: X` is now `components: X` (a module whose exports are the kit)', include: '`include: G` is now `components: G` (globs, files, directories or packages)', tailwind: '`tailwind` is gone: Tailwind is detected from `styles` (pass your Tailwind stylesheet there)', tailwindCss: '`tailwindCss: [...]` is gone: pass the stylesheet itself as `styles` — its @theme, @custom-variant and @utility (and those of the files it @imports) reach the frame on their own', css: "`css: G` is now `styles: G` (the app's style entry files)", }; /** Throws a migration message when options from before 0.75 are passed. */ export function assertNoLegacyKitOptions(options, where) { const found = Object.keys(LEGACY_OPTIONS).filter((key) => options[key] !== undefined); if (found.length === 0) return; throw new TypeError(`${where}: option${found.length > 1 ? 's' : ''} ${found.map((key) => `\`${key}\``).join(', ')} ${found.length > 1 ? 'were' : 'was'} removed in 0.75 — ${found.map((key) => LEGACY_OPTIONS[key]).join('; ')}. See "Migration from 0.74" in the sandbox kit docs.`); } /** Where a shadcn `components.json` puts `ui` by default, per framework layout. */ export const DEFAULT_KIT_DIRS = [ 'inertia/components/ui', 'resources/js/components/ui', 'src/components/ui', 'app/components/ui', 'components/ui', ]; /** A glob as a regular expression over `/`-separated paths: `**`, `*`, `?`, `{a,b}`. */ export function globToRegExp(glob) { let out = ''; for (let index = 0; index < glob.length; index++) { const c = glob[index]; if (c === '*') { if (glob[index + 1] === '*') { index++; if (glob[index + 1] === '/') { index++; out += '(?:.*/)?'; } else out += '.*'; } else out += '[^/]*'; } else if (c === '?') out += '[^/]'; else if (c === '{') { const close = glob.indexOf('}', index); if (close < 0) { out += '\\{'; continue; } out += `(?:${glob .slice(index + 1, close) .split(',') .map((part) => part.replace(/[.+^$()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')) .join('|')})`; index = close; } else out += c.replace(/[.+^$()|[\]\\]/g, '\\$&'); } return new RegExp(`^${out}$`); } /** The part of a glob before its first wildcard: where to start walking. */ function globBase(glob) { const parts = glob.split('/'); const base = []; for (const part of parts) { if (/[*?{]/.test(part)) break; base.push(part); } return base.join('/'); } function walk(dir, into, depth = 0) { if (depth > 12 || !existsSync(dir)) return; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; const path = join(dir, entry.name); if (entry.isDirectory()) walk(path, into, depth + 1); else into.push(path); } } /** The files matching `globs` under `root`, sorted, absolute. Test and story files are left out. */ export function globFiles(root, globs) { const list = typeof globs === 'string' ? [globs] : [...globs]; const found = new Set(); for (const glob of list) { const normalized = glob.replace(/\\/g, '/').replace(/^\.\//, ''); const base = globBase(normalized); const pattern = globToRegExp(normalized); const files = []; const start = resolve(root, base); if (existsSync(start) && statSync(start).isFile()) files.push(start); else walk(start, files); for (const file of files) { const rel = relative(root, file).split(sep).join('/'); if (pattern.test(rel) && !/\.(test|spec|stories)\.[jt]sx?$/.test(rel) && !rel.endsWith('.d.ts')) found.add(file); } } return [...found].sort(); } const SOURCE_GLOB = '*.{tsx,jsx,ts,js,mts,mjs}'; function isPackageSpecifier(spec) { return !spec.startsWith('.') && !isAbsolute(spec) && /^(@[\w.-]+\/)?[\w.-]+(\/.*)?$/.test(spec); } function resolvesAsPackage(root, spec) { try { createRequire(resolve(root, 'package.json')).resolve(spec); return true; } catch { // An ESM-only package (no `require` condition) still resolves its package.json directory. const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]; let dir = root; for (;;) { if (existsSync(join(dir, 'node_modules', name, 'package.json'))) return true; const parent = resolve(dir, '..'); if (parent === dir) return false; dir = parent; } } } /** A module path (with or without extension), resolved like an import would be. */ function resolveModuleFile(path) { if (existsSync(path) && statSync(path).isFile()) return path; for (const extension of ['.tsx', '.ts', '.jsx', '.js', '.mts', '.mjs']) { if (existsSync(`${path}${extension}`)) return `${path}${extension}`; } for (const extension of ['.tsx', '.ts', '.jsx', '.js']) { const index = join(path, `index${extension}`); if (existsSync(index)) return index; } return undefined; } /** The wrapper: a file (relative to the root) or a package specifier. Throws when it is neither. */ export function resolveSandboxKitWrapper(root, wrapper) { if (wrapper === undefined) return undefined; const local = resolveModuleFile(isAbsolute(wrapper) ? wrapper : resolve(root, wrapper)); if (local !== undefined) return local; if (isPackageSpecifier(wrapper) && resolvesAsPackage(root, wrapper)) return wrapper; throw new Error(`genuiSandboxKit: the wrapper "${wrapper}" was not found (a file relative to ${root}, or a package)`); } /** * Resolve the kit's modules. `null` when there is nothing to build — no `components` match, none of * the {@link DEFAULT_KIT_DIRS} exists, and there is no `wrapper`. */ export function resolveSandboxKitFiles(root, options) { const wrapper = resolveSandboxKitWrapper(root, options.wrapper); const specs = options.components === undefined ? DEFAULT_KIT_DIRS.filter((dir) => existsSync(resolve(root, dir))).slice(0, 1) : typeof options.components === 'string' ? [options.components] : [...options.components]; const files = new Set(); const packages = []; const dirs = new Set(); const missing = []; for (const spec of specs) { const normalized = spec.replace(/\\/g, '/'); if (/[*?{]/.test(normalized)) { const matched = globFiles(root, normalized); for (const file of matched) files.add(file); dirs.add(resolve(root, globBase(normalized))); if (matched.length === 0) missing.push(spec); continue; } const path = isAbsolute(normalized) ? normalized : resolve(root, normalized); if (existsSync(path) && statSync(path).isDirectory()) { for (const file of globFiles(path, SOURCE_GLOB)) files.add(file); dirs.add(path); continue; } const file = resolveModuleFile(path); if (file !== undefined) { files.add(file); dirs.add(resolve(file, '..')); continue; } if (isPackageSpecifier(normalized) && resolvesAsPackage(root, normalized)) { packages.push(normalized); continue; } missing.push(spec); } if (files.size === 0 && packages.length === 0 && wrapper === undefined) return null; if (wrapper !== undefined && isAbsolute(wrapper)) dirs.add(resolve(wrapper, '..')); return { files: [...files].sort(), packages, ...(wrapper !== undefined ? { wrapper } : {}), dirs: [...dirs], ...(missing.length > 0 ? { missing } : {}), }; } //# sourceMappingURL=files.js.map