Hone Documentation
Hone is a native, AI-powered code editor built in TypeScript and compiled to native binaries via the Perry compiler — a TypeScript-to-native AOT compiler written in Rust. Hone runs without V8 or Node.js at runtime; the generated binaries link directly against platform UI frameworks.
Who these docs are for
- Hone contributors — developers working on the IDE itself, its editor, services, or built-in extensions
- Plugin developers — anyone building V2 plugins for the Hone marketplace using
@hone/sdk
How to navigate
| Section | Audience | What you’ll find |
|---|---|---|
| Getting Started | Contributors | Build instructions, project layout, Perry constraint cheat sheet |
| Architecture | Contributors | Deep-dives into how each major subsystem works |
| Packages | Contributors | Reference docs for every package in the monorepo |
| Plugin Development | Plugin devs | Guides for building, testing, and publishing plugins |
| Perry Integration | Contributors | Detailed Perry AOT constraints, FFI conventions, and build commands |
| Services | Operators | How to run and deploy Hone’s backend services |
| Testing | Contributors | Per-package test runners and UI testing tools |
| Contributing | Contributors | Dev environment setup and project conventions |
Key concepts
- Perry AOT — TypeScript is compiled ahead-of-time to native machine code. There is no JavaScript runtime. This imposes specific constraints on the TypeScript you can write.
- Monorepo, no workspace manager — Each package (
hone-core,hone-editor,hone-ide, etc.) is independent with its ownpackage.json. There is no top-level package.json or Yarn/pnpm workspace. - TS-authoritative rendering — TypeScript owns all document state. Native platform code (Rust FFI) is a rendering cache that receives computed lines and viewport state.
- Two extension systems — V1 built-in extensions ship with the IDE. V2 plugins are independently compiled native binaries distributed via the marketplace.
Building Hone
Prerequisites
- Bun – used as the test runner for most packages
- Rust toolchain – required to build the Perry compiler and its runtime libraries
- Perry compiler binary – compiles TypeScript to native binaries (no V8/Node.js at runtime)
Install Bun: https://bun.sh Install Rust: https://rustup.rs
Building Perry
Before compiling any Hone package to a native binary, you need a working Perry compiler.
# Build Perry (always disable LTO)
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry
# Build Perry UI library (macOS)
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos
# Rebuild Perry stdlib (after changing perry-runtime source)
cd ../perry && cargo clean -p perry-runtime --release && \
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry
Critical: Always set
CARGO_PROFILE_RELEASE_LTO=offfor all Perry Rust builds. Thin LTO produces bitcode that the macOS clang linker cannot read.
Running Tests
Each package has its own test suite. There is no monorepo-wide test runner.
cd hone-core && bun test # 649+ tests
cd hone-editor && bun test # 353 tests
cd hone-terminal && bun test # 163 tests
cd hone-relay && bun test # 48 tests
cd hone-build && bun test # 21 tests
cd hone-themes && npm test # 452 tests (Jest)
cd hone-extensions && npm test # Vitest
cd hone-api && npm test # Type-check only (tsc --noEmit)
Test runner rules:
hone-core,hone-editor,hone-terminal,hone-relay, andhone-buildusebun test– not vitest, not npx.hone-themesuses Jest.hone-extensionsuses Vitest.
Run a single test file:
bun test tests/buffer.test.ts
Type Checking
cd <package> && bun run typecheck
# or
cd <package> && npx tsc --noEmit
Compiling Native Binaries
Use the Perry compiler to produce platform-specific binaries from TypeScript entry points.
IDE
# macOS native binary
cd hone-ide && perry compile src/app.ts --output hone-ide
# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone
# Web
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html
Services
cd hone-auth && perry compile src/app.ts --output hone-auth
cd hone-marketplace && perry compile src/app.ts --output hone-marketplace
cd hone-build && perry compile src/app.ts --output hone-build
UI Testing (macOS)
geisterhand screenshot --output /tmp/shot.png # Take screenshot
geisterhand click x y # Click at coordinates
For iOS Simulator, use osascript (AppleScript) instead of geisterhand.
Project Structure
Hone is a monorepo of independent packages. There is no top-level package.json and no workspace manager (no npm workspaces, no Turborepo, no Lerna). Each package stands alone with its own dependencies and build configuration.
Package Directory
| Directory | Purpose | Runtime |
|---|---|---|
hone-core/ | Headless IDE services (workspace, settings, git, search, LSP, DAP, AI, extensions) | Bun (tests only) |
hone-editor/ | Embeddable code editor – piece table buffer, multi-cursor, syntax highlighting, diff | Bun (tests), Perry (native) |
hone-ide/ | IDE workbench shell – activity bar, sidebar, tabs, panels, theme engine | Perry (native binary) |
hone-terminal/ | Terminal emulator – VT parser, PTY, cross-platform Rust FFI | Bun (tests), Perry (native) |
hone-auth/ | Auth service (magic-link login, device pairing, subscriptions) – Fastify server | Perry (native binary, 2.8 MB) |
hone-relay/ | WebSocket sync relay (cross-device delta sync, SQLite persistence) | Bun / Perry |
hone-build/ | Plugin build coordinator (submits to perry-hub for cross-platform compilation) | Perry (native binary) |
hone-marketplace/ | Plugin marketplace server (marketplace.hone.codes) | Perry (native binary) |
hone-api/ | Public extension API types (@honeide/api) – pure declarations, zero runtime | tsc only |
hone-extensions/ | 11 built-in IDE extensions (TypeScript, Python, Rust, Go, etc.) | Perry |
hone-extension/ | V2 plugin SDK (@hone/sdk), Rust plugin host, marketplace client, CLI | Mixed |
hone-themes/ | 15 VSCode-compatible color themes (@honeide/themes) – pure JSON data | Jest |
hone-brand/ | Logos, colors, typography, brand guidelines | Static assets |
landing/ | Landing page (hone.codes) – single index.html, no build step | Static |
account.hone.codes/ | Account dashboard SPA | Static |
How Packages Relate
IDE application
hone-ide is the main application binary. It depends on:
hone-editor– the embeddable editor component (buffer, cursors, viewport, syntax highlighting, diff)hone-core– headless IDE services (workspace management, settings, git integration, search, LSP/DAP clients, AI, extension host)hone-terminal– the terminal emulator (VT parsing, PTY management, platform-specific Rust FFI)
Embeddable editor
hone-editor is designed to be used independently of the full IDE. Its core/ directory is entirely platform-independent. Platform-specific rendering lives in native/, with Rust FFI crates for macOS, iOS, Windows, Linux, Android, and Web.
Services
The backend services are standalone binaries, each compiled separately with Perry:
hone-auth(port 8445) – authentication, device pairing, JWT tokenshone-relay(port 8443/8444) – WebSocket rooms for cross-device synchone-marketplace(port 8446) – plugin search, download, publishhone-build(port 8447) – plugin cross-compilation via perry-hub workers
All services run on webserver.skelpo.net with MySQL for persistent storage and SQLite for relay delta persistence.
Extensions and themes
hone-extensions/contains 11 built-in extensions that ship with the IDE.hone-extension/provides the V2 plugin SDK (@hone/sdk) for third-party extension authors, plus the Rust plugin host and CLI tooling.hone-api/defines the public extension API surface (@honeide/api) as pure TypeScript declarations with no runtime code.hone-themes/ships 15 VSCode-compatible color themes as JSON data.
Static sites
landing/is the hone.codes landing page – a singleindex.htmlwith no build step.account.hone.codes/is the account dashboard SPA.hone-brand/holds logos, color definitions, typography, and brand guidelines.
Perry Constraints (Quick Reference)
Perry compiles TypeScript directly to native machine code via Rust codegen. Several JavaScript patterns that rely on V8 runtime behavior are not supported. This page lists each constraint with concrete before/after examples.
For full explanations, see the detailed constraints page.
Constraint Table
| Pattern | Use Instead |
|---|---|
obj[variable] dynamic key access | if/else if per key |
?. optional chaining | Explicit null checks |
?? nullish coalescing | if (x !== undefined) |
/regex/.test() | indexOf or char checks |
{ key } ES6 shorthand | { key: key } explicit |
array.map(fn) on class fields | for loop |
for...of on arrays | for (let i = 0; i < arr.length; i++) |
c >= 'a' && c <= 'z' char ranges | ALPHA_STR.indexOf(c) >= 0 |
Closures capturing this methods | Module-level functions + module-level vars |
requestAnimationFrame | setInterval |
setTimeout self-recursion | setInterval |
| String-returning functions in async | Inline string operations |
new Date() in async | Date.now() |
Examples
No dynamic key access
// Don't
const value = obj[key];
// Do
let value: string;
if (key === "name") {
value = obj.name;
} else if (key === "type") {
value = obj.type;
}
No optional chaining
// Don't
const name = user?.profile?.name;
// Do
let name: string | undefined;
if (user !== null && user !== undefined) {
if (user.profile !== null && user.profile !== undefined) {
name = user.profile.name;
}
}
No nullish coalescing
// Don't
const port = config.port ?? 8080;
// Do
let port = 8080;
if (config.port !== undefined) {
port = config.port;
}
No regex
// Don't
if (/^[a-z]+$/.test(input)) { ... }
// Do
const ALPHA = "abcdefghijklmnopqrstuvwxyz";
let allAlpha = true;
for (let i = 0; i < input.length; i++) {
if (ALPHA.indexOf(input[i]) < 0) {
allAlpha = false;
break;
}
}
No ES6 shorthand properties
// Don't
const obj = { name, age };
// Do
const obj = { name: name, age: age };
No array.map() on class fields
// Don't
class Editor {
lines: string[] = [];
getUpperLines() {
return this.lines.map((l) => l.toUpperCase());
}
}
// Do
class Editor {
lines: string[] = [];
getUpperLines(): string[] {
const result: string[] = [];
for (let i = 0; i < this.lines.length; i++) {
result.push(this.lines[i].toUpperCase());
}
return result;
}
}
No for...of on arrays
// Don't
for (const item of items) {
process(item);
}
// Do
for (let i = 0; i < items.length; i++) {
process(items[i]);
}
No character range comparisons
// Don't
if (c >= 'a' && c <= 'z') { ... }
// Do
const ALPHA_LOWER = "abcdefghijklmnopqrstuvwxyz";
if (ALPHA_LOWER.indexOf(c) >= 0) { ... }
No closures capturing this
// Don't
class App {
count = 0;
start() {
setInterval(() => {
this.count++; // closure captures `this` by value
this.render();
}, 16);
}
}
// Do
let count = 0;
function tick() {
count++;
render();
}
function render() { /* ... */ }
setInterval(tick, 16);
Closure rule: Perry captures closure variables by value, not by reference. Mutations inside a closure do not affect the outer variable. Store mutable state in module-level
letvariables and access them through module-level named functions.
No requestAnimationFrame
// Don't
function loop() {
update();
requestAnimationFrame(loop); // never fires in Perry
}
// Do
setInterval(update, 16);
No setTimeout self-recursion
// Don't
function poll() {
fetchData();
setTimeout(poll, 1000); // only fires once in Perry
}
// Do
setInterval(fetchData, 1000);
No string-returning functions in async
// Don't
async function getName(): Promise<string> {
return buildName(first, last); // returns NaN-boxed pointer
}
// Do
async function getName(): Promise<string> {
const name = first + " " + last; // inline the string operation
return name;
}
No new Date() in async
// Don't
async function logTime() {
const now = new Date();
console.log(now.toISOString());
}
// Do
async function logTime() {
const now = Date.now();
console.log(now.toString());
}
Architecture Overview
Hone is a native IDE built in TypeScript and compiled to native binaries via the Perry AOT compiler. Perry compiles TypeScript directly to machine code through Rust codegen – no V8 or Node.js exists at runtime. The generated binaries link against libperry_stdlib.a (the Perry runtime) and platform-specific UI libraries (perry-ui-macos, perry-ui-ios, perry-ui-windows).
Compilation Pipeline
TypeScript source
|
v
Perry compiler (TS → Rust codegen)
|
v
LLVM IR
|
v
Native binary
+ libperry_stdlib.a (runtime library)
+ perry-ui-{platform} (macOS / iOS / Windows / Linux / Android / Web)
Package Composition
The IDE is assembled from three main packages plus backend services:
hone-ide (workbench shell)
├── hone-editor (embeddable code editor)
│ ├── core/ — platform-independent (buffer, cursor, search, diff, LSP, DAP)
│ ├── view-model/ — reactive state bridge (EditorViewModel)
│ └── native/ — Rust FFI per platform (Metal, Direct2D, Cairo, WASM)
├── hone-core (headless IDE services)
│ ├── workspace — project management, file watching
│ ├── settings — layered settings (language > workspace > user > defaults)
│ ├── git — built-in git operations
│ ├── search — workspace-wide search
│ ├── LSP — language server protocol management
│ ├── DAP — debug adapter protocol management
│ ├── AI — provider registry, inline completion, chat, agent, review
│ ├── extensions — extension registry and lifecycle
│ ├── tasks — task runner
│ ├── formatting — code formatting
│ ├── telemetry — usage telemetry
│ ├── sync — cross-device sync client
│ └── changes queue — batched change processing
└── hone-terminal (terminal emulator)
├── VT parser
├── PTY management
└── cross-platform Rust FFI
Backend Services
Four server-side services support the IDE:
| Service | Host | Port | Purpose |
|---|---|---|---|
| hone-auth | auth.hone.codes | 8445 | Magic-link login, device pairing, JWT tokens, subscriptions |
| hone-relay | sync.hone.codes | 8443/8444 | WebSocket rooms for cross-device delta sync, SQLite persistence |
| hone-marketplace | marketplace.hone.codes | 8446 | Plugin search, download, publish |
| hone-build | — | 8447 | Plugin cross-compilation via perry-hub workers |
All backend services are also compiled to native binaries via Perry and run on webserver.skelpo.net (84.32.223.50, Ubuntu 24.04).
Supporting Packages
| Package | Purpose |
|---|---|
hone-api | Public extension API types (@honeide/api) – pure declarations, zero runtime |
hone-extension | V2 plugin SDK (@hone/sdk), Rust plugin host, marketplace client, CLI |
hone-extensions | 11 built-in language extensions (TypeScript, Python, Rust, Go, etc.) |
hone-themes | 15 VSCode-compatible color themes (@honeide/themes) |
hone-brand | Logos, colors, typography, brand guidelines |
Testing
Each package is tested independently – there is no monorepo test runner:
hone-core: 649+ tests viabun testhone-editor: 353 tests viabun testhone-terminal: 163 tests viabun testhone-relay: 48 tests viabun testhone-build: 21 tests viabun testhone-themes: 452 tests via Jesthone-extensions: Vitesthone-api: type-check only (tsc --noEmit)
Compilation Model
Hone compiles TypeScript to native binaries using the Perry AOT compiler – a TypeScript-to-native ahead-of-time compiler written in Rust. There is no V8, no JIT, and no JavaScript runtime at execution time.
How Perry Compiles TypeScript
Perry performs whole-program compilation:
- Parse – TypeScript source is parsed and type-checked
- Codegen – Rust codegen emits LLVM IR from the typed AST
- Link – LLVM produces a native binary linked against
libperry_stdlib.aand platform UI libraries
# Compile the IDE for macOS
cd hone-ide && perry compile src/app.ts --output hone-ide
# Compile for iOS simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone
# Compile for web (WASM)
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html
NaN-Boxing
All values at runtime are 64-bit doubles. Pointers, strings, objects, and other non-numeric types are encoded inside the NaN payload bits of IEEE 754 doubles. This means:
- Every variable is a single
f64at the machine level - Type checks are bitwise operations on the NaN payload
- No heap tagging or boxed value types
- String parameters across FFI are NaN-boxed
StringHeaderpointers
No Dynamic Dispatch
All types are resolved at compile time. Perry does not support:
- Dynamic property access (
obj[variable]– useif/else ifchains) - Optional chaining (
?.– use explicit null checks) - Nullish coalescing (
??– use explicitif (x !== undefined)) - Regular expressions (
/regex/.test()– useindexOfor character checks)
Closure Semantics
Perry captures closure variables by value, not by reference. This is the most important semantic difference from standard JavaScript/TypeScript:
// WRONG -- Perry captures count by value at closure creation time
let count = 0;
function increment() {
count++; // Always increments a stale copy
}
// CORRECT -- Use module-level variables and module-level functions
let count = 0;
function increment() {
count++; // Module-level function reads the live module-level variable
}
Store mutable state in module-level let variables and access them through module-level named functions.
package.json Perry Section
Each package declares its Perry configuration in package.json:
{
"perry": {
"nativeLibrary": {
"functions": [
"createWindow",
"drawText",
"handleInput"
]
},
"targets": ["macos", "ios", "windows", "linux", "web"]
}
}
FFI functions listed here are resolved at link time. Perry generates __wrapper_<function_name> symbols (double underscore prefix) for each declared function.
FFI Conventions
- String parameters are NaN-boxed
StringHeaderpointers. Rust receives*const u8and usesstr_from_header()to decode. - Use
f64for numeric FFI parameters. - Use
i64for string/pointer FFI parameters. - Do not use
i32– it causes verifier errors.
Perry-Specific Patterns to Avoid
| Pattern | Use Instead |
|---|---|
obj[variable] dynamic key access | if/else if per key |
?. optional chaining | Explicit null checks |
?? nullish coalescing | if (x !== undefined) |
/regex/.test() | indexOf or char checks |
{ key } ES6 shorthand | { key: key } |
array.map(fn) on class fields | for loop |
for...of on arrays | for (let i = 0; i < arr.length; i++) |
c >= 'a' && c <= 'z' char ranges | ALPHA_STR.indexOf(c) >= 0 |
Closures capturing this methods | Module-level functions + module-level vars |
requestAnimationFrame | setInterval |
setTimeout self-recursion | setInterval |
| String-returning functions in async | Inline string operations |
new Date() in async | Date.now() |
Building Perry Itself
When modifying Perry’s Rust source (located at ../perry/):
# Build the compiler
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry
# Build the macOS UI library
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos
# Rebuild stdlib after changing perry-runtime source
cd ../perry && cargo clean -p perry-runtime --release && \
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry
Always use CARGO_PROFILE_RELEASE_LTO=off – thin LTO produces bitcode that the macOS clang linker cannot read.
Editor Architecture
hone-editor is the embeddable code editor component (@honeide/editor). It is used by hone-ide but can also be embedded independently. The editor has three layers: core (platform-independent logic), view-model (reactive state bridge), and native (Rust FFI rendering per platform).
core/ – Platform-Independent
All editor logic lives here. No platform dependencies, no FFI, no rendering.
Buffer (buffer/)
Text storage uses a piece table backed by a rope with a line index:
- Piece table tracks original and add buffers with a sequence of pieces
- Rope provides O(log n) random access and efficient insertions/deletions
- Line index maintains a mapping from line numbers to buffer offsets
- Supports large files without loading entire content into a flat string
Cursor (cursor/)
Multi-cursor management:
- Multiple independent cursors with selection ranges
- Word boundary detection for double-click selection and ctrl+arrow navigation
- Cursor affinity (left/right) for ambiguous positions at line wraps
- Selection operations: expand to word, line, bracket, all
History (history/)
Undo/redo with time-based coalescing:
- Changes within a 500ms window are grouped into a single undo step
- Maximum undo stack depth: 10,000 entries
- Branching undo (redo stack preserved until a new edit diverges)
Viewport (viewport/)
Virtual scrolling for performance with large files:
- Only renders visible lines plus a 10-line buffer zone above and below
- Hidden line tracking for code folding integration
- Smooth scrolling with pixel-level offset tracking
- Viewport resize handling
Tokenizer (tokenizer/)
Incremental syntax highlighting via two engines:
- Lezer parser integration – 8 language grammars: TypeScript, HTML, CSS, JSON, Markdown, Python, Rust, C++
- Keyword-based fallback engine – for languages without a Lezer grammar
- Incremental re-tokenization on edits (only re-parses changed regions)
- Token types map to theme color scopes
Search (search/)
Find and replace across the document:
- Literal and regex search modes
- Chunked search for large files (avoids blocking the main thread)
- Incremental live search (results update as the user types)
- Match highlighting with current-match distinction
Folding (folding/)
Two folding strategies:
- Indent-based – folds regions where indentation increases
- Syntax-based – uses Lezer parse tree to identify foldable nodes (functions, classes, blocks)
- Nested folding with level tracking
Diff (diff/)
Myers diff algorithm implementation:
- O(ND) complexity for computing minimal edit scripts
- Hunk-based operations (apply hunk, revert hunk)
- Inline character-level diff within changed lines
- Used by the git diff view and AI review
Document (document/)
Wraps TextBuffer with document metadata:
- URI, languageId, version counter
isDirtytracking- Encoding detection (UTF-8, UTF-16, etc.)
- Line-ending detection and normalization (LF, CRLF)
Commands (commands/)
Command registry with 5 command modules:
- Editing – insert, delete, indent, comment toggle, auto-close brackets
- Navigation – go to line, go to definition, go to bracket
- Selection – select all, expand selection, shrink selection
- Clipboard – cut, copy, paste with multi-cursor support
- Multi-cursor – add cursor above/below, add cursor at next occurrence
LSP Client (lsp/)
Language Server Protocol client implementation:
- JSON-RPC 2.0 transport layer
- Typed request/response methods:
textDocument/completion– autocompletetextDocument/hover– hover informationtextDocument/publishDiagnostics– errors and warningstextDocument/codeAction– quick fixes and refactoringstextDocument/definition– go to definitiontextDocument/references– find all referencestextDocument/formatting– document formatting
DAP Client (dap/)
Debug Adapter Protocol client:
- Session management: launch and attach configurations
- Breakpoint management (line, conditional, logpoint)
- Execution control: continue, step over, step into, step out, pause
- Stack frame inspection
- Variable inspection with scopes (local, closure, global)
Snippets (snippets/)
- Template processing with tab stops and placeholders
- Language-specific snippet registries
- Variable expansion (
$TM_FILENAME,$CLIPBOARD, etc.)
view-model/ – Reactive State Bridge
EditorViewModel orchestrates all core subsystems and exposes reactive state for rendering:
- Cursor state – positions, selections, blink timer
- Decorations – inline decorations, line decorations, overlay widgets
- Diff view model – side-by-side and inline diff rendering state
- Find widget – search state, match counts, replace mode
- Ghost text – AI inline completion overlay
- Gutter – line numbers, fold indicators, breakpoint markers, diagnostics
- Line layout – computed line heights, wrapped line mapping
- Minimap – scaled-down document overview with viewport indicator
- Overlays – autocomplete popover, hover card, parameter hints
- Theme – resolved token colors and editor colors
native/ – Rust FFI Per Platform
The editor uses a TS-authoritative model: TypeScript is the single source of truth for all document state. The Rust layer is a rendering cache that receives pre-computed line content and viewport state via FFI.
NativeEditorFFI Interface
93+ FFI functions organized by concern:
- Document operations (set content, apply edit, get line)
- Viewport management (scroll, resize, set visible range)
- Cursor rendering (set positions, blink state)
- Selection rendering (set selection ranges)
- Decoration management (add/remove/update decorations)
- Theme application (set colors, font metrics)
- Input handling (key events, mouse events, IME)
Platform Crates
| Platform | Rendering | Text Layout |
|---|---|---|
| macOS | Metal | CoreText |
| iOS | UIKit / Metal | CoreText |
| Windows | Direct2D | DirectWrite |
| Linux | Cairo | Pango |
| Android | Skia | HarfBuzz |
| Web | WASM + Canvas | Browser layout |
Each platform crate implements the same NativeEditorFFI interface, so the TypeScript layer is entirely platform-agnostic.
Workbench Architecture
hone-ide is the IDE workbench shell. It assembles the editor, terminal, and core services into the full IDE experience. Compiled to a native binary via Perry.
Entry Point
src/app.ts is the Perry App() entry point:
App({
title: 'Hone',
width: 1200,
height: 800,
icon: 'hone-icon.png',
body: body
})
Bootstrap sequence:
- Load and apply theme
- Detect platform (
__platform__compile-time constant: 0=macOS, 1=iOS, 2=Android) - Register commands and keybindings
- Initialize panel registry
- Check for first-run (show setup screen) or launch normal workbench
- Initialize plugin system (conditional on
__plugins__compile flag)
Render Tree
src/workbench/render.ts defines the main UI layout:
+--+---------------------+------------------+
| | | |
|A | Sidebar | Editor Area |
|c | (explorer, search, | (tab bar + |
|t | git, debug, etc.) | editors) |
|i | | |
|v | | |
|i | +------------------+
|t | | Panels |
|y | | (terminal, |
| | | output, debug) |
|B | | |
|a | | |
|r | | |
+--+---------------------+------------------+
| Status Bar |
+-------------------------------------------+
- Activity bar – Left icon strip for switching sidebar views
- Sidebar – Contextual panel selected by activity bar
- Editor area – Tab bar with open files, editor instances below
- Panels – Bottom area for terminal, output, debug console
- Status bar – Language mode, cursor position, encoding, git branch, sync status
Views
25+ views organized in src/workbench/views/:
| View | Purpose |
|---|---|
ai-chat | AI chat panel with multi-turn conversation |
ai-inline | AI inline completion UI |
debug | Debugger controls, call stack, variables, watch |
diff | Side-by-side and inline diff viewer |
explorer | File tree, open editors |
extensions | Installed and available extensions |
find | Find and replace across workspace |
git | Staging, commits, branches, remotes |
lsp | Language server UI components: |
– autocomplete: completion popover | |
– diagnostics: error/warning list | |
– hover: hover information card | |
– signature: function signature help | |
notifications | Toast notifications |
plugins | V2 plugin management |
pr-review | Pull request review with AI annotations |
quick-open | Fuzzy file finder (Cmd+P) |
recent | Recently opened files and workspaces |
search | Workspace-wide text search |
settings-ui | Visual settings editor |
setup | First-run configuration wizard |
sync | Cross-device sync status and settings |
tabs | Tab bar and tab management |
terminal | Integrated terminal |
update | Application update notifications |
welcome | Welcome tab with getting-started content |
status-bar | Status bar segments |
Command System
Commands are the primary way to expose IDE functionality:
- Command registry – central map of command ID to handler function
- Keybindings – modifier keys, chord sequences (e.g., Ctrl+K Ctrl+C), platform-specific defaults
- When-clauses – conditional activation (e.g.,
editorTextFocus,inDebugMode) - Commands are contributed by core, views, and extensions
Native Menu Bar
Platform-native menu bar setup with standard menus (File, Edit, Selection, View, Go, Run, Terminal, Help). Menu items map to commands.
Grid Layout System
The workbench uses a grid layout for arranging sidebar, editor area, and panels:
- Resizable splits between sidebar and editor area
- Resizable splits between editor area and bottom panels
- Collapsible sidebar and panels
- Persisted layout dimensions in settings
Panel Registry
Built-in panels are registered at startup:
- Terminal panel
- Output panel
- Debug console panel
- Additional panels contributed by extensions
Theme Engine
Loads and applies VSCode-compatible JSON themes:
- Theme colors accessed via getter functions (
getEditorBackground(),getSideBarBackground(), etc.) - Applied at two levels: workbench chrome colors and editor token colors
- Theme switching without restart
- See Theme Architecture for details
Settings
Layered settings with precedence:
- Language-specific (highest priority)
- Workspace (
.hone/settings.json) - User (
~/.hone/settings.json) - Defaults (lowest priority)
Perry UI Considerations
Perry’s UI system creates widgets once and mutates them imperatively (not declarative/virtual-DOM). Key constraints:
- Widgets are created in the render function and updated via property mutations
- Closures must follow Perry’s by-value capture rule – use module-level state and module-level functions
- No
requestAnimationFrame– usesetIntervalfor animation loops - No
setTimeoutself-recursion – usesetInterval
Sync Architecture
Cross-device sync for the Hone IDE. Edit on desktop and see changes on mobile (and vice versa) in real time. Sync is off by default – users opt in via Settings > Sync.
Components
Auth Service (auth.hone.codes, port 8445)
Magic-link login, device token management, project entitlements. Built with Fastify, backed by MySQL on webserver.skelpo.net.
Relay Server (sync.hone.codes, ports 8443/8444)
WebSocket message router between devices in the same room. SQLite persistence for buffering messages during disconnects.
Auth Flow
1. User enters email in IDE settings
2. IDE calls GET /auth/login?email=...
3. Auth creates 64-char random hex token, stores in magic_links table (15-min expiry)
4. Email sent to user (or logged to console in dev mode)
5. User clicks link: GET /auth/verify?token=...&deviceName=...&platform=...
6. Auth validates token, creates user + device records, generates device token
7. IDE stores deviceToken locally
Device Token Format
userId:deviceId:timestamp.hash
Where hash is a double-djb2 HMAC. A shared secret between auth and relay allows the relay to validate tokens locally without calling back to auth.
Projects and Tiers
Projects map local workspaces to relay rooms. Each project has a projectKey, name, roomId, and userId.
| Tier | Synced Projects |
|---|---|
| free | 0 |
| personal | 1 |
| pro | unlimited |
| team | unlimited |
Project registration is idempotent – registering the same project key twice returns the existing record.
Relay Protocol
Join
{
"type": "join",
"room": "<roomId>",
"device": "<deviceId>",
"token": "<deviceToken>"
}
Joined (response)
{
"type": "joined",
"room": "<roomId>",
"device": "<deviceId>"
}
Messages
{
"from": "<deviceId>",
"to": "host | broadcast | <targetDeviceId>",
"room": "<roomId>",
"payload": { ... }
}
Relay Behavior
- Room-based routing – room IDs hashed via djb2 for slot assignment
- Slot tracking – each connection occupies a slot in a room
- Host election – first device to join a room becomes host
- 60-second message buffer – messages buffered for reconnecting devices (covers brief disconnects)
- Rate limiting – per-connection rate limiting to prevent abuse
- Auth bypass – when
auth.secretis empty, token validation is skipped (dev mode only)
Auth Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /auth/login | Initiate magic-link login (params: email) |
| GET | /auth/verify | Verify magic link and register device (params: token, deviceName, platform) |
| GET | /auth/device | Get device info (header: Authorization) |
| GET | /auth/devices | List all devices for the authenticated user |
| DELETE | /auth/device/:id | Remove a device |
| POST | /auth/project | Register a project for sync |
| GET | /auth/projects | List synced projects |
| DELETE | /auth/project/:id | Remove a synced project |
| GET | /auth/subscription | Get current subscription tier |
All endpoints except /auth/login and /auth/verify require the Authorization: Bearer <deviceToken> header.
Database Schema
users
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
devices
CREATE TABLE devices (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL,
deviceName VARCHAR(255) NOT NULL,
platform VARCHAR(50) NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
lastSeen DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
magic_links
CREATE TABLE magic_links (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
expiresAt DATETIME NOT NULL,
used BOOLEAN DEFAULT FALSE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
projects
CREATE TABLE projects (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL,
projectKey VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
roomId VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_project (userId, projectKey)
);
subscriptions
CREATE TABLE subscriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL UNIQUE,
tier ENUM('free', 'personal', 'pro', 'team') DEFAULT 'free',
expiresAt DATETIME,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
All identifiers use camelCase (matching Hone’s database convention).
IDE Settings
| Setting | Default | Description |
|---|---|---|
syncEnabled | false | Enable cross-device sync |
syncRelayUrl | wss://sync.hone.codes | Relay server WebSocket URL |
syncAuthUrl | https://auth.hone.codes | Auth service URL |
syncDeviceToken | (empty) | Stored device token after login |
Deployment
Both services run on 84.32.223.50 (Ubuntu 24.04):
- TLS termination via Let’s Encrypt + nginx reverse proxy
- Auth config:
auth.conf(KEY=VALUE format) - Relay config:
relay.conf(KEY=VALUE format) - MySQL database: host=webserver.skelpo.net, user=hone
Config File Format
# auth.conf
db.host=webserver.skelpo.net
db.user=hone
db.password=...
db.name=hone
auth.secret=<shared-secret>
smtp.host=...
smtp.port=587
smtp.user=...
smtp.password=...
# relay.conf
auth.secret=<shared-secret>
sqlite.path=/var/lib/hone-relay/relay.db
Self-Hosted
Users can run their own relay and auth services. Both are single native binaries:
- Compile:
perry compile src/app.ts --output hone-auth/hone-relay - Create config file (
auth.conf/relay.conf) - Run the binary
Point the IDE settings (syncRelayUrl, syncAuthUrl) at your own server.
AI Architecture
Hone integrates AI at multiple levels: inline code completion, multi-turn chat, automated code review, and an autonomous agent. The AI system is provider-agnostic, with a registry of adapters for major LLM providers.
Provider System
ProviderRegistry
Central registry managing AI provider adapters. Each adapter implements a common interface for chat completions and streaming.
Adapters:
| Provider | Description |
|---|---|
| Anthropic | Claude models (direct API) |
| OpenAI | GPT models (direct API) |
| Gemini models | |
| Ollama | Local models via Ollama |
| Azure OpenAI | OpenAI models via Azure |
| Bedrock | AWS Bedrock (Anthropic, etc.) |
| Vertex | Google Cloud Vertex AI |
ModelRouter
Selects the appropriate model for a given task based on:
- User preferences (configured default model)
- Task type (completion vs. chat vs. review)
- Model capabilities (context window, tool use support)
- Provider availability
Token Estimation
Estimates token counts before sending requests to:
- Stay within context window limits
- Truncate or chunk context when necessary
- Provide usage feedback to the user
AI Surfaces
1. Inline Completion (ai/inline/)
Ghost text suggestions that appear as the user types.
- Fill-In-Middle (FIM) formatting – constructs prompts with prefix, suffix, and cursor position for infilling
- Debouncing – waits for a pause in typing before sending requests (avoids flooding the provider)
- Caching – recently generated completions are cached and reused when the user types matching characters
- Request lifecycle – manages in-flight requests, cancels stale requests when the cursor moves
The completion appears as dimmed ghost text. The user accepts with Tab or dismisses by continuing to type.
2. Chat (ai/chat/)
Multi-turn AI chat panel integrated into the IDE.
- Multi-turn conversation – maintains conversation history with system prompt, user messages, and assistant responses
- Context collection – automatically gathers relevant context:
- Current file content and cursor position
- Selected text
- Visible viewport
- Diagnostics (errors/warnings)
- Open file list
- Code block extraction – parses assistant responses to identify code blocks with language and file path annotations
- Streaming renderer – renders assistant responses token-by-token as they stream in
3. Review (ai/review/)
AI-powered code review for diffs and pull requests.
- Diff chunking – splits large diffs into reviewable chunks that fit within the model’s context window
- Annotation parsing – extracts structured annotations (line number, severity, suggestion) from the model’s response
- Review engine – orchestrates the review process:
- Collects diff hunks
- Chunks them for the model
- Sends each chunk with review instructions
- Parses and aggregates annotations
- Displays inline annotations in the diff view
Agent System (ai/agent/)
Autonomous task execution with tool calling.
Plan and Execute Loop
- User describes a task in natural language
- Agent creates a plan (sequence of steps)
- Agent executes steps using tools:
- File read/write
- Terminal commands
- Search (workspace-wide)
- LSP queries (find references, go to definition)
- Agent observes results and adjusts plan
- Repeats until task is complete or blocked
Approval Flows
- Auto-approve – read-only operations (file read, search) execute without confirmation
- Prompt for approval – write operations (file edit, terminal commands) require user confirmation
- Batch approval – user can approve all remaining steps in a plan
Error Recovery
When a tool call fails, the agent:
- Observes the error message
- Adjusts its approach
- Retries with a different strategy
- Escalates to the user if repeated failures occur
Activity Logging
All agent actions are logged for transparency:
- Tool calls with arguments and results
- Plan steps with status (pending, in-progress, completed, failed)
- Token usage per step
- Total elapsed time
Extension Architecture
Hone has two extension systems that coexist: V1 built-in extensions for language support that ships with the IDE, and V2 plugins for third-party and community extensions distributed via the marketplace.
V1 Built-in Extensions (hone-extensions/)
Language Packs
11 built-in language extensions ship with the IDE binary:
| Extension | Languages |
|---|---|
| TypeScript | TypeScript, JavaScript, JSX, TSX |
| Python | Python |
| Rust | Rust |
| Go | Go |
| C++ | C, C++, Objective-C |
| HTML/CSS | HTML, CSS, SCSS, Less |
| JSON | JSON, JSONC |
| Markdown | Markdown |
| Docker | Dockerfile, Docker Compose |
| TOML/YAML | TOML, YAML |
| Git | Git commit, Git rebase, .gitignore |
Manifest Format
Each V1 extension declares a hone-extension.json manifest:
{
"name": "typescript",
"displayName": "TypeScript Language",
"version": "1.0.0",
"activationEvents": [
"onLanguage:typescript",
"onLanguage:javascript"
],
"contributes": {
"languages": [
{
"id": "typescript",
"extensions": [".ts", ".tsx"],
"aliases": ["TypeScript"]
}
],
"lspServer": {
"command": "typescript-language-server",
"args": ["--stdio"],
"languages": ["typescript", "javascript"]
},
"commands": [],
"snippets": [
{
"language": "typescript",
"path": "./snippets/typescript.json"
}
]
}
}
Loading
V1 extensions are loaded by ExtensionRegistry in hone-core:
- Scan
hone-extensions/directory forhone-extension.jsonmanifests - Parse manifests and register language contributions
- Activate extensions lazily based on activation events (e.g., when a TypeScript file is opened)
- Start LSP servers as needed
V2 Plugin SDK (hone-extension/)
Overview
V2 plugins are independent packages compiled to native binaries via Perry. They use the @hone/sdk package for the plugin API.
Plugin Structure
import { HonePlugin, HoneHost } from '@hone/sdk';
class MyPlugin extends HonePlugin {
activate(host: HoneHost): void {
host.commands.registerCommand('myPlugin.hello', () => {
host.window.showInformationMessage('Hello from my plugin!');
});
}
deactivate(): void {
// cleanup
}
}
HoneHost Interface
HoneHost provides 80+ types covering:
commands– register and execute commandswindow– show messages, create output channels, status bar itemsworkspace– access files, folders, settingseditor– manipulate the active editor (selections, decorations, edits)languages– register language features (completions, hover, diagnostics)debug– register debug adaptersterminal– create and manage terminal instances
Execution Tiers
Plugins run in one of three tiers based on their declared permissions:
| Tier | Capabilities | Isolation |
|---|---|---|
| InProcess | UI-only (commands, decorations, status bar) | Runs in main process |
| PluginHost | Editor access, file system read | Separate plugin host process |
| IsolatedProcess | Network, file system write, spawn processes | Fully sandboxed process |
The tier is declared in the plugin’s package.json:
{
"hone": {
"tier": "PluginHost",
"permissions": ["fs.read", "editor"]
}
}
Distribution
V2 plugins are distributed via the Hone marketplace (marketplace.hone.codes):
- Build –
hone-buildsubmits the plugin source to perry-hub workers for cross-platform compilation - Publish – compiled binaries are uploaded to the marketplace
- Install – IDE downloads the platform-appropriate binary from the marketplace
- Update – marketplace notifies IDE of available updates
CLI
The hone-extension package includes a CLI for plugin development:
hone ext init– scaffold a new plugin projecthone ext build– compile the pluginhone ext publish– publish to the marketplacehone ext test– run plugin tests
Coexistence
Both systems can:
- Register commands in the command registry
- Contribute UI elements (sidebar views, status bar items, decorations)
- Interact with the editor (read/write documents, manage cursors)
- Respond to lifecycle events
V1 extensions handle core language support and ship bundled with the IDE. V2 plugins extend the IDE with third-party functionality and are installed independently by users. Both register with the same underlying command and contribution systems, so they interoperate seamlessly.
Theme Architecture
Hone uses VSCode-compatible JSON themes for both workbench colors and syntax highlighting. Themes are applied at two levels and can be switched at runtime without restarting the IDE.
Theme Format
Themes use the standard VSCode JSON theme format, making it possible to import any existing VSCode theme directly.
{
"name": "Hone Dark",
"type": "dark",
"colors": {
"editor.background": "#1e1e1e",
"editor.foreground": "#d4d4d4",
"sideBar.background": "#252526",
"statusBar.background": "#007acc",
"activityBar.background": "#333333"
},
"tokenColors": [
{
"scope": ["keyword", "storage.type"],
"settings": {
"foreground": "#569cd6"
}
},
{
"scope": ["string"],
"settings": {
"foreground": "#ce9178"
}
}
]
}
Two-Layer System
UI Theme (Workbench Colors)
The colors object defines colors for every workbench element:
- Editor: background, foreground, line highlight, selection, cursor
- Sidebar: background, foreground, border
- Activity bar: background, foreground, badge
- Status bar: background, foreground (normal, debugging, no-folder)
- Tabs: active/inactive background and foreground, border
- Panel: background, border
- Title bar: background, foreground
- Input fields, dropdowns, buttons, scrollbars, minimap, etc.
Colors are accessed through getter functions:
getEditorBackground()
getEditorForeground()
getSideBarBackground()
getStatusBarBackground()
getActivityBarBackground()
getTabActiveBackground()
getTabInactiveBackground()
getPanelBackground()
// ... and many more
Token Theme (Syntax Highlighting)
The tokenColors array maps TextMate scopes to colors and font styles:
keyword– language keywords (if,else,return, etc.)string– string literalscomment– commentsvariable– variable namesfunction– function names and callstype– type names, classes, interfacesconstant– constants, numbers, booleansoperator– operatorspunctuation– brackets, semicolons, commas
Token scopes support dot-separated specificity (e.g., entity.name.function.typescript is more specific than entity.name.function).
Built-in Themes
15 themes ship with the IDE (hone-themes package):
| Theme | Type |
|---|---|
| Hone Dark | dark |
| Hone Light | light |
| Catppuccin | dark |
| Dracula | dark |
| GitHub Dark | dark |
| GitHub Light | light |
| Gruvbox Dark | dark |
| High Contrast Dark | dark (high contrast) |
| High Contrast Light | light (high contrast) |
| Monokai | dark |
| Nord | dark |
| One Dark | dark |
| Solarized Dark | dark |
| Solarized Light | light |
| Tokyo Night | dark |
WCAG Contrast Validation
The hone-themes package includes 452 tests (via Jest) that validate:
- Schema validation – every theme conforms to the expected JSON structure
- WCAG contrast ratios – text colors meet accessibility contrast requirements against their backgrounds
- Normal text: minimum 4.5:1 contrast ratio (AA)
- Large text: minimum 3:1 contrast ratio (AA)
- Required color keys – all themes define the minimum set of required colors
- Token color coverage – all themes provide colors for core syntax scopes
Run theme tests:
cd hone-themes && npm test
Importing VSCode Themes
Any VSCode JSON theme file can be imported directly:
- Place the
.jsontheme file in the themes directory - The theme engine reads and parses the VSCode format natively
- No conversion or transformation step is needed
This compatibility means the thousands of themes available in the VSCode ecosystem work in Hone out of the box.
Theme Application
When a theme is applied:
- The theme JSON is parsed and colors are resolved (including defaults for missing keys)
- Workbench colors are set on all UI elements via getter functions
- Token colors are compiled into a scope-to-style lookup table for the tokenizer
- The editor re-renders with the new colors
- The selected theme is persisted in user settings
Theme switching is instant – no restart required.
hone-api
Package: @honeide/api — pure TypeScript declarations, zero runtime code. Defines the public extension API surface that all Hone extensions program against.
Namespaces
Each namespace corresponds to a separate source file:
| Namespace | Source | Purpose |
|---|---|---|
commands | commands.ts | Command registry and execution |
workspace | workspace.ts | Workspace, folders, and file operations |
window | ui.ts (exported as window) | UI components and window management |
editor | editor.ts | Editor state, selections, viewport |
languages | languages.ts | Language registration and configuration |
debug | debug.ts | Debugger protocol and control |
terminal | terminal.ts | Terminal emulator and PTY |
ai | ai.ts | AI chat/completion APIs |
ui | ui.ts | UI component APIs (panels, status bar, notifications) |
sync | sync.ts | Cross-device sync types |
Core Types
Defined in types.ts:
Disposable
Cleanup interface returned by registrations and subscriptions.
ExtensionContext
Passed to the extension’s activate function. Provides:
subscriptions— array ofDisposableobjects; disposed when the extension deactivatesextensionPath— absolute path to the extension’s install directorystoragePath— per-workspace storage pathglobalStoragePath— global (cross-workspace) storage path
ActivateFunction
(context: ExtensionContext) => void | Promise<void>
DeactivateFunction
() => void | Promise<void>
Testing
cd hone-api && npm test
Type-check only via tsc --noEmit. No runtime tests — the package contains no executable code.
hone-core
Headless IDE services. Runtime: Bun (tests only). 649+ tests.
cd hone-core && bun test
Service Modules
workspace/
Multi-root workspace management. Tracks root folders, filesystem entries, and workspace configuration (.hone/ directory).
Exports: Workspace, WorkspaceFolder, FileEntry, FileWatcher, FileIndex
settings/
Layered settings resolution with 4-layer priority (highest to lowest):
- Language overrides
- Workspace overrides
- User settings
- Defaults
VS Code-compatible format. Exports: SettingsStore, KeybindingResolver, schema validation.
git/
Git integration via child_process. Parses status, diff, log, and blame output. Includes platform clients for GitHub, GitLab, and Bitbucket.
Exports: GitClient, diff/blame/log parsers
search/
Workspace-wide text search. Supports literal and regex modes with case sensitivity and whole-word matching. Ripgrep integration for performance.
Exports: searchFileContent, RipgrepSearcher
protocols/lsp/
Language Server Protocol implementation. JSON-RPC 2.0 message framing.
Exports: JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, ClientCapabilities, LSP types
protocols/dap/
Debug Adapter Protocol implementation. DAP message structure with Content-Length framing.
Exports: DapRequest, DapResponse, DapEvent, BreakpointManager, DapManager, DapClient
ai/provider/
Multi-provider AI system with dynamic routing.
ProviderRegistry— registers and resolves AI providersModelRouter— selects provider/model based on task requirements- Token estimation utilities
Adapters: Anthropic, OpenAI, Google, Ollama, Azure OpenAI, Bedrock, Vertex
ai/inline/
Inline code completion using Fill-In-Middle (FIM) formatting. Debouncing prevents excessive requests; caching avoids redundant completions.
Exports: InlineCompletionProvider, CompletionDebouncer, CompletionCache
ai/chat/
Multi-turn chat with streaming support. Manages conversation history, collects editor/workspace context, extracts code blocks from responses.
Exports: ChatModel, ContextCollector, StreamingRenderer
ai/agent/
Autonomous agent tasks. Executes a plan-then-act loop with tool calling, user approval flows, and error recovery.
Exports: AgentOrchestrator, AgentPlan, ActivityLog, AGENT_TOOLS
ai/review/
AI-powered pull request review. Splits diffs into reviewable chunks, parses annotations from AI responses.
Exports: ReviewEngine, chunkDiff, parseAnnotations
extensions/
Built-in extension management (V1 system). Loads, activates, and deactivates extensions based on parsed manifests.
Exports: ExtensionRegistry, ExtensionHost, parseManifest
tasks/
Task configuration and execution. Parses tasks.json format (VS Code-compatible), executes shell and process tasks.
Exports: parseTaskConfig, TaskRunner
sync/
Cross-device sync protocol. Handles device pairing, encryption, and transport (LAN discovery and relay fallback). Room-based sync with envelope format and sequence numbers.
queue/
Changes queue (review inbox). Tracks proposed changes with pending/accepted/rejected states. Detects conflicts before applying diffs.
Exports: ChangesQueue, DiffApplier, ConflictDetector, TrustConfig
formatting/
Document formatting rules. Perry-safe pure TypeScript (no regex). Handles trailing whitespace trimming, final newline insertion, and indentation normalization.
telemetry/
Anonymous usage telemetry. Batches events using the Chirp protocol (max 50 events per batch). No PII collected.
hone-editor
Package: @honeide/editor — embeddable code editor. 353 tests.
cd hone-editor && bun test
The editor is structured in three layers: core (platform-independent logic), view-model (reactive state bridge), and native (Rust FFI per platform).
Core
buffer/
Piece table backed by a rope with a line index. TextBuffer wraps rope internals with a clean API for editing and querying.
cursor/
Multi-cursor management. CursorManager handles primary and secondary cursors with merging, sorting, selection ranges, and word boundary detection.
history/
UndoManager with time-based coalescing (500ms window, max depth 10000). Each operation records: edits, deleted texts, cursor state before and after. Supports computeInverseEdits for redo.
viewport/
ViewportManager for virtual scrolling. Computes visible line range with a 10-line buffer zone. Tracks hidden lines from code folding. Includes ScrollController and LineHeightCache.
tokenizer/
SyntaxEngine via Lezer parser integration. Incremental tokenization — only re-tokenizes affected regions after edits. Token theme resolution maps parser tokens to editor colors. KeywordSyntaxEngine serves as a fallback when no grammar is available.
Bundled grammars: TypeScript, HTML, CSS, JSON, Markdown, Python, Rust, C++
search/
SearchEngine supporting literal and regex modes, chunked for large files. Replace supports regex capture group expansion. IncrementalSearch powers the live find widget.
folding/
Two folding strategies:
- Indent-based — fallback for languages without a grammar
- Syntax-based — uses Lezer parse tree
FoldState tracks collapsed ranges.
diff/
Myers diff algorithm with O(ND) complexity. DiffResult contains hunks typed as add, delete, or modify. Utilities: mergeAdjacentHunks, splitHunk, navigateHunks. Supports inline character-level diff within hunks.
document/
EditorDocument combines TextBuffer with metadata: URI, languageId, version, isDirty, encoding, and line-ending detection. EditBuilder provides a transaction API for batching edits.
commands/
CommandRegistry maps string IDs to handler functions. Five command modules:
- Editing (insert, delete, indent)
- Navigation (move, scroll, go-to)
- Selection (select word, line, all)
- Clipboard (cut, copy, paste)
- Multicursor (add cursor above/below, select occurrences)
lsp-client/
LSPClient wrapping JSON-RPC transport. Typed methods for completion, hover, diagnostics, and code actions. Manages the initialize/initialized lifecycle. ServerCapabilityChecker queries what the server supports.
dap-client/
DAPClient for debug sessions. Supports launch/attach configurations, breakpoint management, stepping (into/over/out), stack frame inspection, and variable evaluation.
snippets/
Snippet template processing with tab stops, placeholders, and variable expansion. Language-specific snippet registries.
View-Model
Reactive state bridge between core subsystems and rendering.
EditorViewModel is the main orchestrator. It connects buffer, cursor, viewport, tokenizer, search, and folding into a unified state that the native layer consumes.
Sub-models:
| Model | Responsibility |
|---|---|
CursorState | Cursor positions and selection visuals |
Decorations | Inline and margin decorations |
DiffViewModel | Side-by-side and inline diff state |
FindWidget | Find/replace UI state |
GhostText | Inline completion preview |
Gutter | Line numbers, fold markers, breakpoints |
LineLayout | Line wrapping and layout metrics |
Minimap | Document overview rendering state |
Overlays | Hover cards, autocomplete popups, signature help |
EditorTheme | Token and UI color resolution |
Native (Rust FFI)
NativeEditorFFI interface defines 93+ FFI functions covering rendering, input handling, events, font metrics, and color management.
Key components:
NativeRenderCoordinator— orchestrates rendering calls to the platform layerTouchInputHandler— processes touch events for mobile platforms- Word wrap computation
Six platform crates:
| Platform | Rendering Stack |
|---|---|
| macOS | Metal, CoreGraphics, CoreText |
| iOS | UIKit |
| Windows | Direct2D, DirectWrite |
| Linux | Cairo, Pango |
| Android | Native .so |
| Web | WASM |
hone-ide
IDE workbench shell. Compiles to a native binary via Perry.
# macOS
cd hone-ide && perry compile src/app.ts --output hone-ide
# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone
# Web
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html
Entry Point
src/app.ts performs startup in order:
- Load themes from
@honeide/themes - Detect platform and screen dimensions
- Register built-in commands and panels
- Build the visual workbench (or show first-run setup screen)
- Call
App({ title: 'Hone', width: 1200, height: 800, icon: 'hone-icon.png', body: body })
The plugin system initializes conditionally based on the __plugins__ compile flag.
Commands
src/commands.ts provides registerCommand() with:
id— unique string identifiertitle— display namecategory— grouping (e.g., “File”, “Edit”, “View”)handler— function to executeshowInPalette— whether the command appears in the command palette
Commands are the central mechanism for keybindings, menus, and the command palette.
Keybindings
src/keybindings.ts handles keyboard shortcut resolution.
- Modifier support: Ctrl, Shift, Alt, Meta (Cmd on macOS)
- Chord sequences (e.g., Ctrl+K then Ctrl+C)
- When-clauses for context-dependent activation
CmdOrCtrlnormalizes to Cmd on macOS, Ctrl on other platforms
Views
25+ views in src/workbench/views/:
| View | Purpose |
|---|---|
ai-chat | AI conversation panel |
ai-inline | Inline AI completion UI |
debug | Debugger controls and state |
diff | Side-by-side and inline diff |
explorer | File tree browser |
extensions | Extension management |
find | Find and replace |
git | Git status, staging, commit |
lsp | Autocomplete, diagnostics, hover, signature help |
notifications | Toast notifications |
plugins | Plugin management (V2) |
pr-review | Pull request review |
quick-open | Fuzzy file finder |
recent | Recent files and workspaces |
search | Workspace-wide search |
settings-ui | Visual settings editor |
setup | First-run configuration |
sync | Cross-device sync status |
tabs | Editor tab bar |
terminal | Integrated terminal |
update | Update notifications |
welcome | Welcome/start page |
status-bar | Bottom status bar |
Theme Engine
Loads VS Code-compatible JSON theme files. Ships with 15 built-in themes.
- UI colors accessed via getter functions
- Dynamic theme switching at runtime
- Token colors resolved through the tokenizer’s theme mapping
hone-terminal
Package: @honeide/terminal — terminal emulator. 163 tests.
cd hone-terminal && bun test
Components
vt-parser/
VT100/xterm escape sequence parser. Handles:
- CSI (Control Sequence Introducer) — cursor movement, erase, scroll, SGR attributes
- OSC (Operating System Command) — window title, clipboard, hyperlinks
- DCS (Device Control String) — device-specific sequences
pty/
PTY management with platform-specific backends:
- Unix (macOS/Linux) — POSIX PTY via
openpty/forkpty - Windows — ConPTY for modern terminal support
Spawns shell processes and manages bidirectional I/O between the emulator and the child process.
input/
Input encoding layer:
- Key encoder — maps keyboard events to VT escape sequences
- Mouse encoder — translates mouse events to xterm mouse reporting sequences
emulator.ts
Terminal emulator core. Ties together the parser, screen buffer, and input handler into a unified terminal instance. Manages:
- Screen buffer (primary and alternate)
- Cursor state and attributes
- Scroll region and scrollback
- Character set handling
Rendering
Cross-platform rendering via Rust FFI, following the same pattern as hone-editor’s native layer. The TypeScript emulator owns the terminal state; Rust handles platform-specific drawing.
hone-extension
V2 plugin SDK package. Contains the SDK, Rust plugin host, marketplace client, and CLI tooling.
SDK (@hone/sdk)
Base Classes and Interfaces
HonePlugin— base class that plugins extendHoneHost— host interface type (capabilities the IDE exposes to plugins)HoneHostImpl— host implementationCanvasContext— custom UI rendering context for plugin panels
Types
80+ type definitions organized by domain:
Editor
BufferId, Position, Range, Selection, TextEdit
Filesystem
FileStat, FileInfo, FileWatchEvent
UI
PanelElement, StatusBarItemOptions, DecorationTypeOptions, GutterProviderOptions, ContextMenuOptions, NotificationOptions
Process and Network
SpawnOptions, HttpRequest, HttpResponse
Configuration
ConfigSchema
Events
DocumentOpenEvent, FormatDocumentEvent, and others
Manifest
PluginManifest, PluginCapabilities, PluginTier
Helper Functions
Edit construction:
insertEdit(position, text)
replaceEdit(range, text)
deleteEdit(range)
Position/range construction:
pos(line, character)
range(startLine, startChar, endLine, endChar)
Manifest utilities:
deriveTier(manifest) // Determines plugin tier from capabilities
validateManifest(manifest) // Validates manifest structure
parsePluginManifest(json) // Parses raw JSON into typed manifest
Testing Utilities
MockHost— mock implementation ofHoneHostfor unit testing pluginscreateTestBuffer— creates an in-memory buffer for test scenarios
Rust Plugin Host
Loads compiled plugin binaries at runtime. Exposes HoneHostAPI struct that implements the host interface, bridging plugin calls to IDE services.
Marketplace Client
API client for marketplace.hone.codes. Handles plugin search, download, and publish operations.
CLI
Command-line tools for plugin development: scaffolding, building, testing, and publishing plugins.
hone-extensions
11 built-in language extension packs (V1 system). Tests via npm test (Vitest).
Extensions
| # | Directory | Languages |
|---|---|---|
| 1 | typescript/ | TypeScript, JavaScript, JSX, TSX |
| 2 | python/ | Python |
| 3 | rust/ | Rust |
| 4 | go/ | Go |
| 5 | cpp/ | C, C++ |
| 6 | html-css/ | HTML, CSS, SCSS |
| 7 | json/ | JSON, JSONC |
| 8 | markdown/ | Markdown |
| 9 | docker/ | Dockerfile |
| 10 | toml-yaml/ | TOML, YAML |
| 11 | git/ | Git commit messages, gitignore, etc. |
Manifest Format
Each extension has a hone-extension.json manifest with the following structure:
Metadata:
id— unique extension identifiername— display nameversion— semver version stringpublisher— publisher identifierdescription— short descriptionlicense— SPDX license identifierengines— compatible Hone version rangemain— entry point file
Activation events:
Extensions declare when they activate. Examples:
onLanguage:typescriptonLanguage:pythononLanguage:rust
Contributes:
languages— language definitions (id, aliases, file extensions, language configuration)lspServers— LSP server configurationscommands— contributed commandssnippets— snippet files per languageconfiguration— settings schemakeybindings— keyboard shortcuts
Adding a New Extension
- Create a directory under
extensions/ - Add a
hone-extension.jsonmanifest - Define language contributions (file extensions, aliases, language configuration)
- Optionally configure an LSP server, commands, snippets
- Add to the extension registry
hone-themes
Package: @honeide/themes — 15 VSCode-compatible color themes as pure JSON data. 452 tests via npm test (Jest).
Themes
| Theme | File | Type |
|---|---|---|
| Hone Dark | hone-dark.json | Dark |
| Hone Light | hone-light.json | Light |
| Catppuccin | catppuccin.json | Dark |
| Dracula | dracula.json | Dark |
| GitHub Dark | github-dark.json | Dark |
| GitHub Light | github-light.json | Light |
| Gruvbox Dark | gruvbox-dark.json | Dark |
| High Contrast Dark | high-contrast-dark.json | Dark |
| High Contrast Light | high-contrast-light.json | Light |
| Monokai | monokai.json | Dark |
| Nord | nord.json | Dark |
| One Dark | one-dark.json | Dark |
| Solarized Dark | solarized-dark.json | Dark |
| Solarized Light | solarized-light.json | Light |
| Tokyo Night | tokyo-night.json | Dark |
JSON Schema
Each theme file follows the VSCode color theme format:
type—"dark"or"light"colors— object mapping UI color keys to hex values:editor.background,editor.foregroundactivityBar.backgroundsideBar.backgroundstatusBar.backgroundtab.activeBackground- and many more
tokenColors— array of token color rules, each with:scope— TextMate scope selector(s)settings—foreground(hex color),fontStyle(bold, italic, underline)
Tests
- Schema validation — every theme conforms to the expected structure
- WCAG contrast — foreground/background color pairs meet accessibility contrast ratios
- Completeness — all required color keys are present in every theme
Importing Themes
Any VSCode JSON theme can be imported directly — the format is compatible.
hone-auth
Auth service for Hone sync. Perry-compiled native binary (2.8 MB) with Fastify + MySQL.
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/auth/info | GET | Discovery — returns available auth methods |
/auth/login?email=... | GET | Creates magic link, sends email |
/auth/verify?token=...&deviceName=...&platform=... | GET | Verifies magic link, creates user + device, returns device token |
/auth/validate?token=... | GET | Validates a device token (returns userId + tier) |
/auth/me?token=... | GET | Returns user profile |
/projects?token=... | GET | Lists user’s registered projects |
/projects/register?token=...&projectKey=...&name=...&roomId=... | GET | Registers project for sync |
/devices?token=... | GET | Lists user’s registered devices |
/health | GET | Health check |
Magic-Link Flow
- Create a 64-char random hex token, store in the
magic_linkstable with a 15-minute expiry. - If SMTP is configured, send the link via email. Otherwise, the token is logged server-side (dev mode).
- On verify: validate the link (not already used, not expired), mark it as used, find or create the user, create a device record, and generate a device token.
Device Token Format
userId:deviceId:timestamp.hash
The hash is a double-djb2 HMAC:
djb2(String(djb2(secret + "|" + payload)) + "|" + secret + "|" + payload)
Database Schema
MySQL database on webserver.skelpo.net (user: hone). Tables:
usersdevicesmagic_linksprojectssubscriptions
All identifiers use camelCase.
Configuration
File: auth.conf
DB_HOST=webserver.skelpo.net
DB_USER=hone
DB_PASS=<password>
DB_NAME=hone
PORT=8445
AUTH_SECRET=<shared-secret>
AUTH_BASE_URL=https://auth.hone.codes
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<sendgrid-key>
SMTP_FROM=Hone <noreply@hone.dev>
Build
cd hone-auth && perry compile src/app.ts --output hone-auth
hone-relay
WebSocket sync relay. Routes messages between devices in the same room. 48 tests via bun test.
Protocol
Join a room:
{"type": "join", "room": "<roomId>", "device": "<deviceId>", "token": "<deviceToken>"}
Response:
{"type": "joined", "room": "<roomId>", "device": "<deviceId>"}
Send a message:
{"from": "<deviceId>", "to": "host|broadcast|<targetDeviceId>", "room": "<roomId>", "payload": {...}}
Message Targets
"host"— send to the room’s host device"broadcast"— send to all devices in the room except the sender"<deviceId>"— send directly to a specific device
Internals
- Room-based routing — messages are routed within rooms identified by the djb2 hash of roomId
- Slot tracking — each connection gets a slot with wsId, deviceId, and roomId
- Host election — the first device to join a room becomes the host
- Buffer — messages are buffered for 60 seconds for reconnecting devices
- Auth bypass — when
auth.secretis empty, token validation is skipped (dev mode) - Rate limiting — per-connection with configurable windows
- Stateless — rooms exist only while clients are connected
Configuration
File: relay.conf
host=0.0.0.0
port=8443
auth.secret=<shared-secret>
Ports
- 8443 — HTTP
- 8444 — WebSocket
Build
Perry-compiled or runs on Bun:
cd hone-relay && perry compile src/app.ts --output hone-relay
hone-build
Plugin build coordinator. Submits plugin source to perry-hub workers for cross-platform compilation. Perry-compiled native binary. 21 tests via bun test.
Purpose
When a plugin developer publishes a plugin, it needs to be compiled for all supported platforms. hone-build coordinates this by submitting compilation jobs to perry-hub workers, which run the Perry compiler for each target platform.
Features
- Artifact storage — stores compiled plugin binaries per platform
- Platform normalization — maps platform identifiers to Perry compilation targets
- Cross-platform compilation orchestration — fans out builds to perry-hub workers and collects results
Configuration
File: build.conf
PORT=8447
Build
cd hone-build && perry compile src/app.ts --output hone-build
hone-marketplace
Plugin marketplace server at marketplace.hone.codes. Perry-compiled native binary.
Features
- Plugin search and discovery — query available plugins by name, category, or keyword
- Plugin download — serve compiled binaries per platform
- Plugin publishing — submit plugins for review and distribution
- Version management — track and serve multiple versions per plugin
Port
8446
Build
cd hone-marketplace && perry compile src/app.ts --output hone-marketplace
Deployment
Runs on webserver.skelpo.net alongside the auth and relay services.
Plugin Development Overview
Hone supports two paths for extending the editor, each suited to different use cases.
V1 Built-in Extensions
Located in hone-extensions/. These ship with the IDE binary.
- Primarily for language support: LSP servers, syntax definitions, snippets
- Defined by a
hone-extension.jsonmanifest - Loaded by
ExtensionRegistryinhone-core - 11 built-in extensions ship today (TypeScript, Python, Rust, Go, C++, HTML/CSS, JSON, Markdown, Docker, TOML/YAML, Git)
Best for: core language features that every user needs out of the box.
V2 Independent Plugins
Located in hone-extension/ (the SDK package). These are standalone native plugins distributed through the Hone marketplace.
- Compiled to native binaries via Perry
- Use
@hone/sdk(HonePluginbase class) - Distributed and installed through the Hone marketplace
- Three execution tiers based on declared capabilities (InProcess, PluginHost, IsolatedProcess)
- Full access to editor, filesystem, UI, network, and process APIs depending on tier
Best for: third-party tools, custom UI panels, external integrations, anything beyond built-in language support.
Which Path to Choose
| Criteria | V1 Built-in | V2 Plugin |
|---|---|---|
| Ships with IDE | Yes | No (marketplace) |
| Language support (LSP, syntax) | Primary use case | Possible but not typical |
| Custom UI panels | No | Yes |
| Network access | No | Yes (Tier 3) |
| Process spawning | No | Yes (Tier 3) |
| Marketplace distribution | No | Yes |
| Requires Perry compilation | No (data-driven) | Yes |
Choose V1 if you are adding a language to the IDE itself. Choose V2 if you are building a standalone tool, integration, or any plugin for the marketplace.
V1 Built-in Extensions
Built-in extensions use hone-extension.json manifests and ship with the IDE binary. They are loaded by the ExtensionRegistry in hone-core.
Manifest Structure
Each extension lives in its own directory under extensions/<lang>/ and is defined by a hone-extension.json file:
{
"id": "hone.typescript",
"name": "TypeScript",
"version": "1.0.0",
"publisher": "hone",
"description": "TypeScript and JavaScript language support",
"license": "MIT",
"engines": { "hone": ">=1.0.0" },
"main": "src/index.ts",
"activationEvents": [
"onLanguage:typescript",
"onLanguage:javascript",
"onLanguage:typescriptreact",
"onLanguage:javascriptreact"
],
"contributes": {
"languages": [
{
"id": "typescript",
"aliases": ["TypeScript", "ts"],
"extensions": [".ts", ".mts", ".cts"],
"configuration": "./language-configuration.json"
}
],
"lspServers": [
{
"id": "typescript-language-server",
"command": "typescript-language-server",
"args": ["--stdio"],
"languages": ["typescript", "javascript", "typescriptreact", "javascriptreact"]
}
],
"commands": [],
"snippets": [],
"configuration": {},
"keybindings": []
}
}
Manifest Fields
| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier (convention: hone.<language>) |
name | Yes | Display name |
version | Yes | Semver version string |
publisher | Yes | Publisher identifier |
description | Yes | Short description |
license | Yes | SPDX license identifier |
engines.hone | Yes | Minimum compatible Hone version |
main | No | Entry point file (if extension has runtime code) |
activationEvents | Yes | Events that trigger extension loading |
Contribution Points
The contributes object declares what the extension provides:
- languages – Language definitions with file extensions, aliases, and configuration
- lspServers – LSP server configurations (command, args, supported languages)
- commands – Commands registered in the command palette
- snippets – Code snippets for the language
- configuration – Settings the extension exposes
- keybindings – Default keyboard shortcuts
Walkthrough: Adding a New Language
- Create a directory:
extensions/<lang>/ - Create
hone-extension.jsonwith the language metadata - Define language contributions: file extensions, aliases, language configuration
- Configure an LSP server if one exists (specify the command, args, and supported language IDs)
- Add commands, snippets, and keybindings as needed
- Register the extension in the extension system
Language Configuration
The language-configuration.json file controls editor behavior for the language:
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "'", "close": "'" }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}
Existing Built-in Extensions
| Extension | Languages | LSP Server |
|---|---|---|
| TypeScript | TypeScript, JavaScript, TSX, JSX | typescript-language-server |
| Python | Python | pylsp or pyright |
| Rust | Rust | rust-analyzer |
| Go | Go | gopls |
| C++ | C, C++, Objective-C | clangd |
| HTML/CSS | HTML, CSS, SCSS, Less | vscode-html-languageserver |
| JSON | JSON, JSONC | vscode-json-languageserver |
| Markdown | Markdown | – |
| Docker | Dockerfile, Docker Compose | – |
| TOML/YAML | TOML, YAML | – |
| Git | Git commit, Git rebase, .gitignore | – |
V2 Plugin SDK
The V2 plugin system uses @hone/sdk to build native Hone plugins compiled by Perry and distributed via the marketplace.
Installation
npm install @hone/sdk
Plugin Lifecycle
- init – Host calls
hone_plugin_init(host_api). The plugin stores the host API pointer for later use. - activate – Host calls
hone_plugin_activate(). The plugin initializes its state, registers commands, and sets up event handlers. - hooks – Host calls hook methods as events occur (e.g.,
hone_plugin_on_document_format(event_ptr),hone_plugin_on_command(cmd_ptr)). - deactivate – Host calls
hone_plugin_deactivate(). The plugin cleans up resources. - unload – Host calls
dlclose(). The plugin binary is unloaded from memory.
Basic Plugin Structure
import { HonePlugin } from '@hone/sdk';
import type { HoneHost } from '@hone/sdk';
export class MyPlugin extends HonePlugin {
activate(host: HoneHost): void {
host.log('info', 'My plugin activated');
host.commands.register('my-plugin.greet', 'Greet User', () => {
host.ui.notify({ message: 'Hello from my plugin!' });
});
}
deactivate(): void {
// Clean up resources
}
}
Host APIs
The HoneHost interface provides access to IDE functionality. Available APIs depend on declared capabilities – undeclared capabilities have null function pointers and will not be linked.
Editor
Requires editor.read and/or editor.write capabilities.
// Read buffer content
const text = host.editor.getBufferText(bufferId);
const lines = host.editor.getLines(bufferId, startLine, endLine);
const selection = host.editor.getSelection(bufferId);
const lineCount = host.editor.getLineCount(bufferId);
// Write edits (requires editor.write)
host.editor.submitEdits(bufferId, [
{ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, text: 'replacement' }
]);
Filesystem
Requires filesystem.read and/or filesystem.write capabilities.
// Read (patterns must match declared globs)
const content = host.fs.readText('/path/to/file.txt');
const exists = host.fs.exists('/path/to/file.txt');
const entries = host.fs.listDirectory('/path/to/dir');
// Write (requires filesystem.write)
host.fs.writeText('/path/to/file.txt', 'content');
host.fs.delete('/path/to/file.txt');
host.fs.createDirectory('/path/to/dir', { recursive: true });
UI
Requires various ui.* capabilities.
// Status bar (ui.statusbar)
const item = host.ui.createStatusBarItem({ text: 'My Plugin', alignment: 'left' });
host.ui.updateStatusBarItem(item, { text: 'Updated' });
// Panels (ui.panel)
const panel = host.ui.createPanel({
title: 'My Panel',
icon: 'list',
content: [
{ type: 'Heading', text: 'Results', level: 2 },
{ type: 'List', items: ['Item 1', 'Item 2'] }
]
});
// Notifications (ui.notifications)
host.ui.notify({ message: 'Operation complete', type: 'info' });
// Command palette (ui.commandPalette)
host.commands.register('my-plugin.action', 'My Action', callback);
Process
Requires process.spawn capability with allowlisted binaries.
const result = host.process.spawn('git', ['status', '--porcelain'], {
cwd: host.getWorkspacePath()
});
Network
Requires network capability (forces Tier 3 execution).
const response = host.network.httpRequest({
method: 'GET',
url: 'https://api.example.com/data',
headers: { 'Authorization': 'Bearer token' }
});
Custom UI Panels
Plugins can create panels with structured content using PanelElement types:
| Element | Description |
|---|---|
Text | Plain or styled text |
Heading | Section heading (levels 1-4) |
List | Ordered or unordered list |
Tree | Collapsible tree structure |
Table | Tabular data |
Input | Text input field |
Button | Clickable button |
Separator | Visual divider |
Progress | Progress bar or spinner |
CodeBlock | Syntax-highlighted code |
Group | Container for other elements |
Testing
Use MockHost and createTestBuffer from the SDK to test plugin logic without the full IDE:
import { MockHost, createTestBuffer } from '@hone/sdk';
const host = new MockHost();
const buffer = createTestBuffer('hello world');
const plugin = new MyPlugin();
plugin.activate(host);
// Assert plugin behavior against mock host
Compiling
Plugins are compiled to native shared libraries using Perry:
perry compile src/index.ts --output my-plugin
The output is a platform-specific shared library (.dylib on macOS, .dll on Windows, .so on Linux) that the Hone plugin host loads via dlopen/dlclose.
Perry AOT Constraints
Plugin code is compiled by Perry and must follow its constraints. Avoid:
- Optional chaining (
?.) – use explicit null checks - Nullish coalescing (
??) – useif (x !== undefined) - Dynamic key access (
obj[variable]) – useif/else ifper key for...ofon arrays – use index-basedforloops- ES6 shorthand properties (
{ key }) – use{ key: key } - Closures capturing
this– use module-level functions and variables
See the Perry constraints reference for the full list.
Capabilities & Tiers
Every V2 plugin declares its capabilities in the manifest. The declared capabilities determine the plugin’s execution tier, which controls isolation and security boundaries.
Execution Tiers
| Tier | Name | Description | Execution Model |
|---|---|---|---|
| 1 | InProcess | UI-only (themes, keymaps, color schemes) | Loaded in main process |
| 2 | PluginHost | Editor access, filesystem reads, UI elements | Shared plugin host process |
| 3 | IsolatedProcess | Network, filesystem writes, process spawning, terminal, webviews | Own sandboxed process |
Tiers are derived automatically from declared capabilities. You do not choose a tier directly.
Tier Derivation Rules
Tier 3 Triggers
Any of these capabilities forces the plugin into its own sandboxed process:
network: truefilesystem.write: trueprocess.spawn: [...](non-empty array)terminal: trueui.webview: true
Tier 2 Triggers
These capabilities place the plugin in the shared plugin host (unless a Tier 3 trigger is also present):
editor.read,editor.write,editor.decorationsfilesystem.read: [...](non-empty array)- Any
ui.*capability (panel, statusbar, gutter, commandPalette, contextMenu, notifications)
Tier 1
No code capabilities declared. The plugin is purely declarative data: theme JSON, keymap JSON, or other static contributions. Loaded directly in the main process with no isolation overhead.
All Capabilities
| Capability | Type | Description |
|---|---|---|
editor.read | boolean | Read buffer text, selections, language ID, line count |
editor.write | boolean | Submit edits (routed through the Changes Queue) |
editor.decorations | boolean | Underlines, highlights, gutter icons |
filesystem.read | string[] | Read files matching declared glob patterns |
filesystem.write | boolean | Write and delete files and directories |
network | boolean | HTTP requests to external services |
process.spawn | string[] | Spawn allowlisted external binaries |
terminal | boolean | Interactive terminal access |
ui.panel | boolean | Create side panels with structured content |
ui.statusbar | boolean | Add and update status bar items |
ui.gutter | boolean | Gutter icons next to line numbers |
ui.commandPalette | boolean | Register commands in the command palette |
ui.contextMenu | boolean | Add items to right-click context menus |
ui.notifications | boolean | Toast notifications |
ui.webview | boolean | Embedded webviews (Tier 3 only) |
Declaring Capabilities
Capabilities are declared in plugin.json:
{
"id": "com.example.my-formatter",
"name": "My Formatter",
"version": "1.0.0",
"capabilities": {
"editor": {
"read": true,
"write": true,
"decorations": true
},
"filesystem": {
"read": ["**/*.config.json"]
},
"ui": {
"statusbar": true,
"commandPalette": true,
"notifications": true
}
}
}
This plugin would be assigned Tier 2: it uses editor and UI capabilities but does not declare network, filesystem write, process spawn, terminal, or webview.
Enforcement Layers
Capabilities are enforced at three independent layers. All three must agree for an API call to succeed.
1. Compile-time (Perry)
Only API functions matching declared capabilities are linked into the plugin binary. Attempting to import an undeclared API causes a compile error. This is the earliest and strictest check.
2. Runtime (OS Sandbox)
OS-level restrictions are applied before the plugin binary loads:
| Platform | Mechanism |
|---|---|
| macOS | sandbox-exec profiles |
| Linux | seccomp-bpf filters |
| Windows | Job Objects + AppContainer |
The sandbox profile is generated from the declared capabilities. A Tier 3 plugin with network: true but no filesystem.write will have network access allowed but filesystem writes blocked at the OS level.
3. Host API (Null Pointers)
In the C ABI HoneHostAPI struct, function pointers for undeclared capabilities are set to null. If a plugin somehow bypasses the first two layers and calls an undeclared function pointer, it dereferences null and crashes. This is the final safety net.
Principle of Least Privilege
Declare only the capabilities your plugin actually needs. Fewer capabilities means:
- Lower tier (less isolation overhead, better performance)
- Smaller attack surface
- Easier review for marketplace approval
- More user trust (users see capability declarations before installing)
Host API (C ABI)
The plugin host communicates with plugins through a C ABI struct of function pointers. This page documents the raw interface between the host and plugin binaries.
Entry Point
Every plugin compiled by Perry exposes a single initialization function:
void hone_plugin_init(HoneHostAPI* host);
The host passes a HoneHostAPI struct populated with function pointers. Function pointers for undeclared capabilities are null.
HoneHostAPI Struct
typedef struct {
// ── Always available ──────────────────────────────────────
void (*log)(int32_t level, const uint8_t* msg, uint32_t msg_len);
int64_t (*get_config)(const uint8_t* key, uint32_t key_len);
int64_t (*get_workspace_path)(void);
// ── editor.read ───────────────────────────────────────────
int64_t (*buffer_get_text)(int64_t buffer_id);
int64_t (*buffer_get_lines)(int64_t buffer_id, int32_t start, int32_t end);
int64_t (*buffer_get_selection)(int64_t buffer_id);
int64_t (*buffer_get_selections)(int64_t buffer_id);
int64_t (*buffer_get_language_id)(int64_t buffer_id);
int64_t (*buffer_get_file_path)(int64_t buffer_id);
int32_t (*buffer_get_line_count)(int64_t buffer_id);
int64_t (*get_active_buffer_id)(void);
int64_t (*get_open_buffer_ids)(void);
// ── editor.write ──────────────────────────────────────────
int64_t (*buffer_submit_edits)(int64_t buffer_id, int64_t edits_ptr);
void (*buffer_set_selection)(int64_t buffer_id, int64_t sel_ptr);
void (*buffer_set_selections)(int64_t buffer_id, int64_t sels_ptr);
// ── editor.decorations ────────────────────────────────────
int32_t (*create_decoration_type)(int64_t opts_ptr);
void (*set_decorations)(int64_t buffer_id, int32_t type_id, int64_t ranges_ptr);
void (*clear_decorations)(int32_t type_id);
// ── filesystem.read ───────────────────────────────────────
int64_t (*file_read_text)(const uint8_t* path, uint32_t path_len);
int32_t (*file_exists)(const uint8_t* path, uint32_t path_len);
int64_t (*file_stat)(const uint8_t* path, uint32_t path_len);
int64_t (*directory_list)(const uint8_t* path, uint32_t path_len);
// ── filesystem.write ──────────────────────────────────────
void (*file_write_text)(const uint8_t* path, uint32_t path_len,
const uint8_t* content, uint32_t content_len);
void (*file_delete)(const uint8_t* path, uint32_t path_len);
void (*directory_create)(const uint8_t* path, uint32_t path_len,
int32_t recursive);
// ── process.spawn ─────────────────────────────────────────
int64_t (*spawn)(const uint8_t* cmd, uint32_t cmd_len,
int64_t args_ptr, int64_t opts_ptr);
// ── network ───────────────────────────────────────────────
int64_t (*http_request)(int64_t req_ptr);
// ── ui.statusbar ──────────────────────────────────────────
int32_t (*statusbar_create_item)(int64_t opts_ptr);
void (*statusbar_update_item)(int32_t id, int64_t opts_ptr);
void (*statusbar_remove_item)(int32_t id);
// ── ui.panel ──────────────────────────────────────────────
int32_t (*panel_create)(int64_t opts_ptr);
void (*panel_update)(int32_t id, int64_t content_ptr);
void (*panel_dispose)(int32_t id);
// ── ui.commandPalette ─────────────────────────────────────
void (*command_register)(const uint8_t* id, uint32_t id_len,
const uint8_t* title, uint32_t title_len);
void (*command_unregister)(const uint8_t* id, uint32_t id_len);
// ── ui.notifications ──────────────────────────────────────
void (*notify)(int64_t opts_ptr);
} HoneHostAPI;
String Encoding
All strings in the C ABI are UTF-8 encoded with explicit length. There are no null terminators.
Perry uses NaN-boxed StringHeader pointers internally. On the Rust side, the host extracts strings using str_from_header(), which reads the pointer and length from the NaN-boxed value.
Passing strings to the host: Provide a const uint8_t* pointer and a uint32_t length.
Receiving strings from the host: The host returns a NaN-boxed StringHeader pointer (int64_t). Use Perry’s string utilities to read the content.
Return Value Encoding
| Return Type | C Type | Notes |
|---|---|---|
| Strings | int64_t | NaN-boxed StringHeader pointer, allocated by host |
| Arrays | int64_t | NaN-boxed array pointer |
| Booleans | int32_t | 0 or 1 |
| IDs | int32_t | Opaque handle (decoration types, status bar items, panels) |
| Complex structs | int64_t | Pointer to host-allocated struct |
| Void | void | No return value |
Plugin Lifecycle Functions
The host expects these exported symbols from the plugin binary:
| Symbol | When Called | Purpose |
|---|---|---|
hone_plugin_init | Load time | Receives host API pointer |
hone_plugin_activate | After init | Plugin initializes state |
hone_plugin_deactivate | Before unload | Plugin cleans up resources |
Hook Functions
The host calls optional hook functions when relevant events occur. Plugins export only the hooks they handle:
| Symbol | Event |
|---|---|
hone_plugin_on_document_open | Document opened in editor |
hone_plugin_on_document_close | Document closed |
hone_plugin_on_document_change | Document content changed |
hone_plugin_on_document_save | Document saved |
hone_plugin_on_document_format | Format request |
hone_plugin_on_command | Registered command invoked |
hone_plugin_on_selection_change | Cursor/selection changed |
Each hook receives an int64_t pointer to an event struct allocated by the host. The plugin reads event data from this struct and must not free it.
FFI Conventions
These Perry FFI rules apply to all plugin code:
- String parameters are NaN-boxed
StringHeaderpointers. Rust receives*const u8and usesstr_from_header(). - Perry generates
__wrapper_<function_name>symbols (double underscore prefix). - All FFI functions must be listed in
package.jsonunderperry.nativeLibrary.functions. - Use
f64for numeric FFI parameters andi64for string/pointer parameters. Usingi32causes verifier errors.
Publishing Plugins
This page covers how to prepare, build, and publish a V2 plugin to the Hone marketplace.
Manifest Requirements
Every published plugin needs a complete plugin.json:
{
"id": "com.example.my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "A short description of what the plugin does",
"author": {
"name": "Your Name",
"email": "you@example.com"
},
"license": "MIT",
"engines": {
"hone": ">=1.0.0"
},
"main": "src/index.ts",
"capabilities": {
"editor": {
"read": true,
"write": true
},
"ui": {
"commandPalette": true,
"notifications": true
}
},
"contributes": {
"commands": [
{
"id": "my-plugin.run",
"title": "Run My Plugin"
}
]
}
}
Required Fields
| Field | Description |
|---|---|
id | Globally unique identifier (reverse-domain convention) |
name | Display name shown in the marketplace |
version | Semver version string |
description | Short description (shown in search results) |
author | Author name and contact |
license | SPDX license identifier |
engines.hone | Minimum compatible Hone version |
main | Entry point TypeScript file |
capabilities | Declared capabilities (determines execution tier) |
Validating the Manifest
Use validateManifest from @hone/sdk to check your manifest before submission:
import { validateManifest } from '@hone/sdk';
const errors = validateManifest('./plugin.json');
if (errors.length > 0) {
console.error('Manifest errors:', errors);
}
Cross-platform Compilation
When you submit a plugin, the build service (hone-build, port 8447) sends it to perry-hub workers for compilation. You submit TypeScript source code; the service produces native binaries for all supported platforms:
| Platform | Architectures |
|---|---|
| macOS | x86_64, aarch64 |
| Windows | x86_64 |
| Linux | x86_64, aarch64 |
| iOS | aarch64 (if applicable) |
| Android | aarch64 (if applicable) |
You do not need to compile for each platform yourself. The build service handles cross-compilation.
Publishing Flow
-
Develop and test locally. Compile with
perry compile src/index.ts --output my-pluginand test against a local Hone instance. -
Validate the manifest. Run
validateManifestto catch issues before submission. -
Submit to the marketplace. Use the CLI or the marketplace client from
@hone/sdk:hone plugin publishThis uploads your source to the build service, which compiles for all platforms and registers the result with the marketplace server (
hone-marketplace, port 8446). -
Build service compiles. perry-hub workers produce native shared libraries for each platform/architecture combination.
-
Plugin goes live. Once all platform builds succeed, the plugin appears in the Hone marketplace. Users can discover and install it from within the IDE.
Versioning
Follow semver:
- Patch (1.0.0 -> 1.0.1): Bug fixes, no API changes
- Minor (1.0.0 -> 1.1.0): New features, backward compatible
- Major (1.0.0 -> 2.0.0): Breaking changes
Each published version creates a new marketplace entry. Previous versions remain available. Users can:
- Auto-update within a major version (default)
- Pin to a specific version
- Roll back to any previously published version
Pre-publication Checklist
-
plugin.jsonpassesvalidateManifestwith no errors - Plugin compiles locally with
perry compile - All declared capabilities are actually used
- No unnecessary capabilities declared (principle of least privilege)
- Version number incremented from previous release
- Description and metadata are accurate
- License is specified and correct
Perry Constraints (Detailed)
Perry is an AOT (ahead-of-time) compiler that transforms TypeScript directly into native machine code. Because there is no runtime interpreter or JIT, certain JavaScript/TypeScript patterns that rely on runtime dynamism are not supported. This guide documents every constraint with explanations and corrected code.
1. Dynamic Key Access
Pattern: obj[variable]
Perry resolves all property accesses at compile time. A dynamic key like obj[key] requires runtime dispatch to determine which property to read — Perry has no mechanism for this.
// BROKEN — Perry cannot resolve the property at compile time
function getConfig(config: Config, key: string): string {
return config[key];
}
// FIXED — enumerate every possible key explicitly
function getConfig(config: Config, key: string): string {
if (key === 'host') return config.host;
else if (key === 'port') return config.port;
else if (key === 'protocol') return config.protocol;
return '';
}
This also applies to dynamic writes (obj[key] = value) and computed property access on arrays of objects.
2. Optional Chaining (?.)
Pattern: obj?.prop, arr?.[0], fn?.()
Perry does not implement the optional chaining operator. The compiler will fail or produce incorrect code.
// BROKEN
const name = user?.profile?.name;
// FIXED — explicit null/undefined checks
let name = '';
if (user !== null && user !== undefined) {
if (user.profile !== null && user.profile !== undefined) {
name = user.profile.name;
}
}
Every level of the chain needs its own guard. This is verbose but produces correct native code.
3. Nullish Coalescing (??)
Pattern: value ?? fallback
Not implemented in Perry’s codegen. The operator is not recognized.
// BROKEN
const port = config.port ?? 8080;
// FIXED
let port = 8080;
if (config.port !== undefined) {
port = config.port;
}
If you also need to guard against null:
let port = 8080;
if (config.port !== null && config.port !== undefined) {
port = config.port;
}
4. Regular Expressions
Pattern: /regex/, RegExp, .test(), .match(), .replace() with regex
Perry has no regex engine. Any code that constructs or uses a RegExp will fail.
// BROKEN
if (/^[a-z]+$/.test(input)) {
// ...
}
// FIXED — manual character checking
const ALPHA = 'abcdefghijklmnopqrstuvwxyz';
let allAlpha = true;
for (let i = 0; i < input.length; i++) {
if (ALPHA.indexOf(input.charAt(i)) < 0) {
allAlpha = false;
break;
}
}
For string splitting or searching, use indexOf, substring, and manual parsing loops.
// BROKEN
const parts = line.split(/\s+/);
// FIXED — split on single space, trim first
const trimmed = line.trim();
const parts: string[] = [];
let start = 0;
for (let i = 0; i <= trimmed.length; i++) {
if (i === trimmed.length || trimmed.charAt(i) === ' ') {
if (i > start) {
parts.push(trimmed.substring(start, i));
}
start = i + 1;
}
}
5. ES6 Shorthand Properties
Pattern: { name, age }
Perry requires explicit key-value pairs in object literals.
// BROKEN
const name = 'Alice';
const age = 30;
const user = { name, age };
// FIXED
const user = { name: name, age: age };
This applies everywhere: return statements, function arguments, variable declarations.
6. Array.map on Class Fields
Pattern: this.items.map(fn)
Perry cannot dispatch .map() on arrays stored as class instance fields. The method resolution fails at compile time.
// BROKEN
class UserList {
users: User[] = [];
getNames(): string[] {
return this.users.map(u => u.name);
}
}
// FIXED — use a for loop
class UserList {
users: User[] = [];
getNames(): string[] {
const names: string[] = [];
for (let i = 0; i < this.users.length; i++) {
names.push(this.users[i].name);
}
return names;
}
}
This also applies to .filter(), .reduce(), .find(), and other higher-order array methods on class fields. Use indexed for loops instead.
7. for...of on Arrays
Pattern: for (const item of items)
Perry does not implement the iterator protocol. for...of loops will not work on arrays.
// BROKEN
for (const item of items) {
process(item);
}
// FIXED
for (let i = 0; i < items.length; i++) {
const item = items[i];
process(item);
}
Also avoid for...in on objects — use explicit property access instead.
8. Character Range Comparisons
Pattern: c >= 'a' && c <= 'z'
Perry does not support ordering comparisons on characters (strings of length 1). The comparison operators work on numbers, not character codes.
// BROKEN
function isLowerAlpha(c: string): boolean {
return c >= 'a' && c <= 'z';
}
// FIXED
const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
function isLowerAlpha(c: string): boolean {
return LOWER_ALPHA.indexOf(c) >= 0;
}
For digits:
const DIGITS = '0123456789';
function isDigit(c: string): boolean {
return DIGITS.indexOf(c) >= 0;
}
9. Closure Capture Semantics
Pattern: Closures that read or mutate outer variables
Perry captures closure variables by value at the time the closure is created. Any subsequent mutation to the outer variable is invisible to the closure, and any mutation inside the closure does not affect the outer variable.
// BROKEN — closure captures initial value of this.count
class Counter {
count = 0;
setup() {
setInterval(() => {
this.count++; // increments a stale copy, not the real field
console.log(this.count);
}, 1000);
}
}
// FIXED — module-level state with named functions
let _count = 0;
function incrementCount() {
_count++;
console.log(_count);
}
setInterval(incrementCount, 1000);
Rules:
- Store mutable state in module-level
letvariables. - Access that state through module-level named functions (not closures).
- Never capture
thisin a closure passed tosetInterval, event handlers, or callbacks.
10. requestAnimationFrame
Pattern: requestAnimationFrame(callback)
RAF never fires in Perry. The Perry runtime does not have a browser-style event loop with frame callbacks.
// BROKEN — callback never executes
function animate() {
updateFrame();
requestAnimationFrame(animate);
}
// FIXED
setInterval(updateFrame, 16); // ~60fps
11. setTimeout Self-Recursion
Pattern: setTimeout called inside its own callback
When setTimeout is called inside the callback of a previous setTimeout, the new timeout only fires once and then stops. This breaks recursive polling patterns.
// BROKEN — fires once, then stops
function poll() {
doWork();
setTimeout(poll, 1000);
}
setTimeout(poll, 1000);
// FIXED
setInterval(doWork, 1000);
If you need to stop the interval later, capture the return value:
const id = setInterval(doWork, 1000);
// later:
clearInterval(id);
12. String-Returning Functions in Async Context
Pattern: Returning a string from an async function or a function called in async context
String values returned from async contexts are NaN-boxed StringHeader pointers. When the receiving code treats the return value as a string, it gets a numeric pointer instead.
// BROKEN
async function getName(): Promise<string> {
return fetchName(); // returns NaN-boxed pointer
}
// FIXED — inline the string operation at the call site
// instead of returning strings through async boundaries
Where possible, perform string operations directly where the result is needed rather than passing strings across async boundaries.
13. new Date() in Async Context
Pattern: new Date() inside an async function
The Date constructor malfunctions in async contexts due to how Perry handles object allocation across execution frames.
// BROKEN
async function getTimestamp(): Promise<number> {
const now = new Date();
return now.getTime();
}
// FIXED — Date.now() returns a plain number, no object allocation
async function getTimestamp(): Promise<number> {
return Date.now();
}
Date.now() is safe everywhere because it returns a primitive number, avoiding the object allocation issue.
Quick Reference
| Avoid | Use Instead |
|---|---|
obj[variable] | if/else if per key |
?. optional chaining | Explicit null checks |
?? nullish coalescing | if (x !== undefined) |
/regex/.test() | indexOf or char checks |
{ key } shorthand | { key: key } |
this.arr.map(fn) | for loop |
for...of on arrays | for (let i = 0; ...) |
c >= 'a' && c <= 'z' | ALPHA.indexOf(c) >= 0 |
Closures capturing this | Module-level functions + state |
requestAnimationFrame | setInterval |
setTimeout recursion | setInterval |
| String returns in async | Inline string ops |
new Date() in async | Date.now() |
FFI Conventions
Perry’s FFI (Foreign Function Interface) bridges TypeScript code and Rust native libraries. This is how the editor, terminal, and other packages call into platform-specific Rust code for rendering, input handling, and OS integration.
How It Works
TypeScript code calls functions declared as external. Perry compiles these calls into native function invocations against symbols in a linked Rust static library (.a file). The Rust side exports C-ABI functions that Perry can call directly.
NaN-Boxed String Parameters
Perry represents all values as 64-bit NaN-boxed floats internally. Strings are no exception — a string parameter is a NaN-boxed pointer to a StringHeader struct in memory.
On the Rust side, string parameters arrive as raw pointers:
#![allow(unused)]
fn main() {
use perry_runtime::str_from_header;
#[no_mangle]
pub extern "C" fn __wrapper_setTitle(title_ptr: i64) {
let title = unsafe { str_from_header(title_ptr as *const u8) };
// title is now a &str
}
}
The str_from_header() utility (from perry-runtime) extracts the string data from the NaN-boxed pointer.
Function Naming Convention
Perry generates wrapper symbols with a double-underscore prefix. A TypeScript declaration like:
declare function renderLine(text: string, lineNum: number, x: number, y: number): void;
becomes a call to the native symbol __wrapper_renderLine. The Rust implementation must use this exact name:
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn __wrapper_renderLine(
text: i64, // NaN-boxed string pointer
line_num: f64,
x: f64,
y: f64,
) {
let text_str = unsafe { str_from_header(text as *const u8) };
// render using platform APIs
}
}
package.json FFI Declarations
Every FFI function must be registered in the package’s package.json under perry.nativeLibrary.functions. This tells the Perry compiler which external symbols to expect during linking.
{
"name": "@honeide/editor",
"perry": {
"nativeLibrary": {
"name": "hone_editor_macos",
"functions": [
{
"name": "renderLine",
"params": ["i64", "i64", "f64", "f64"],
"returns": "void"
},
{
"name": "setViewportSize",
"params": ["f64", "f64"],
"returns": "void"
},
{
"name": "getClipboardText",
"params": [],
"returns": "i64"
},
{
"name": "setClipboardText",
"params": ["i64"],
"returns": "void"
}
]
}
}
}
The name field is the Rust crate name (and the resulting .a library file). The functions array lists every function the TypeScript side calls.
Parameter Type Rules
| FFI type | Use for | Rust type |
|---|---|---|
f64 | Numbers (integers, floats, booleans) | f64 |
i64 | Strings, pointers | i64 (cast to *const u8 for strings) |
void | No return value | () |
Do not use i32. It causes verifier errors in Perry’s code generator. Always use f64 for numeric values and i64 for string/pointer values, even if the logical type is a 32-bit integer.
#![allow(unused)]
fn main() {
// WRONG — i32 causes verifier errors
pub extern "C" fn __wrapper_setLine(line: i32) { ... }
// CORRECT
pub extern "C" fn __wrapper_setLine(line: f64) {
let line_num = line as i32; // cast inside the function
}
}
Return Values
Functions can return f64 (numbers), i64 (strings/pointers), or void. To return a string from Rust to TypeScript, allocate a StringHeader and return its pointer as i64:
#![allow(unused)]
fn main() {
use perry_runtime::alloc_string;
#[no_mangle]
pub extern "C" fn __wrapper_getClipboardText() -> i64 {
let text = get_clipboard_contents();
alloc_string(&text) as i64
}
}
Per-Platform Crates
Each platform has its own FFI crate that implements the same set of declared functions using native APIs:
| Platform | Crate | Location |
|---|---|---|
| macOS | hone_editor_macos | hone-editor/native/macos/ |
| iOS | hone_editor_ios | hone-editor/native/ios/ |
| Windows | hone_editor_windows | hone-editor/native/windows/ |
| Linux | hone_editor_linux | hone-editor/native/linux/ |
| Android | hone_editor_android | hone-editor/native/android/ |
All crates export the same __wrapper_* symbols, so the TypeScript code is platform-agnostic. Perry links the correct crate based on the --target flag.
Building Perry
Perry is the TypeScript-to-native AOT compiler that powers Hone. It lives in a sibling directory (../perry/) and is written in Rust. This page covers building Perry itself and using it to compile Hone packages.
Prerequisites
- Rust toolchain (install via rustup)
- Xcode Command Line Tools (macOS) for the system linker
- The Perry source at
../perry/relative to the Hone repo root
Building Perry Components
The Perry Compiler
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry
This produces the perry binary used to compile TypeScript to native code.
Perry UI Library (macOS)
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos
Required for compiling any package that renders UI on macOS (hone-ide, hone-editor).
Perry Standard Library
Rebuild after changing perry-runtime source:
cd ../perry && cargo clean -p perry-runtime --release && \
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry
The cargo clean step is necessary because Cargo may not detect changes to the runtime’s generated code.
LTO Warning
Always use CARGO_PROFILE_RELEASE_LTO=off for all Perry Rust builds. Without this flag, Cargo defaults to thin LTO, which produces LLVM bitcode that the macOS clang linker cannot read. The build will succeed but linking Hone packages will fail with cryptic errors.
You can also set this permanently in ../perry/.cargo/config.toml:
[profile.release]
lto = false
Compiling Hone Packages
Once Perry is built, use it to compile Hone’s TypeScript packages into native binaries.
IDE
# macOS native binary
cd hone-ide && perry compile src/app.ts --output hone-ide
# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone
# Web (WASM)
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html
Auth Server
cd hone-auth && perry compile src/app.ts --output hone-auth
Produces a ~2.8MB static binary with no runtime dependencies.
Marketplace
cd hone-marketplace && perry compile src/app.ts --output hone-marketplace
Build Server
cd hone-build && perry compile src/app.ts --output hone-build
Build Order
When building everything from scratch:
- Perry compiler —
cargo build --release -p perry - Perry stdlib —
cargo build --release -p perry-stdlib - Perry UI library —
cargo build --release -p perry-ui-macos(or target platform) - FFI crates — Build the native crates in
hone-editor/native/andhone-terminal/native/ - Hone packages —
perry compileeach package
Steps 1-3 only need to be repeated when Perry itself changes. Step 4 is needed when FFI code changes. Step 5 is the normal development cycle.
Troubleshooting
“unknown file type” linker error:
You forgot CARGO_PROFILE_RELEASE_LTO=off. Rebuild Perry with LTO disabled.
“undefined symbol _wrapper*” linker error:
A function declared in package.json under perry.nativeLibrary.functions is not exported by the Rust FFI crate. Check that the #[no_mangle] pub extern "C" fn __wrapper_<name> exists and matches exactly.
“verifier error” during compilation:
An FFI function uses i32 parameters. Change them to f64 (numbers) or i64 (strings/pointers).
Platform Targets
Perry compiles TypeScript to native binaries for multiple platforms. Each target uses platform-specific UI libraries and rendering backends.
Supported Platforms
| Platform | Target Flag | UI Library | Rendering Backend |
|---|---|---|---|
| macOS | (default, no flag) | perry-ui-macos | Metal, CoreGraphics, CoreText |
| iOS | --target ios-simulator | perry-ui-ios | UIKit, CoreGraphics, CoreText |
| Windows | --target windows | perry-ui-windows | Direct2D, DirectWrite, DirectComposition |
| Linux | --target linux | perry-ui-linux | Cairo, Pango |
| Android | --target android | perry-ui-android | Android NDK |
| Web | --target web | (WASM) | WebAssembly, Canvas/WebGL |
Compile-Time Platform Detection
Perry provides the __platform__ compile-time constant. Code guarded by platform checks is dead-code-eliminated for non-matching platforms.
| Value | Platform |
|---|---|
| 0 | macOS |
| 1 | iOS |
| 2 | Android |
declare const __platform__: number;
function getDefaultFontSize(): number {
if (__platform__ === 0) {
return 13; // macOS — standard desktop size
} else if (__platform__ === 1) {
return 16; // iOS — larger for touch
} else if (__platform__ === 2) {
return 16; // Android — larger for touch
}
return 13;
}
Because __platform__ is resolved at compile time, the non-matching branches are stripped entirely from the binary. This means platform-specific imports and FFI calls in dead branches do not need to link.
Per-Platform FFI Crates
Each platform has dedicated Rust crates that implement the FFI functions using native APIs. These crates all export the same __wrapper_* symbols, so TypeScript code remains platform-agnostic.
Editor FFI crates:
| Platform | Crate | Key APIs |
|---|---|---|
| macOS | hone-editor/native/macos/ | CoreText for text shaping, Metal for GPU rendering |
| iOS | hone-editor/native/ios/ | CoreText for text, UIKit for input |
| Windows | hone-editor/native/windows/ | DirectWrite for text, Direct2D for rendering |
| Linux | hone-editor/native/linux/ | Pango for text, Cairo for rendering |
| Android | hone-editor/native/android/ | Android NDK Canvas, Skia |
Terminal FFI crates follow the same pattern in hone-terminal/native/.
Compilation Examples
# macOS (default target)
perry compile src/app.ts --output hone-ide
# iOS Simulator
perry compile src/app.ts --target ios-simulator --output Hone
# Web
perry compile src/app.ts --target web --output hone-ide.html
UI Testing by Platform
macOS — geisterhand
geisterhand is the UI automation tool for macOS Perry binaries.
# Capture screenshot
geisterhand screenshot --output /tmp/shot.png
# Click at pixel coordinates
geisterhand click 200 350
# Type text
geisterhand type "hello world"
iOS Simulator — AppleScript
For iOS Simulator targets, use osascript (AppleScript) to drive the Simulator app:
# Tap at coordinates in Simulator
osascript -e 'tell application "Simulator" to activate'
# Use Accessibility APIs or xcrun simctl for interaction
geisterhand does not work with the iOS Simulator — it only targets native macOS windows.
Output Artifacts
| Target | Output | Size (typical) |
|---|---|---|
| macOS | Static binary | 5-15 MB |
| iOS | .app bundle | 8-20 MB |
| Web | .html + .wasm | 2-5 MB |
| Auth server (macOS) | Static binary | ~2.8 MB |
All native binaries are statically linked — no runtime dependencies, no Node.js, no V8. The binary includes the Perry stdlib, platform UI library, and all FFI crates.
Running the Auth Service
The auth service (hone-auth) handles user authentication, device pairing, and subscription management. It runs as a native Perry binary on port 8445.
Configuration
Create auth.conf in the working directory with KEY=VALUE pairs:
DB_HOST=webserver.skelpo.net
DB_USER=hone
DB_PASS=<password>
DB_NAME=hone
PORT=8445
AUTH_SECRET=<shared-secret>
AUTH_BASE_URL=https://auth.hone.codes
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<sendgrid-key>
SMTP_FROM=Hone <noreply@hone.dev>
| Key | Required | Description |
|---|---|---|
DB_HOST | Yes | MySQL server hostname |
DB_USER | Yes | MySQL username |
DB_PASS | Yes | MySQL password |
DB_NAME | Yes | MySQL database name |
PORT | No | Listen port (default: 8445) |
AUTH_SECRET | Yes | Shared secret for JWT signing. Must match the relay service’s auth.secret. |
AUTH_BASE_URL | Yes | Public URL for magic-link callbacks |
SMTP_HOST | No | SMTP server for sending emails |
SMTP_PORT | No | SMTP port (default: 587) |
SMTP_USER | No | SMTP username |
SMTP_PASS | No | SMTP password |
SMTP_FROM | No | From address for emails |
Database Setup
The auth service uses MySQL. Create the database and user:
CREATE DATABASE hone;
CREATE USER 'hone'@'%' IDENTIFIED BY '<password>';
GRANT ALL PRIVILEGES ON hone.* TO 'hone'@'%';
Tables are created automatically on first startup. The schema uses camelCase for all identifiers (table names, column names).
Running
cd /opt/hone-auth && ./hone-auth
The service reads auth.conf from its working directory.
Health Check
GET /health
Returns 200 OK when the service is running and the database connection is healthy.
Authentication Flow
- Magic link request: Client sends email address to
POST /auth/magic-link. The service generates a one-time token and emails a login link. - Token verification: User clicks the link, which hits
GET /auth/verify?token=<token>. The service validates the token and returns a JWT. - Device pairing: Client registers a device with
POST /auth/devicesusing the JWT. The service returns a device token used for relay authentication.
Development Mode
If SMTP configuration is omitted, magic-link tokens are logged to stdout instead of emailed. This enables local development without an email provider.
[dev] Magic link token for user@example.com: abc123def456
Shared Secret
The AUTH_SECRET value is critical for the system to work end-to-end:
- The auth service uses it to sign JWTs and device tokens.
- The relay service uses the same secret (as
auth.secretinrelay.conf) to validate device tokens locally, without making HTTP calls back to the auth service.
If these values do not match, devices will fail to authenticate with the relay.
Running the Relay Service
The relay service (hone-relay) provides real-time cross-device synchronization via WebSocket. It manages rooms, buffers messages for reconnecting devices, and persists deltas to SQLite.
Configuration
Create relay.conf in the working directory:
host=0.0.0.0
port=8443
auth.secret=<shared-secret>
| Key | Required | Description |
|---|---|---|
host | No | Bind address (default: 0.0.0.0) |
port | No | HTTP port (default: 8443) |
auth.secret | No | Shared secret for token validation. Must match auth service’s AUTH_SECRET. |
Ports
| Port | Protocol | Purpose |
|---|---|---|
| 8443 | HTTP | REST API, health checks |
| 8444 | WebSocket | Real-time sync connections |
Running
cd /opt/hone-relay && ./hone-relay
The service reads relay.conf from its working directory.
Development Mode
Leave auth.secret empty or omit it entirely to disable token validation. All connections will be accepted without authentication. This is useful for local development.
host=127.0.0.1
port=8443
auth.secret=
Sync Protocol
Devices connect via WebSocket and join rooms identified by project ID. The relay:
- Authenticates the device token against the shared secret (or skips in dev mode).
- Assigns the connection to a project room.
- Forwards deltas from any device in the room to all other devices.
- Persists deltas to SQLite for offline reconciliation.
- Buffers messages for 60 seconds for devices that temporarily disconnect.
Message Buffering
When a device disconnects, the relay holds its pending messages for 60 seconds. If the device reconnects within that window, it receives the buffered messages and resumes without data loss. After 60 seconds, the buffer is dropped and the device must do a full reconciliation from SQLite on reconnect.
Rate Limiting
Rate limiting is per-connection with configurable windows. Under default settings, a single device can send up to 100 messages per second. This is sufficient for real-time typing sync without overwhelming other devices in the room.
SQLite Persistence
Deltas are stored in a local SQLite database (relay.db in the working directory). This provides:
- Offline reconciliation: Devices that were offline for longer than the 60-second buffer window can catch up from persisted deltas.
- Audit trail: All changes are recorded with timestamps and device IDs.
The database file is created automatically on first startup.
Tests
cd hone-relay && bun test
Runs 48 tests covering auth, buffer management, WebSocket hub, and configuration parsing.
Running the Marketplace
The marketplace service (hone-marketplace) powers marketplace.hone.codes — the plugin registry for Hone V2 extensions.
Overview
| Property | Value |
|---|---|
| Binary | hone-marketplace |
| Port | 8446 |
| Domain | marketplace.hone.codes |
Running
cd /opt/hone-marketplace && ./hone-marketplace
Capabilities
The marketplace provides:
- Search: Full-text search of published plugins by name, description, and tags.
- Download: Retrieve compiled plugin binaries for a specific platform target.
- Publish: Upload new plugin versions (authenticated via auth service tokens).
- Metadata: Plugin details, version history, download counts, ratings.
Plugin Lifecycle
- A plugin author develops a V2 plugin using the
@hone/sdkpackage (hone-extension/). - The author submits the plugin source to the build service, which compiles it for all supported platforms via perry-hub workers.
- The compiled artifacts are uploaded to the marketplace via
POST /plugins. - Users discover plugins through the IDE’s extensions panel or the marketplace website.
- The IDE downloads the appropriate platform binary from the marketplace.
Compilation
cd hone-marketplace && perry compile src/app.ts --output hone-marketplace
Running the Build Service
The build service (hone-build) coordinates cross-platform plugin compilation. It accepts plugin source code and dispatches compilation jobs to perry-hub workers.
Overview
| Property | Value |
|---|---|
| Binary | hone-build |
| Port | 8447 |
Configuration
Create build.conf in the working directory:
PORT=8447
Running
cd /opt/hone-build && ./hone-build
How It Works
- A plugin author submits source code to the build service.
- The build service creates compilation jobs for each supported platform target (macOS, iOS, Windows, Linux, Android, Web).
- Jobs are dispatched to perry-hub workers — remote machines with the Perry compiler installed for each target platform.
- Workers compile the plugin and return the platform-specific binary artifacts.
- The build service collects all artifacts and stores them for retrieval by the marketplace.
Compilation
cd hone-build && perry compile src/app.ts --output hone-build
Tests
cd hone-build && bun test
Runs 21 tests covering artifact storage and platform normalization.
Deployment
All Hone backend services run on a single server and are managed by systemd. TLS is terminated by nginx with Let’s Encrypt certificates.
Server
| Property | Value |
|---|---|
| IP | 84.32.223.50 |
| OS | Ubuntu 24.04 |
| Host | webserver.skelpo.net |
Services
| Service | Binary Path | Port | Systemd Unit | Domain |
|---|---|---|---|---|
| Auth | /opt/hone-auth/hone-auth | 8445 | hone-auth.service | auth.hone.codes |
| Relay (HTTP) | /opt/hone-relay/hone-relay | 8443 | hone-relay.service | sync.hone.codes |
| Relay (WS) | (same binary) | 8444 | (same) | sync.hone.codes/ws |
| Marketplace | /opt/hone-marketplace/hone-marketplace | 8446 | hone-marketplace.service | marketplace.hone.codes |
| Build | /opt/hone-build/hone-build | 8447 | hone-build.service | — |
DNS
A records pointing to 84.32.223.50:
auth.hone.codessync.hone.codesmarketplace.hone.codes
TLS
Certificates are provisioned by Let’s Encrypt via certbot and auto-renewed. nginx terminates TLS and proxies to the service ports over localhost HTTP.
nginx Configuration
Standard HTTP Service (Auth, Marketplace)
server {
listen 443 ssl;
server_name auth.hone.codes;
ssl_certificate /etc/letsencrypt/live/auth.hone.codes/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.hone.codes/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8445;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
WebSocket Service (Relay)
server {
listen 443 ssl;
server_name sync.hone.codes;
ssl_certificate /etc/letsencrypt/live/sync.hone.codes/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sync.hone.codes/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws {
proxy_pass http://127.0.0.1:8444;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
}
}
The proxy_read_timeout on the WebSocket location prevents nginx from closing idle connections.
Systemd Units
Each service has a systemd unit file in /etc/systemd/system/.
Example: hone-auth.service
[Unit]
Description=Hone Auth Service
After=network.target mysql.service
[Service]
Type=simple
WorkingDirectory=/opt/hone-auth
ExecStart=/opt/hone-auth/hone-auth
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Common Operations
# Start a service
sudo systemctl start hone-auth
# Stop a service
sudo systemctl stop hone-auth
# Restart after deploying a new binary
sudo systemctl restart hone-auth
# View logs
sudo journalctl -u hone-auth -f
# Enable service on boot
sudo systemctl enable hone-auth
Database
MySQL runs on the same server (webserver.skelpo.net). Only the auth service connects to MySQL directly. The relay uses local SQLite.
| Property | Value |
|---|---|
| Host | webserver.skelpo.net |
| User | hone |
| Database | hone |
Landing Page
The landing page (hone.codes) is a static index.html with no build step:
scp landing/index.html root@webserver.skelpo.net:/var/www/hone.codes/
Deploying a New Binary
-
Compile the package on the build machine:
cd hone-auth && perry compile src/app.ts --output hone-auth -
Upload the binary:
scp hone-auth root@webserver.skelpo.net:/opt/hone-auth/hone-auth -
Restart the service:
ssh root@webserver.skelpo.net 'systemctl restart hone-auth' -
Verify:
curl https://auth.hone.codes/health
Test Runners
Hone uses per-package test runners. There is no monorepo-level test command — each package must be tested independently.
Test Matrix
| Package | Runner | Command | Test Count | Import |
|---|---|---|---|---|
| hone-core | Bun | bun test | 649+ | import { describe, it, expect } from 'bun:test' |
| hone-editor | Bun | bun test | 353 | import { describe, it, expect } from 'bun:test' |
| hone-terminal | Bun | bun test | 163 | import { describe, it, expect } from 'bun:test' |
| hone-relay | Bun | bun test | 48 | import { describe, it, expect } from 'bun:test' |
| hone-build | Bun | bun test | 21 | import { describe, it, expect } from 'bun:test' |
| hone-themes | Jest | npm test | 452 | import { describe, it, expect } from '@jest/globals' |
| hone-extensions | Vitest | npm test | — | import { describe, it, expect } from 'vitest' |
| hone-api | tsc | npm test | — | (type-check only, no runtime tests) |
Running Tests
All tests in a package
cd hone-core && bun test
cd hone-editor && bun test
cd hone-terminal && bun test
cd hone-relay && bun test
cd hone-build && bun test
cd hone-themes && npm test
cd hone-extensions && npm test
cd hone-api && npm test
A single test file
cd hone-editor && bun test tests/buffer.test.ts
With Bun, pass the file path as an argument. Bun matches on filename, so partial names also work:
bun test buffer # runs all test files containing "buffer" in the name
Type checking
cd <package> && bun run typecheck
# or
npx tsc --noEmit
Important: Use the Correct Runner
The runner matters. Do not use npx vitest or npx jest in Bun-based packages — they will fail because test files import from bun:test, which only Bun provides.
| If the test file imports from… | Use this runner |
|---|---|
bun:test | bun test |
@jest/globals or no import (Jest globals) | npm test (Jest) |
vitest | npm test (Vitest) |
Writing Tests (Bun Packages)
import { describe, it, expect, beforeEach } from 'bun:test';
describe('PieceTable', () => {
let table: PieceTable;
beforeEach(() => {
table = new PieceTable('hello world');
});
it('should insert text', () => {
table.insert(5, ' beautiful');
expect(table.getText()).toBe('hello beautiful world');
});
it('should delete text', () => {
table.delete(5, 6);
expect(table.getText()).toBe('hello');
});
});
What Each Package Tests
| Package | Coverage Areas |
|---|---|
| hone-core | Workspace management, settings, git integration, search, LSP client, DAP client, AI service, extension host |
| hone-editor | Piece table buffer, multi-cursor editing, undo/redo, viewport calculations, tokenizer, search & replace, code folding, diff algorithm, LSP integration, DAP integration |
| hone-terminal | VT100/VT220 escape sequence parser, terminal buffer, keyboard input handling, terminal emulator integration |
| hone-relay | Token authentication, message buffering, WebSocket hub, configuration parsing |
| hone-build | Artifact storage, platform target normalization |
| hone-themes | JSON schema validation for all 11 themes, WCAG contrast ratio compliance |
| hone-extensions | Built-in extension functionality |
| hone-api | Type correctness of the public extension API (compile-time only) |
UI Testing
Hone’s UI is rendered natively on each platform. Since there is no browser DOM, UI testing uses platform-specific automation tools.
macOS — geisterhand
geisterhand is the UI automation tool for testing Perry-compiled macOS binaries. It interacts with native windows at the pixel level.
Taking Screenshots
geisterhand screenshot --output /tmp/shot.png
Captures the frontmost window to a PNG file. Use this to verify visual state during tests.
Clicking
geisterhand click 200 350
Sends a mouse click at the specified (x, y) pixel coordinates relative to the screen.
Typical Test Flow
- Launch the Perry-compiled binary.
- Wait for the window to appear.
- Take a screenshot to verify initial state.
- Perform interactions (clicks, key presses).
- Take another screenshot to verify the result.
# Launch the IDE
./hone-ide &
# Wait for window
sleep 2
# Screenshot the initial state
geisterhand screenshot --output /tmp/initial.png
# Click the file explorer icon in the activity bar
geisterhand click 25 60
# Screenshot after interaction
geisterhand screenshot --output /tmp/after-click.png
iOS Simulator
For iOS Simulator targets, use AppleScript (osascript) to automate interactions. geisterhand does not work with the Simulator.
# Activate the Simulator
osascript -e 'tell application "Simulator" to activate'
For programmatic interaction with the simulated device, use xcrun simctl:
# Boot a simulator
xcrun simctl boot "iPhone 15 Pro"
# Install the app
xcrun simctl install booted Hone.app
# Launch the app
xcrun simctl launch booted codes.hone.ide
# Take a screenshot
xcrun simctl io booted screenshot /tmp/ios-shot.png
Limitations
- No DOM testing: There is no browser, no
document, noquerySelector. All UI assertions are visual (screenshot comparison) or coordinate-based. - No accessibility tree (yet): Automated assertions currently rely on pixel coordinates and screenshot diffs rather than semantic element queries.
- Platform-specific: Each platform requires its own automation toolchain. macOS uses geisterhand, iOS uses simctl/osascript, and other platforms have their own tools.
Dev Environment Setup
Hone is a monorepo of independent packages with no workspace manager. Each package manages its own dependencies.
Prerequisites
| Tool | Purpose | Install |
|---|---|---|
| Bun | Runtime and test runner for core packages | curl -fsSL https://bun.sh/install | bash |
| Rust | Building Perry compiler and native FFI crates | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Node.js | Runtime for hone-themes (Jest), hone-extensions (Vitest), hone-api (tsc) | Download from nodejs.org or use nvm |
| Xcode CLI Tools | macOS system linker (required for Perry) | xcode-select --install |
Clone the Repository
git clone https://github.com/HoneIDE/hone.git
cd hone
Build the Perry Compiler
Perry lives in a sibling directory. Clone and build it first:
cd ..
git clone https://github.com/nicktypemern/perry.git
cd perry
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos
cd ../hone
The CARGO_PROFILE_RELEASE_LTO=off flag is mandatory — without it, the macOS linker cannot process the resulting bitcode.
Install Dependencies
Each package manages its own node_modules. Install them individually:
# Bun-based packages
cd hone-core && bun install && cd ..
cd hone-editor && bun install && cd ..
cd hone-terminal && bun install && cd ..
cd hone-relay && bun install && cd ..
cd hone-build && bun install && cd ..
# Node-based packages
cd hone-themes && npm install && cd ..
cd hone-extensions && npm install && cd ..
cd hone-api && npm install && cd ..
There is no top-level package.json or npm install at the repo root.
Verify the Setup
Run tests in a couple of packages to confirm everything works:
cd hone-core && bun test
cd ../hone-editor && bun test
If tests pass, your environment is ready.
Optional: Build the IDE
Once Perry is built and dependencies are installed, you can compile the IDE:
cd hone-ide && perry compile src/app.ts --output hone-ide
./hone-ide
This produces a native macOS binary that launches the full IDE.
Troubleshooting
bun: command not found:
Bun is not on your PATH. Run the install script again or add ~/.bun/bin to your PATH.
perry: command not found:
The Perry binary is at ../perry/target/release/perry. Either add it to your PATH or use the full path.
Linker errors when compiling with Perry:
Rebuild Perry with LTO disabled: CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry.
Test import errors (Cannot find module 'bun:test'):
You are running the test with the wrong runner. Use bun test, not npx vitest or npx jest, for packages that import from bun:test.
Conventions
Coding and project conventions for contributing to Hone.
Perry-Safe TypeScript
All code that will be compiled by Perry must avoid the constrained patterns. The key rules:
- No optional chaining (
?.) — use explicit null checks. - No nullish coalescing (
??) — useif (x !== undefined). - No dynamic key access (
obj[variable]) — use if/else chains. - No
for...ofon arrays — use indexedforloops. - No regex — use
indexOfand character checks. - No ES6 shorthand properties — write
{ key: key }. - No
Array.map/filter/reduceon class fields — useforloops. - No closures capturing
this— use module-level state and named functions.
When in doubt, prefer explicit and verbose over concise and clever.
Database Schemas
All database identifiers use camelCase:
CREATE TABLE magicLinks (
id INT PRIMARY KEY AUTO_INCREMENT,
userId INT NOT NULL,
tokenHash VARCHAR(64) NOT NULL,
expiresAt BIGINT NOT NULL,
createdAt BIGINT NOT NULL
);
This applies to table names, column names, and index names.
Test Imports
Bun-based packages (hone-core, hone-editor, hone-terminal, hone-relay, hone-build) import test utilities from bun:test:
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
Do not import from vitest, jest, or @jest/globals in these packages.
Platform Detection
Use the __platform__ compile-time constant for platform-specific code:
declare const __platform__: number;
// 0 = macOS, 1 = iOS, 2 = Android
if (__platform__ === 0) {
// macOS-specific code
}
Non-matching branches are dead-code-eliminated at compile time.
Configuration Files
Service configuration uses KEY=VALUE format in .conf files:
PORT=8445
DB_HOST=webserver.skelpo.net
AUTH_SECRET=my-secret
No quotes around values. No section headers. One key-value pair per line.
Project Structure
- No Rust in hone-ide. All Rust code is in
../perry/(compiler, runtime, UI libraries) or inhone-editor/native/andhone-terminal/native/(FFI crates). The IDE package is pure TypeScript. - No workspace manager. Each package is independent with its own
package.json, its ownnode_modules, and its own test runner. - No shared dependencies. Packages do not hoist or share
node_modules. Install dependencies in each package individually.
Closure Rule
Perry captures closure variables by value at creation time. Mutations after closure creation are invisible to the closure.
// WRONG — Perry captures the initial value of count
let count = 0;
setInterval(() => { count++; }, 1000); // always increments from 0
// RIGHT — module-level named function reads current state
let _count = 0;
function tick() { _count++; }
setInterval(tick, 1000);
Store mutable state in module-level let variables. Access it through module-level named functions. Never capture this in a closure.
FFI Conventions
- Use
f64for numeric parameters,i64for string/pointer parameters. Neveri32. - Functions are exported as
__wrapper_<name>symbols. - All FFI functions must be declared in
package.jsonunderperry.nativeLibrary.functions.
See FFI Conventions for full details.