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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,34 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --no-save || true
# `perry/i18n` and `@honeide/*/perry/live` resolution warnings are
# expected (per CLAUDE.md / followup.md) — they're Perry FFI manifest
# imports the typecheck can't resolve. Filter them so the action
# doesn't fail on noise that's been audited.
- name: typecheck (filtered)
# @honeide/core|editor|terminal are `file:../hone-*` siblings that live in
# their own repos, so a lone checkout of this repo cannot resolve them and
# the typecheck fails with TS2307 on CI while passing on a dev machine.
# That mismatch is why the old noise filter listed `@honeide/.+/perry/live`
# as "expected" — it was papering over a broken CI environment, not a
# broken import. release.yml already clones these; do the same here.
- name: Clone dependencies
run: |
set -o pipefail
bun run typecheck 2>&1 | tee tc.out
# Count errors that aren't pre-existing noise.
REAL=$(grep -E "error" tc.out | grep -vE "perry/i18n|@honeide/.+/perry/live|widgetSetTooltip|clipboardWrite|editorSet[A-Z]|getContent|setLineBackground|clearLineBackgrounds|clearFindHighlights|setFindHighlights|getCharWidth|visibleRange|getCursorLine|getCursorColumn|setCursorPosition|ViewportManager|executeCommand|clipboardRead|EditorOptions|getCurrentSelection|dispose|setLanguage|getFindMatch|getFindCurrent|getScrollTop|setScrollTop|getCursorPos|Cannot find name 't[0-9]'|setLineDiagnostics|clearDiagnostics|pushDecorations|setThemeMode" | wc -l)
echo "Non-noise errors: $REAL"
# Tolerate up to N pre-existing noise patterns we haven't fully
# categorized. Bump down as the project quiets the rest.
if [ "$REAL" -gt 0 ]; then
echo "::error::New typecheck errors detected outside the noise filter."
grep -E "error" tc.out | grep -vE "perry/i18n|@honeide/.+/perry/live|widgetSetTooltip|clipboardWrite|editorSet[A-Z]|getContent|setLineBackground|clearLineBackgrounds|clearFindHighlights|setFindHighlights|getCharWidth|visibleRange|getCursorLine|getCursorColumn|setCursorPosition|ViewportManager|executeCommand|clipboardRead|EditorOptions|getCurrentSelection|dispose|setLanguage|getFindMatch|getFindCurrent|getScrollTop|setScrollTop|getCursorPos|Cannot find name 't[0-9]'|setLineDiagnostics|clearDiagnostics|pushDecorations|setThemeMode" | head -20
exit 1
fi
git clone --depth 1 "$HONE_CORE_REPO" ../hone-core
git clone --depth 1 "$HONE_EDITOR_REPO" ../hone-editor
git clone --depth 1 "$HONE_TERMINAL_REPO" ../hone-terminal
rm -rf ../hone-core/.git ../hone-editor/.git ../hone-terminal/.git
env:
HONE_CORE_REPO: ${{ secrets.HONE_CORE_REPO }}
HONE_EDITOR_REPO: ${{ secrets.HONE_EDITOR_REPO }}
HONE_TERMINAL_REPO: ${{ secrets.HONE_TERMINAL_REPO }}
- run: bun install --no-save || true
# The typecheck is clean and gates for real. The previous version of this
# step piped tsc through a ~30-pattern "known noise" filter, but the pipe
# ran under `set -o pipefail`, so a non-zero tsc exit killed the step
# before the filter was ever consulted — the job had failed on every run
# since May and gated nothing. The noise it filtered is gone: `perry/i18n`
# and `widgetSetTooltip` now have real declarations in src/types/, the
# agentic seed fixtures are excluded in tsconfig, and the ViewportManager /
# textSetFontFamily errors it hid were genuine bugs (now fixed). Keep this
# step plain — if it goes red, something is actually broken.
- name: typecheck
run: bun run typecheck

perry-compile-macos:
name: Perry compile macOS
Expand Down
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,23 @@ PROJECT_PLAN.md
stderr*.log
screenshot*.png
.perry/
.perry-cache/
.claude/

# Ad-hoc Perry compile outputs (app bundles, web/html, JS bundle, debug PE)
*.app/
*.html
__perry_js_bundle.js
hone-ide-debug

# Bare-name one-off Perry compile outputs
HoneIDE
editor-component
hone-ide-baseline
hone-min
paths
render
settings
sidebar-render
telemetry
ui-helpers
Binary file removed hone-ide-debug
Binary file not shown.
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@honeide/ide",
"version": "0.1.0",
"private": true,
"bundleId": "com.hone.ide",
"description": "Hone IDE — AI-native code editor for all platforms",
"main": "src/app.ts",
Expand All @@ -15,10 +16,21 @@
"dependencies": {
"@honeide/core": "file:../hone-core",
"@honeide/editor": "file:../hone-editor",
"@honeide/lsp-bridge": "file:./native/lsp",
"@honeide/terminal": "file:../hone-terminal"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.7.0"
},
"perry": {
"//": "Perry >=0.5.1235 refuses to link a dependency's perry.nativeLibrary unless the host allow-lists it (PerryTS/perry#497). Both of these are our own first-party crates — the editor's Core Text/tree-sitter renderer and the terminal's PTY. Without this, `perry compile` fails and the only way through is PERRY_ALLOW_PERRY_FEATURES=1, which disables the check globally rather than approving these two. Entries must match the IMPORT SPECIFIER, not the package name — hence /perry/live for the terminal.",
"allow": {
"nativeLibrary": [
"@honeide/editor/perry",
"@honeide/terminal/perry/live",
"@honeide/lsp-bridge/perry/live"
]
}
}
}
132 changes: 130 additions & 2 deletions src/types/perry-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@
// ---------------------------------------------------------------------------

declare module 'fs' {
/** Node-shaped stat result. Perry populates the standard predicate methods
* and *Ms timestamp fields (perry-runtime/src/fs/stats.rs). */
export interface Stats {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
size: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
mode: number;
}

export function readFileSync(path: string, encoding?: string): string;
export function writeFileSync(path: string, data: string, encoding?: string): void;
export function readdirSync(path: string): string[];
export function existsSync(path: string): boolean;
export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
export function unlinkSync(path: string): void;
/** Perry extension — check if path is a directory. */
export function statSync(path: string, options?: object): Stats;
export function lstatSync(path: string, options?: object): Stats;
/** Open a file and return its fd. Used to redirect background-process output. */
export function openSync(path: string, flags: string): number;
/** Perry extension — check if path is a directory. Prefer `../fs-compat`'s
* `isDirectory`, which shims this over statSync for the current toolchain. */
export function isDirectory(path: string): boolean;
}

Expand Down Expand Up @@ -54,10 +72,21 @@ declare module 'node:path' {
}

declare module 'child_process' {
/** Handle returned by `spawn`. Only the members hone actually uses are
* declared — see `../process-compat`'s spawnBackground. */
export interface ChildProcess {
pid?: number;
unref(): void;
}

export function execSync(command: string, options?: object): string;
/** Argv-array spawn — no shell, args passed directly to the executable. Safe for untrusted inputs. */
export function spawnSync(command: string, args: string[], options?: object): { stdout: string; stderr: string; status: number };
/** Perry extension — spawn a background process. */
/** Node-standard async spawn. Prefer `../process-compat`'s `spawnBackground`
* wrapper for fire-and-forget launches. */
export function spawn(command: string, args: string[], options?: object): ChildProcess;
/** Perry extension — spawn a background process. Prefer `../process-compat`'s
* `spawnBackground`, which reimplements this over standard `spawn`. */
export function spawnBackground(command: string, args: string[], options?: string | object): { pid: number; handleId: number };
}

Expand Down Expand Up @@ -122,6 +151,105 @@ declare module 'perry/thread' {
export function parallelFilter<T>(data: T[], predicate: (item: T) => boolean): T[];
}

// ---------------------------------------------------------------------------
// Buffer / node:crypto
//
// Perry maps node:crypto natively (perry-stdlib/src/crypto/*). Only the surface
// hone actually uses is declared. Signatures below were verified empirically
// against Perry-compiled probes, not copied from @types/node — notably,
// X25519 `export()` returns an opaque Perry surrogate string rather than real
// SPKI/PKCS8 DER (see src/workbench/sync-crypto.ts).
// ---------------------------------------------------------------------------

interface PerryBuffer {
toString(encoding?: string): string;
length: number;
}

declare var Buffer: {
from(data: string, encoding?: string): PerryBuffer;
from(data: ArrayBuffer | PerryBuffer): PerryBuffer;
alloc(size: number): PerryBuffer;
};

declare module 'crypto' {
interface Hmac {
update(data: string | PerryBuffer): Hmac;
digest(encoding: string): string;
}
interface Hash {
update(data: string | PerryBuffer): Hash;
digest(encoding: string): string;
}
interface Cipher {
update(data: string, inputEncoding: string, outputEncoding: string): string;
final(outputEncoding: string): string;
/** GCM only — must be read after final(). */
getAuthTag(): PerryBuffer;
}
interface Decipher {
update(data: string, inputEncoding: string, outputEncoding: string): string;
/** GCM only — THROWS if the auth tag does not verify. */
final(outputEncoding: string): string;
setAuthTag(tag: PerryBuffer): void;
}
/** Opaque key handle. Perry's export() yields a surrogate string, not DER. */
interface KeyObject {
export(options: { type: string; format: string }): PerryBuffer;
}

export function randomBytes(size: number): PerryBuffer;
export function createHash(algorithm: string): Hash;
export function createHmac(algorithm: string, key: string | PerryBuffer): Hmac;
export function hkdfSync(digest: string, ikm: PerryBuffer, salt: PerryBuffer, info: PerryBuffer, keylen: number): ArrayBuffer;
export function generateKeyPairSync(type: string): { publicKey: KeyObject; privateKey: KeyObject };
export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): PerryBuffer;
export function createCipheriv(algorithm: string, key: PerryBuffer, iv: PerryBuffer): Cipher;
export function createDecipheriv(algorithm: string, key: PerryBuffer, iv: PerryBuffer): Decipher;
export function createPublicKey(options: { key: PerryBuffer; format: string; type: string }): KeyObject;
export function createPrivateKey(options: { key: PerryBuffer; format: string; type: string }): KeyObject;

const _default: {
randomBytes: typeof randomBytes;
createHash: typeof createHash;
createHmac: typeof createHmac;
hkdfSync: typeof hkdfSync;
generateKeyPairSync: typeof generateKeyPairSync;
diffieHellman: typeof diffieHellman;
createCipheriv: typeof createCipheriv;
createDecipheriv: typeof createDecipheriv;
createPublicKey: typeof createPublicKey;
createPrivateKey: typeof createPrivateKey;
};
export default _default;
}

declare module 'node:crypto' {
export * from 'crypto';
}

// ---------------------------------------------------------------------------
// Perry i18n (compile-time localization)
// ---------------------------------------------------------------------------

declare module 'perry/i18n' {
/** Localize a string key. With no [i18n] config in perry.toml the key is
* returned as-is. `{param}` placeholders are filled from `params`; a `count`
* param selects the CLDR plural variant for the active locale. */
export function t(key: string, params?: { [key: string]: string | number }): string;

/** Locale-aware format wrappers. Each returns a display string for the
* active locale (default `en`). Date/time wrappers take epoch milliseconds. */
export function Currency(value: number): string;
export function Percent(value: number): string;
export function FormatNumber(value: number): string;
export function ShortDate(epochMs: number): string;
export function LongDate(epochMs: number): string;
export function FormatTime(epochMs: number): string;
/** Bypass locale formatting — emit the value verbatim. */
export function Raw(value: number): string;
}

// ---------------------------------------------------------------------------
// bun:test (for test files)
// ---------------------------------------------------------------------------
Expand Down
23 changes: 20 additions & 3 deletions src/types/perry-ui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ declare module 'perry/ui' {
export function textSetColor(text: unknown, r: number, g: number, b: number, a: number): void;
export function textSetFontSize(widget: unknown, size: number): void;
export function textSetFontWeight(widget: unknown, size: number, weight: number): void;
export function textSetFontFamily(widget: unknown, size: number, family: string): void;
/** Set the font family. Size is set separately via textSetFontSize — Perry's
* signature is (widget, family), unlike textSetFontWeight which takes a size.
* Passing an extra arg makes Perry's table dispatch silently skip the call. */
export function textSetFontFamily(widget: unknown, family: string): void;
export function textSetString(widget: unknown, value: string): void;
export function textSetWraps(widget: unknown, wraps: number): void;

Expand All @@ -61,6 +64,8 @@ declare module 'perry/ui' {
export function widgetSetHeight(widget: unknown, height: number): void;
export function widgetSetHugging(widget: unknown, priority: number): void;
export function widgetSetHidden(widget: unknown, hidden: number): void;
/** Native tooltip on hover (NSView.setToolTip on macOS — VoiceOver picks it up). */
export function widgetSetTooltip(widget: unknown, text: string): void;
export function widgetSetContextMenu(widget: unknown, menu: unknown): void;
export function widgetMatchParentHeight(widget: unknown): void;
export function widgetMatchParentWidth(widget: unknown): void;
Expand All @@ -72,7 +77,15 @@ declare module 'perry/ui' {
// ScrollView mutations
export function scrollViewSetChild(scrollView: unknown, child: unknown): void;
/** Scroll to coordinates. */
export function scrollViewScrollTo(scrollView: unknown, x: number, y: number): void;
/** Scroll so `child` is visible. Perry's 8 platform runtimes all implement
* perry_ui_scrollview_scroll_to(scroll_handle, child_handle) — this 2-arg
* form is the real signature.
*
* ⚠️ It does not currently work: Perry's dispatch table wrongly declares
* [Widget, F64, F64], and an arity mismatch lowers to a silent no-op. There
* is no (scrollView, x, y) variant to use instead — the runtime takes two
* handles, so a numeric x would be read as a widget. Blocked upstream; see
* scrollToBottom() in views/ai-chat/chat-panel.ts. */
/** Scroll to make a widget visible. */
export function scrollViewScrollTo(scrollView: unknown, widget: unknown): void;

Expand Down Expand Up @@ -100,7 +113,11 @@ declare module 'perry/ui' {
// Dialogs
export function openFolderDialog(callback: (path: string) => void): void;
export function openFileDialog(callback: (path: string) => void): void;
export function saveFileDialog(callback: (path: string) => void, defaultName?: string, directory?: string): void;
/** All three args are REQUIRED — Perry's table declares [Closure, Str, Str].
* They were marked optional here, but omitting one is an arity mismatch, and
* Perry lowers those to a silent no-op rather than an error: the dialog would
* simply never open. Pass '' rather than dropping an argument. */
export function saveFileDialog(callback: (path: string) => void, defaultName: string, directory: string): void;
export function pollOpenFile(): string;

// Menu
Expand Down
Loading
Loading