diff --git a/AGENTS.md b/AGENTS.md index bd55e06..151601e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,4 +59,34 @@ if (container) { python jules-scratch/verification/verify_introspection_tool.py ``` -By following these instructions, you can avoid the common pitfalls of this unique environment and ensure that your contributions are successful. \ No newline at end of file +By following these instructions, you can avoid the common pitfalls of this unique environment and ensure that your contributions are successful. + +## Development Tools + +To streamline the development process, a suite of tools has been created in the `tools` directory. You should use these tools to manage components and dependencies. + +### Component Manager + +Use the `component-manager.js` script to create and manage components. + +* **To create a new component:** + ```bash + node tools/component-manager.js create-component + ``` + This will create a new, self-rendering React component in the `src` directory with the correct boilerplate. + +* **To set the active component for testing:** + ```bash + node tools/component-manager.js set-active-component + ``` + This will automatically update `index.html` to load the specified component. + +### Dependency Manager + +The `dependency-manager.js` script manages the project's CDN dependencies. + +* **To inject/update dependencies:** + ```bash + node tools/dependency-manager.js + ``` + This script reads the `dependencies.json` file and injects the required ` - - + + +
- + \ No newline at end of file diff --git a/src/JulesIntrospector.jsx b/src/JulesIntrospector.jsx new file mode 100644 index 0000000..c552ab0 --- /dev/null +++ b/src/JulesIntrospector.jsx @@ -0,0 +1,193 @@ +const { useState, useCallback, useEffect, useRef } = React; + +// --- Helper Components --- +const StatusIcon = ({ status }) => { + const iconMap = { + pass: , + fail: , + warn: , + info: , + loading: + }; + return iconMap[status] || null; +}; + +const ProfileCard = ({ title, status, children, learnMoreUrl }) => { + const statusClasses = { pass: 'border-green-500/50 bg-green-500/10', fail: 'border-red-500/50 bg-red-500/10', warn: 'border-yellow-500/50 bg-yellow-500/10', info: 'border-blue-500/50 bg-blue-500/10', loading: 'border-gray-500/50 bg-gray-500/10 animate-pulse' }; + return

{title}

{children}
{learnMoreUrl && Learn more →}
; +}; + +const FeasibleAlternativesReport = () => ( +
+

Feasible Development Alternatives Report

+

This report outlines what is and isn't possible in the current browser environment.

+
+
+

Category 1: Feasible

+
    +
  • Single-Page Applications (SPAs)
  • +
  • Offline-Capable Apps & PWAs
  • +
  • Client-Side Tools (WASM)
  • +
  • 2D/3D Graphics & Visualizations (WebGL)
  • +
  • Local Single-Threaded AI/ML
  • +
+
+
+

Category 2: Infeasible

+

The critical limitation is that this environment is not cross-origin isolated, which disables `SharedArrayBuffer`.

+
    +
  • In-Browser Node.js (npm)
  • +
  • In-Browser Virtual Machines
  • +
  • Multi-Threaded AI/ML
  • +
+
+
+
+); + +// --- OS Profiler Component --- +function OSProfiler() { + const [results, setResults] = useState({ crossOriginIsolated: { status: 'loading' }, sharedArrayBuffer: { status: 'loading' }, webAssembly: { status: 'loading' }, isSecureContext: { status: 'loading' }, webGL: { status: 'loading' }, webSocket: { status: 'loading' }, indexedDB: { status: 'loading' }, serviceWorker: { status: 'loading' }, userAgent: { status: 'info', message: navigator.userAgent } }); + const [overallVerdict, setOverallVerdict] = useState({ status: 'loading', message: 'Awaiting critical checks...' }); + const [showAlternatives, setShowAlternatives] = useState(false); + + useEffect(() => { + const newResults = {}; + const isIsolated = window.crossOriginIsolated === true; + newResults.crossOriginIsolated = isIsolated ? { status: 'pass', message: 'Environment is cross-origin isolated.' } : { status: 'fail', message: 'Environment is NOT cross-origin isolated.' }; + const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined'; + newResults.sharedArrayBuffer = hasSharedArrayBuffer ? { status: 'pass', message: 'SharedArrayBuffer is available.' } : { status: 'fail', message: 'SharedArrayBuffer is not available.' }; + setOverallVerdict(isIsolated && hasSharedArrayBuffer ? { status: 'pass', message: 'This environment can run a VM or WebContainer.' } : { status: 'fail', message: 'This environment cannot run a VM or WebContainer.' }); + newResults.webAssembly = (typeof WebAssembly === 'object') ? { status: 'pass', message: 'WebAssembly is supported.' } : { status: 'fail', message: 'WebAssembly is not supported.' }; + newResults.isSecureContext = window.isSecureContext ? { status: 'pass', message: 'Page is in a secure context (HTTPS).' } : { status: 'warn', message: 'Page is not in a secure context.' }; + try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); newResults.webGL = gl ? { status: 'pass', message: 'WebGL is supported.' } : { status: 'warn', message: 'WebGL is not supported.' }; } catch (e) { newResults.webGL = { status: 'fail', message: 'Could not check for WebGL support.' }; } + try { const ws = new WebSocket('wss://echo.websocket.org/'); ws.onopen = () => { setResults(prev => ({ ...prev, webSocket: { status: 'pass', message: 'WebSocket connections allowed.' } })); ws.close(); }; ws.onerror = () => setResults(prev => ({ ...prev, webSocket: { status: 'warn', message: 'WebSocket connections may be blocked.' } })); } catch (e) { newResults.webSocket = { status: 'fail', message: 'Failed to init WebSocket.' }; } + newResults.indexedDB = ('indexedDB' in window) ? { status: 'pass', message: 'IndexedDB is available.' } : { status: 'warn', message: 'IndexedDB is not available.' }; + newResults.serviceWorker = ('serviceWorker' in navigator) ? { status: 'pass', message: 'Service Workers are supported.' } : { status: 'warn', message: 'Service Workers are not supported.' }; + setResults(prev => ({ ...prev, ...newResults })); + }, []); + + return ( +
+
+

OS Environment Profile

+
+
{overallVerdict.status === 'pass' ? '✅' : '❌'}

Overall Assessment: {overallVerdict.status === 'pass' ? 'Go' : 'No-Go'}

{overallVerdict.message}

+
+ {overallVerdict.status !== 'loading' &&

What does this result mean?

} + {showAlternatives && } +
+

Critical VM Requirements

{results.crossOriginIsolated.message}

{results.crossOriginIsolated.status === 'fail' && (

To fix this, serve your page with these HTTP headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
)}

{results.sharedArrayBuffer.message}

+

Core Browser Capabilities

{results.isSecureContext.message}

{results.webAssembly.message}

{results.webGL.message}

{results.webSocket.message}

{results.indexedDB.message}

{results.serviceWorker.message}

+

Environment Information

{results.userAgent.message}

+
+
+
+ ); +} + +// --- CLI Component from GeminiAppCanvasCLI --- +function CLI({ appRootRef }) { + const [history, setHistory] = useState([{ command: 'welcome', output: 'CLI initialized. Type `help` for a list of commands.' }]); + const [command, setCommand] = useState(''); + const terminalRef = useRef(null); + + const runTest = async (title, testFn) => { try { const data = await testFn(); const icon = data.toString().includes('denied') || data.toString().includes('blocked') ? 'ℹ️' : '✅'; return `${icon} ${title.padEnd(25, ' ')} ${data}`; } catch (error) { return `❌ ${title.padEnd(25, ' ')} Failed. Reason: ${error.message}`; } }; + const runComprehensiveProbe = async () => { let output = "🔬 Running Comprehensive Environment Probe...\n"; const categories = { "Storage & Quotas": [ runTest('Cookies', () => Promise.resolve(navigator.cookieEnabled ? 'Enabled' : 'Disabled')), runTest('LocalStorage', () => { localStorage.setItem('probe', 'test'); localStorage.removeItem('probe'); return Promise.resolve('Writable'); }), runTest('SessionStorage', () => { sessionStorage.setItem('probe', 'test'); sessionStorage.removeItem('probe'); return Promise.resolve('Writable'); }), runTest('IndexedDB', () => new Promise((resolve, reject) => { const request = indexedDB.open('ProbeDB'); request.onerror = () => reject(new Error('Open failed')); request.onsuccess = () => { request.result.close(); indexedDB.deleteDatabase('ProbeDB'); resolve('Accessible'); }; })), runTest('Storage Quota', async () => { if (!navigator.storage || !navigator.storage.estimate) throw new Error('StorageManager API not supported.'); const quota = await navigator.storage.estimate(); return `${(quota.usage / 1024 / 1024).toFixed(2)}MB used of ${(quota.quota / 1024 / 1024).toFixed(2)}MB quota`; }), ], "Modern & Media APIs": [ runTest('Geolocation', () => new Promise((resolve, reject) => { if (!navigator.geolocation) reject(new Error('API not available.')); const timeout = setTimeout(() => reject(new Error('Permission denied or timed out.')), 5000); navigator.geolocation.getCurrentPosition( (pos) => { clearTimeout(timeout); resolve(`Lat/Lon received.`); }, (err) => { clearTimeout(timeout); reject(new Error(`Error: ${err.message}`)); } ); })), runTest('Web Audio API', () => { const context = new (window.AudioContext || window.webkitAudioContext)(); if (!context) throw new Error('Not supported.'); context.close(); return Promise.resolve('AudioContext created.'); }), runTest('Payment Request API', () => Promise.resolve(window.PaymentRequest ? 'Available' : 'Not supported.')), runTest('Web Share API', () => Promise.resolve(navigator.share ? 'Available' : 'Not supported.')), ], "Networking & Permissions": [ runTest('Fetch (Egress)', () => fetch('https://httpbin.org/get').then(res => `Request OK (${res.status})`)), runTest('WebSockets', () => new Promise((resolve, reject) => { const ws = new WebSocket('wss://socketsbay.com/wss/v2/1/demo/'); const timeout = setTimeout(() => { ws.close(); reject(new Error('Connection timed out')); }, 2000); ws.onopen = () => { clearTimeout(timeout); ws.close(); resolve('Connection allowed'); }; ws.onerror = () => { clearTimeout(timeout); reject(new Error('Connection failed or blocked')); }; })), runTest('Permissions(Clipboard)', () => navigator.permissions.query({ name: 'clipboard-write' }).then(p => `Clipboard-write state: ${p.state}`)), ], }; for (const category in categories) { output += `\n--- ${category} ---\n`; const results = await Promise.all(categories[category]); output += results.join('\n'); } return output.trim(); }; + const formatReactValue = (value, depth = 0, maxDepth = 3) => { if (depth > maxDepth) return '[...]'; if (value === null) return 'null'; if (typeof value === 'string') return `"${value}"`; if (typeof value !== 'object') return value.toString(); if (Array.isArray(value)) return `[Array(${value.length})]`; if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`; if (value.$$typeof === Symbol.for('react.element')) return `<${value.type.name || 'Component'} />`; const constructorName = value.constructor ? value.constructor.name : 'Object'; if (constructorName !== 'Object') return `[Object: ${constructorName}]`; return JSON.stringify(value); }; + const inspectFiberNode = (fiber, depth = 0, maxDepth = 10) => { if (!fiber || depth > maxDepth) return ''; const indent = ' '.repeat(depth); let output = `${indent}└── <${fiber.type ? (fiber.type.name || 'Component') : 'Unknown'}>\n`; if (fiber.child) output += inspectFiberNode(fiber.child, depth + 1, maxDepth); if (fiber.sibling) output += inspectFiberNode(fiber.sibling, depth, maxDepth); return output; }; + const advancedFiberTree = (rootElement) => { if (!rootElement) return "Error: App root not found."; const fiberKey = Object.keys(rootElement).find(key => key.startsWith('__reactFiber$')); if (!fiberKey) return "Error: Could not find React Fiber key."; const rootFiber = rootElement[fiberKey]; if (!rootFiber) return "Error: Could not access root Fiber node."; return inspectFiberNode(rootFiber).trim(); }; + const getGlobals = (filter) => Object.keys(window).filter(k => !filter || k.toLowerCase().includes(filter.toLowerCase())).join('\n'); + const inspectPath = (path) => { if(!path) return "Usage: inspect "; try { let v = path.split('.').reduce((o,k)=>o[k], window); return formatReactValue(v, 0, 10); } catch (e) { return e.message; }}; + const executeCode = (code) => { try { return formatReactValue(new Function(`return ${code}`)(), 0, 10); } catch (e) { return e.message; }}; + + const processCommand = async (cmd) => { + const [mainCommand, ...args] = cmd.trim().split(' '); + let output; + const commandMap = { + help: () => `Available commands:\n- help: Show this message.\n- probe: Run sandbox scan.\n- clear: Clear screen.\n- fibertree: Inspect React component tree.\n- globals [filter]: List global properties.\n- inspect : Inspect a JS object.\n- eval : Execute JS code.`, + clear: () => { setHistory([]); return null; }, + probe: runComprehensiveProbe, + fibertree: () => advancedFiberTree(appRootRef.current), + globals: () => getGlobals(args[0]), + inspect: () => inspectPath(args[0]), + eval: () => executeCode(args.join(' ')), + }; + try { const result = await Promise.resolve(commandMap[mainCommand.toLowerCase()]()); if (result !== null) output = result; else return; } catch (e) { output = `Command not found or error: ${mainCommand}`; } + setHistory(h => [...h, { command: cmd, output }]); + }; + + useEffect(() => { terminalRef.current?.scrollTo(0, terminalRef.current.scrollHeight); }, [history]); + + return ( +
+
document.getElementById('cli-input')?.focus()}> + {history.map((entry, index) => ( +
+
>{entry.command}
+
{entry.output}
+
+ ))} +
+ > + setCommand(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); processCommand(command); setCommand(''); } }} className="bg-transparent border-none text-white focus:outline-none w-full" autoFocus autoComplete="off" /> +
+
+
+ ); +} + + +// --- Main Introspector Component --- +function JulesIntrospector() { + const [jsCode, setJsCode] = useState(''); + const [introspectionResult, setIntrospectionResult] = useState(null); + const [activeTab, setActiveTab] = useState('introspect'); + const appVersion = "3.0.0"; + const appRootRef = useRef(null); + + const handleCodeChange = (event) => { setJsCode(event.target.value); setIntrospectionResult(null); }; + const introspectCode = () => { const functions = [], variables = []; try { if (typeof acorn === 'undefined' || typeof estraverse === 'undefined') throw new Error("AST parsing libraries not available."); const ast = acorn.parse(jsCode, { ecmaVersion: 2020, sourceType: 'module', allowHashBang: true }); estraverse.traverse(ast, { enter: function (node) { if (node.type === 'FunctionDeclaration' && node.id) functions.push(node.id.name); else if (node.type === 'VariableDeclarator' && node.id) { if (node.init && (node.init.type === 'FunctionExpression' || node.init.type === 'ArrowFunctionExpression')) functions.push(node.id.name); else variables.push(node.id.name); } else if (node.type === 'ClassDeclaration' && node.id) functions.push(node.id.name); } }); setIntrospectionResult({ functions, variables }); } catch (error) { setIntrospectionResult({ functions: [], variables: [], error: `Failed to parse: ${error.message}` }); } }; + const copyResultsToClipboard = useCallback(() => { if (!introspectionResult) return; const resultsText = JSON.stringify(introspectionResult, null, 2); navigator.clipboard.writeText(resultsText).then(() => alert('Copied!'), () => alert('Failed to copy.')); }, [introspectionResult]); + const downloadResults = useCallback(() => { if (!introspectionResult) return; const filename = `js-introspector-results.json`; const blob = new Blob([JSON.stringify(introspectionResult, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }, [introspectionResult]); + + return ( +
+

Jules' Comprehensive Introspector

+

Version {appVersion}

+ +
+ + + +
+ +
+ {activeTab === 'introspect' && ( +
+ + + {introspectionResult && ( +
+

Results

+ {introspectionResult.error &&
Error:{introspectionResult.error}
} +

Functions ({introspectionResult.functions.length})

{introspectionResult.functions.length > 0 ?
    {introspectionResult.functions.map((func, i) =>
  • {func}
  • )}
:

None identified.

}
+

Variables ({introspectionResult.variables.length})

{introspectionResult.variables.length > 0 ?
    {introspectionResult.variables.map((v, i) =>
  • {v}
  • )}
:

None identified.

}
+
+
+ )} +
+ )} + {activeTab === 'os_profile' && } + {activeTab === 'cli' && } +
+
+ ); +} + +// --- Self-Rendering Logic --- +const container = document.getElementById('root'); +if (container) { + const root = ReactDOM.createRoot(container); + root.render(React.createElement(JulesIntrospector)); +} \ No newline at end of file diff --git a/src/TestComponent.jsx b/src/TestComponent.jsx new file mode 100644 index 0000000..3f168d0 --- /dev/null +++ b/src/TestComponent.jsx @@ -0,0 +1,16 @@ + +function TestComponent() { + const { useState, useEffect } = React; + return ( +
+

TestComponent Component

+
+ ); +} + +// --- Self-Rendering Logic --- +const container = document.getElementById('root'); +if (container) { + const root = ReactDOM.createRoot(container); + root.render(React.createElement(TestComponent)); +} diff --git a/tools/component-manager.js b/tools/component-manager.js new file mode 100644 index 0000000..d483edd --- /dev/null +++ b/tools/component-manager.js @@ -0,0 +1,67 @@ +const fs = require('fs'); +const path = require('path'); + +const command = process.argv[2]; +const componentName = process.argv[3]; + +const SRC_DIR = 'src'; +const INDEX_HTML_PATH = 'index.html'; + +const componentTemplate = (name) => ` +function ${name}() { + const { useState, useEffect } = React; + return ( +
+

${name} Component

+
+ ); +} + +// --- Self-Rendering Logic --- +const container = document.getElementById('root'); +if (container) { + const root = ReactDOM.createRoot(container); + root.render(React.createElement(${name})); +} +`; + +function createComponent(name) { + if (!name) { + console.error('Error: Component name is required.'); + process.exit(1); + } + const componentPath = path.join(SRC_DIR, `${name}.jsx`); + if (fs.existsSync(componentPath)) { + console.error(`Error: Component ${name} already exists.`); + process.exit(1); + } + fs.writeFileSync(componentPath, componentTemplate(name)); + console.log(`Component ${name} created at ${componentPath}`); +} + +function setActiveComponent(name) { + if (!name) { + console.error('Error: Component name is required.'); + process.exit(1); + } + const componentPath = path.join(SRC_DIR, `${name}.jsx`); + if (!fs.existsSync(componentPath)) { + console.error(`Error: Component ${name} does not exist.`); + process.exit(1); + } + + let indexHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8'); + indexHtml = indexHtml.replace(/src=".*\.jsx"/, `src="${componentPath}"`); + fs.writeFileSync(INDEX_HTML_PATH, indexHtml); + console.log(`Active component set to ${name}`); +} + +if (command === 'create-component') { + createComponent(componentName); +} else if (command === 'set-active-component') { + setActiveComponent(componentName); +} else { + console.log('Usage:'); + console.log(' node tools/component-manager.js create-component '); + console.log(' node tools/component-manager.js set-active-component '); +} \ No newline at end of file diff --git a/tools/dependency-manager.js b/tools/dependency-manager.js new file mode 100644 index 0000000..ac06a44 --- /dev/null +++ b/tools/dependency-manager.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +const DEPENDENCIES_PATH = 'dependencies.json'; +const INDEX_HTML_PATH = 'index.html'; +const DEPENDENCY_PLACEHOLDER = ''; + +function injectDependencies() { + const dependencies = JSON.parse(fs.readFileSync(DEPENDENCIES_PATH, 'utf8')); + let indexHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8'); + + // Remove existing dependency scripts to ensure idempotency + const dependencyUrls = Object.values(dependencies); + dependencyUrls.forEach(url => { + const scriptTagRegex = new RegExp(`\\n?`, 'g'); + indexHtml = indexHtml.replace(scriptTagRegex, ''); + }); + + const scriptTags = dependencyUrls + .map(url => ``) + .join('\n '); + + if (indexHtml.includes(DEPENDENCY_PLACEHOLDER)) { + indexHtml = indexHtml.replace(DEPENDENCY_PLACEHOLDER, scriptTags); + } else { + // If the placeholder is not found, inject before the closing tag + indexHtml = indexHtml.replace('', ` ${scriptTags}\n`); + } + + fs.writeFileSync(INDEX_HTML_PATH, indexHtml); + console.log('Dependencies injected into index.html'); +} + +injectDependencies(); \ No newline at end of file