Files
luma/Agent/instruments/codeshare.ts
Ole André Vadla Ravnås 92357bfd80 Add LumaCore package + cross-platform scaffolding
Lay the groundwork for a GTK/Adwaita frontend by extracting
shared infrastructure into a LumaCore Swift package. Agent
sources and LumaBundleCompiler move to top-level locations
so both the Xcode project and SPM can reference them.

New structure:
- Package.swift: root manifest with LumaCore lib + compiler
- Sources/LumaCore/: engine, persistence, models, and
  platform-abstracted token store
- Sources/LumaBundleCompiler/: moved from LumaBundleCompiler/
- Agent/: moved from Luma/Agent/
- LumaGtk/: skeleton GTK app package
2026-04-06 18:59:29 +02:00

203 lines
5.6 KiB
TypeScript

import type { Instrument, InstrumentContext } from "../core/instrument.js";
interface CodeShareConfig {
name: string;
source: string;
exports: string[];
}
interface CodeShareGlobalsEvent {
type: "codeshare-globals";
names: string[];
}
export const instrument: Instrument<CodeShareConfig> = {
async create(ctx, initialConfig) {
const runtime = new CodeShareRuntime(ctx, initialConfig);
await runtime.load();
return {
async updateConfig(next) {
await runtime.update(next);
},
async dispose() {
await runtime.dispose();
}
};
}
};
class CodeShareRuntime {
private readonly ctx: InstrumentContext;
private config: CodeShareConfig;
private listeners: InvocationListener[] = [];
private replacedTargets = new Set<NativePointer>();
constructor(ctx: InstrumentContext, config: CodeShareConfig) {
this.ctx = ctx;
this.config = config;
}
async load(): Promise<void> {
const wrapped = makeWrappedSource(this.config.source, this.config.exports);
this.evaluateWithInterceptorWrapper(wrapped);
this.emitGlobalsHint();
}
async update(next: CodeShareConfig): Promise<void> {
const sourceChanged = next.source !== this.config.source;
const exportsChanged = exportsChangedOrLengthDiff(next.exports, this.config.exports);
this.config = next;
if (!sourceChanged && !exportsChanged) {
return;
}
await this.dispose();
await this.load();
}
async dispose(): Promise<void> {
for (const listener of this.listeners) {
try {
listener.detach();
} catch { }
}
this.listeners = [];
for (const target of this.replacedTargets) {
try {
Interceptor.revert(target);
} catch { }
}
this.replacedTargets.clear();
try {
Interceptor.flush();
} catch { }
const g = globalThis as any;
for (const name of this.config.exports) {
try {
delete g[name];
} catch { }
}
}
private evaluateWithInterceptorWrapper(source: string): void {
const g = globalThis as any;
const originalInterceptor = g.Interceptor;
const runtime = this;
const wrappedInterceptor: any = { ...originalInterceptor };
wrappedInterceptor.attach = function (
target: NativePointerValue,
callbacksOrProbe: InvocationListenerCallbacks | InstructionProbeCallback,
data?: NativePointerValue,
): InvocationListener {
const listener = originalInterceptor.attach(target, callbacksOrProbe, data);
runtime.registerListener(listener);
return listener;
};
wrappedInterceptor.replace = function (
target: NativePointerValue,
replacement: NativePointerValue,
data?: NativePointerValue,
): void {
runtime.registerReplacedTarget(parseNativePointerValue(target));
return originalInterceptor.replace(target, replacement, data);
};
wrappedInterceptor.replaceFast = function (
target: NativePointerValue,
replacement: NativePointerValue,
): NativePointer {
const trampoline = originalInterceptor.replaceFast(target, replacement);
runtime.registerReplacedTarget(parseNativePointerValue(target));
return trampoline;
};
wrappedInterceptor.revert = function (target: NativePointerValue): void {
runtime.unregisterReplacedTarget(parseNativePointerValue(target));
return originalInterceptor.revert(target);
};
g.Interceptor = wrappedInterceptor;
try {
Script.evaluate(this.config.name, source);
} finally {
g.Interceptor = originalInterceptor;
}
}
private registerListener(listener: InvocationListener): void {
this.listeners.push(listener);
}
private registerReplacedTarget(target: NativePointer): void {
this.replacedTargets.add(target);
}
private unregisterReplacedTarget(target: NativePointer): void {
this.replacedTargets.delete(target);
}
private emitGlobalsHint(): void {
const names = this.config.exports;
if (names.length === 0) {
return;
}
const evt: CodeShareGlobalsEvent = {
type: "codeshare-globals",
names
};
this.ctx.emit(evt as any);
}
}
function makeWrappedSource(original: string, exports: string[]): string {
const exportEntries = exports
.map(name => ` ${name}`)
.join(",\n");
return [
"(function (global) {",
original,
"",
"const __lumaExports = {",
exportEntries,
"};",
"for (const [k, v] of Object.entries(__lumaExports)) {",
" global[k] = __lumaExports[k];",
"}",
"})(globalThis);"
].join("\n");
}
function exportsChangedOrLengthDiff(a: string[], b: string[]): boolean {
if (a.length !== b.length)
return true;
for (let i = 0; i !== a.length; i++) {
if (a[i] !== b[i])
return true;
}
return false;
}
function parseNativePointerValue(val: NativePointerValue): NativePointer {
if (isObjectWrapper(val)) {
return val.handle;
}
return val;
}
function isObjectWrapper(val: NativePointerValue): val is ObjectWrapper {
return (val as ObjectWrapper).handle !== undefined;
}