Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,888 changes: 1,774 additions & 114 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"devDependencies": {
"typescript": "^5.4.0",
"vite": "^5.2.0",
"vite-plugin-monaco-editor": "^1.1.0"
"vite-plugin-monaco-editor": "^1.1.0",
"vitest": "^4.1.0"
}
}
122 changes: 122 additions & 0 deletions src/openclaw/stubs-behavior.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it, expect } from 'vitest';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);

// ─── sharp ───────────────────────────────────────────────────────────────────

describe('sharp stub', () => {
const sharp = require(path.join(__dirname, 'stubs/sharp/index.cjs'));

it('exports a callable function', () => {
expect(typeof sharp).toBe('function');
});

it('returns a chainable builder', () => {
const builder = sharp(Buffer.alloc(0));
expect(builder.resize()).toBe(builder);
expect(builder.jpeg()).toBe(builder);
expect(builder.png()).toBe(builder);
});

it('toBuffer resolves to an empty Buffer', async () => {
const buf = await sharp().resize().toBuffer();
expect(Buffer.isBuffer(buf)).toBe(true);
expect(buf.length).toBe(0);
});

it('metadata includes hasAlpha and channels', async () => {
const meta = await sharp().metadata();
expect(meta).toHaveProperty('hasAlpha', false);
expect(meta).toHaveProperty('channels', 0);
expect(meta).toHaveProperty('width', 0);
expect(meta).toHaveProperty('height', 0);
});

it('has static methods', () => {
expect(typeof sharp.cache).toBe('function');
expect(typeof sharp.concurrency).toBe('function');
});
});

// ─── @lydell/node-pty ────────────────────────────────────────────────────────

describe('node-pty stub', () => {
const pty = require(path.join(__dirname, 'stubs/lydell-node-pty/index.cjs'));

it('exports spawn function', () => {
expect(typeof pty.spawn).toBe('function');
});

it('spawn throws with descriptive error', () => {
expect(() => pty.spawn('bash', [])).toThrow(/unavailable/i);
});
});

// ─── playwright-core ─────────────────────────────────────────────────────────

describe('playwright-core stub', () => {
const pw = require(path.join(__dirname, 'stubs/playwright-core/index.cjs'));

it('exports browser types', () => {
expect(pw.chromium).toBeDefined();
expect(pw.firefox).toBeDefined();
expect(pw.webkit).toBeDefined();
});

it('chromium.launch rejects with descriptive error', async () => {
await expect(pw.chromium.launch()).rejects.toThrow(/unavailable/i);
});

it('exports devices as empty object', () => {
expect(pw.devices).toEqual({});
});

it('exports TimeoutError class', () => {
expect(pw.errors.TimeoutError).toBeDefined();
expect(new pw.errors.TimeoutError()).toBeInstanceOf(Error);
});
});

// ─── sqlite-vec ──────────────────────────────────────────────────────────────

describe('sqlite-vec stub', () => {
const sv = require(path.join(__dirname, 'stubs/sqlite-vec/index.cjs'));

it('exports load as no-op', () => {
expect(typeof sv.load).toBe('function');
expect(sv.load()).toBeUndefined();
});

it('exports getLoadablePath returning empty string', () => {
expect(sv.getLoadablePath()).toBe('');
});
});

// ─── node-llama-cpp ──────────────────────────────────────────────────────────

describe('node-llama-cpp stub', () => {
const llama = require(path.join(__dirname, 'stubs/node-llama-cpp/index.cjs'));

it('getLlama rejects with descriptive error', async () => {
await expect(llama.getLlama()).rejects.toThrow(/unavailable/i);
});

it('LlamaModel constructor throws', () => {
expect(() => new llama.LlamaModel()).toThrow(/unavailable/i);
});

it('exports resolveModelFile', async () => {
expect(typeof llama.resolveModelFile).toBe('function');
await expect(llama.resolveModelFile()).rejects.toThrow(/unavailable/i);
});

it('exports LlamaLogLevel with expected levels', () => {
expect(llama.LlamaLogLevel).toBeDefined();
expect(typeof llama.LlamaLogLevel.error).toBe('number');
expect(typeof llama.LlamaLogLevel.warn).toBe('number');
});
});
59 changes: 59 additions & 0 deletions src/openclaw/stubs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest';
import { STUB_FILES, INSTALL_STUBS_SCRIPT } from './stubs.js';

const PACKAGES = ['sharp', 'lydell-node-pty', 'playwright-core', 'sqlite-vec', 'node-llama-cpp'];

describe('STUB_FILES', () => {
it('has exactly 15 entries (5 packages x 3 files)', () => {
expect(Object.keys(STUB_FILES)).toHaveLength(15);
});

it.each(PACKAGES)('%s has package.json, index.cjs, index.mjs', (pkg) => {
const prefix = `.openclaw-stubs/${pkg}`;
expect(STUB_FILES[`${prefix}/package.json`]).toBeDefined();
expect(STUB_FILES[`${prefix}/index.cjs`]).toBeDefined();
expect(STUB_FILES[`${prefix}/index.mjs`]).toBeDefined();
});

it.each(PACKAGES)('%s package.json has dual exports', (pkg) => {
const parsed = JSON.parse(STUB_FILES[`.openclaw-stubs/${pkg}/package.json`]);
expect(parsed.exports['.']).toHaveProperty('import');
expect(parsed.exports['.']).toHaveProperty('require');
expect(parsed.main).toBe('./index.cjs');
});

it.each(PACKAGES)('%s index.cjs uses explicit module.exports', (pkg) => {
const cjs = STUB_FILES[`.openclaw-stubs/${pkg}/index.cjs`];
expect(cjs).toContain('module.exports');
expect(cjs).toContain('__esModule');
});

it.each(PACKAGES)('%s index.mjs re-exports from cjs', (pkg) => {
const mjs = STUB_FILES[`.openclaw-stubs/${pkg}/index.mjs`];
expect(mjs).toContain("from './index.cjs'");
expect(mjs).toContain('export');
});
});

describe('INSTALL_STUBS_SCRIPT', () => {
it('copies all 5 packages to node_modules', () => {
expect(INSTALL_STUBS_SCRIPT).toContain('cp -r workspace/.openclaw-stubs/sharp node_modules/sharp');
expect(INSTALL_STUBS_SCRIPT).toContain('cp -r workspace/.openclaw-stubs/playwright-core node_modules/playwright-core');
expect(INSTALL_STUBS_SCRIPT).toContain('cp -r workspace/.openclaw-stubs/sqlite-vec node_modules/sqlite-vec');
expect(INSTALL_STUBS_SCRIPT).toContain('cp -r workspace/.openclaw-stubs/node-llama-cpp node_modules/node-llama-cpp');
expect(INSTALL_STUBS_SCRIPT).toContain('cp -r workspace/.openclaw-stubs/lydell-node-pty node_modules/@lydell/node-pty');
});

it('creates @lydell scope dir', () => {
expect(INSTALL_STUBS_SCRIPT).toContain('mkdir -p node_modules/@lydell');
});

it('guards against missing node_modules', () => {
expect(INSTALL_STUBS_SCRIPT).toContain('[ -d node_modules ]');
});

it('removes existing packages before copying', () => {
expect(INSTALL_STUBS_SCRIPT).toContain('rm -rf node_modules/sharp');
expect(INSTALL_STUBS_SCRIPT).toContain('rm -rf node_modules/@lydell/node-pty');
});
});
48 changes: 48 additions & 0 deletions src/openclaw/stubs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SHARP_PKG from './stubs/sharp/package.json?raw';
import SHARP_CJS from './stubs/sharp/index.cjs?raw';
import SHARP_MJS from './stubs/sharp/index.mjs?raw';
import PTY_PKG from './stubs/lydell-node-pty/package.json?raw';
import PTY_CJS from './stubs/lydell-node-pty/index.cjs?raw';
import PTY_MJS from './stubs/lydell-node-pty/index.mjs?raw';
import PW_PKG from './stubs/playwright-core/package.json?raw';
import PW_CJS from './stubs/playwright-core/index.cjs?raw';
import PW_MJS from './stubs/playwright-core/index.mjs?raw';
import SQLVEC_PKG from './stubs/sqlite-vec/package.json?raw';
import SQLVEC_CJS from './stubs/sqlite-vec/index.cjs?raw';
import SQLVEC_MJS from './stubs/sqlite-vec/index.mjs?raw';
import LLAMA_PKG from './stubs/node-llama-cpp/package.json?raw';
import LLAMA_CJS from './stubs/node-llama-cpp/index.cjs?raw';
import LLAMA_MJS from './stubs/node-llama-cpp/index.mjs?raw';

/** Workspace files to mount under `.openclaw-stubs/`. */
export const STUB_FILES: Record<string, string> = {
'.openclaw-stubs/sharp/package.json': SHARP_PKG,
'.openclaw-stubs/sharp/index.cjs': SHARP_CJS,
'.openclaw-stubs/sharp/index.mjs': SHARP_MJS,
'.openclaw-stubs/lydell-node-pty/package.json': PTY_PKG,
'.openclaw-stubs/lydell-node-pty/index.cjs': PTY_CJS,
'.openclaw-stubs/lydell-node-pty/index.mjs': PTY_MJS,
'.openclaw-stubs/playwright-core/package.json': PW_PKG,
'.openclaw-stubs/playwright-core/index.cjs': PW_CJS,
'.openclaw-stubs/playwright-core/index.mjs': PW_MJS,
'.openclaw-stubs/sqlite-vec/package.json': SQLVEC_PKG,
'.openclaw-stubs/sqlite-vec/index.cjs': SQLVEC_CJS,
'.openclaw-stubs/sqlite-vec/index.mjs': SQLVEC_MJS,
'.openclaw-stubs/node-llama-cpp/package.json': LLAMA_PKG,
'.openclaw-stubs/node-llama-cpp/index.cjs': LLAMA_CJS,
'.openclaw-stubs/node-llama-cpp/index.mjs': LLAMA_MJS,
};

/** Shell script to copy stubs into node_modules/ after npm install. */
export const INSTALL_STUBS_SCRIPT = `
echo "[ClawLess] Installing native dependency stubs..."
cd ..
[ -d node_modules ] || { echo "[ClawLess] ERROR: node_modules not found"; exit 1; }
rm -rf node_modules/sharp && cp -r workspace/.openclaw-stubs/sharp node_modules/sharp
mkdir -p node_modules/@lydell && rm -rf node_modules/@lydell/node-pty && cp -r workspace/.openclaw-stubs/lydell-node-pty node_modules/@lydell/node-pty
rm -rf node_modules/playwright-core && cp -r workspace/.openclaw-stubs/playwright-core node_modules/playwright-core
rm -rf node_modules/sqlite-vec && cp -r workspace/.openclaw-stubs/sqlite-vec node_modules/sqlite-vec
rm -rf node_modules/node-llama-cpp && cp -r workspace/.openclaw-stubs/node-llama-cpp node_modules/node-llama-cpp
cd workspace
echo "[ClawLess] Stubs installed."
`;
5 changes: 5 additions & 0 deletions src/openclaw/stubs/lydell-node-pty/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';
function spawn() { throw new Error('node-pty unavailable in WebContainer'); }
module.exports.spawn = spawn;
module.exports.default = { spawn };
module.exports.__esModule = true;
3 changes: 3 additions & 0 deletions src/openclaw/stubs/lydell-node-pty/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pty from './index.cjs';
export default pty.default;
export const spawn = pty.spawn;
7 changes: 7 additions & 0 deletions src/openclaw/stubs/lydell-node-pty/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@lydell/node-pty",
"version": "1.2.0-beta.3",
"main": "./index.cjs",
"exports": { ".": { "import": "./index.mjs", "require": "./index.cjs" } },
"type": "commonjs"
}
16 changes: 16 additions & 0 deletions src/openclaw/stubs/node-llama-cpp/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';
const ERR = 'node-llama-cpp unavailable in WebContainer';
async function getLlama() { throw new Error(ERR); }
class LlamaModel { constructor() { throw new Error(ERR); } }
class LlamaContext { constructor() { throw new Error(ERR); } }
class LlamaChatSession { constructor() { throw new Error(ERR); } }
async function resolveModelFile() { throw new Error(ERR); }
const LlamaLogLevel = { error: 0, warn: 1, info: 2, debug: 3 };
module.exports.getLlama = getLlama;
module.exports.LlamaModel = LlamaModel;
module.exports.LlamaContext = LlamaContext;
module.exports.LlamaChatSession = LlamaChatSession;
module.exports.resolveModelFile = resolveModelFile;
module.exports.LlamaLogLevel = LlamaLogLevel;
module.exports.default = { getLlama, LlamaModel, LlamaContext, LlamaChatSession, resolveModelFile, LlamaLogLevel };
module.exports.__esModule = true;
8 changes: 8 additions & 0 deletions src/openclaw/stubs/node-llama-cpp/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import llama from './index.cjs';
export default llama.default;
export const getLlama = llama.getLlama;
export const LlamaModel = llama.LlamaModel;
export const LlamaContext = llama.LlamaContext;
export const LlamaChatSession = llama.LlamaChatSession;
export const resolveModelFile = llama.resolveModelFile;
export const LlamaLogLevel = llama.LlamaLogLevel;
7 changes: 7 additions & 0 deletions src/openclaw/stubs/node-llama-cpp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "node-llama-cpp",
"version": "3.16.2",
"main": "./index.cjs",
"exports": { ".": { "import": "./index.mjs", "require": "./index.cjs" } },
"type": "commonjs"
}
14 changes: 14 additions & 0 deletions src/openclaw/stubs/playwright-core/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';
const ERR = () => Promise.reject(new Error('playwright unavailable in WebContainer'));
const stub = { launch: ERR, launchServer: ERR, connect: ERR, connectOverCDP: ERR, executablePath: () => '', name: () => 'stub' };
const chromium = { ...stub, name: () => 'chromium' };
const firefox = { ...stub, name: () => 'firefox' };
const webkit = { ...stub, name: () => 'webkit' };
module.exports.chromium = chromium;
module.exports.firefox = firefox;
module.exports.webkit = webkit;
module.exports.devices = {};
module.exports.errors = { TimeoutError: class TimeoutError extends Error {} };
module.exports.selectors = { register: () => Promise.resolve() };
module.exports.default = { chromium, firefox, webkit };
module.exports.__esModule = true;
8 changes: 8 additions & 0 deletions src/openclaw/stubs/playwright-core/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import pw from './index.cjs';
export default pw.default;
export const chromium = pw.chromium;
export const firefox = pw.firefox;
export const webkit = pw.webkit;
export const devices = pw.devices;
export const errors = pw.errors;
export const selectors = pw.selectors;
7 changes: 7 additions & 0 deletions src/openclaw/stubs/playwright-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "playwright-core",
"version": "1.58.2",
"main": "./index.cjs",
"exports": { ".": { "import": "./index.mjs", "require": "./index.cjs" } },
"type": "commonjs"
}
26 changes: 26 additions & 0 deletions src/openclaw/stubs/sharp/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';
function createBuilder() {
const b = {};
// All chainable methods return builder; add more as needed during D3 debugging
for (const m of ['resize','rotate','flip','flop','sharpen','blur','flatten','negate',
'normalize','greyscale','grayscale','removeAlpha','extract','trim','extend',
'composite','png','jpeg','webp','avif','tiff','gif','raw','toFormat','withMetadata']) {
b[m] = () => b;
}
b.toBuffer = () => Promise.resolve(Buffer.alloc(0));
b.toFile = () => Promise.reject(new Error('sharp unavailable in WebContainer'));
b.metadata = () => Promise.resolve({ width: 0, height: 0, format: 'unknown', channels: 0, hasAlpha: false });
b.clone = () => createBuilder();
b.pipe = () => b;
return b;
}
function sharp() { return createBuilder(); }
sharp.cache = () => {};
sharp.concurrency = () => 0;
sharp.counters = () => ({ queue: 0, process: 0 });
sharp.simd = () => false;
sharp.versions = { vips: '0.0.0', sharp: '0.34.5' };
sharp.format = {};
module.exports = sharp;
module.exports.default = sharp;
module.exports.__esModule = true;
8 changes: 8 additions & 0 deletions src/openclaw/stubs/sharp/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import sharp from './index.cjs';
export default sharp;
export const cache = sharp.cache;
export const concurrency = sharp.concurrency;
export const counters = sharp.counters;
export const simd = sharp.simd;
export const versions = sharp.versions;
export const format = sharp.format;
7 changes: 7 additions & 0 deletions src/openclaw/stubs/sharp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "sharp",
"version": "0.34.5",
"main": "./index.cjs",
"exports": { ".": { "import": "./index.mjs", "require": "./index.cjs" } },
"type": "commonjs"
}
7 changes: 7 additions & 0 deletions src/openclaw/stubs/sqlite-vec/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';
function load() {}
function getLoadablePath() { return ''; }
module.exports.load = load;
module.exports.getLoadablePath = getLoadablePath;
module.exports.default = { load, getLoadablePath };
module.exports.__esModule = true;
4 changes: 4 additions & 0 deletions src/openclaw/stubs/sqlite-vec/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sv from './index.cjs';
export default sv.default;
export const load = sv.load;
export const getLoadablePath = sv.getLoadablePath;
7 changes: 7 additions & 0 deletions src/openclaw/stubs/sqlite-vec/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "sqlite-vec",
"version": "0.1.7",
"main": "./index.cjs",
"exports": { ".": { "import": "./index.mjs", "require": "./index.cjs" } },
"type": "commonjs"
}
3 changes: 3 additions & 0 deletions src/raw-imports.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module '*.json?raw' { const c: string; export default c; }
declare module '*.cjs?raw' { const c: string; export default c; }
declare module '*.mjs?raw' { const c: string; export default c; }