diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0962ea525..38a869e35 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -236,7 +236,8 @@ server's own KV store — see below. attached backend workspace; they are never transferred by the edge. Special key: `yas.target = ` — sets the default for non-browser -CLI commands. Browsers discover Relay routes from the home server and keep the +CLI commands. A name is resolved against the home server's catalogue (below) +on each connection. Browsers discover Relay routes from the home server and keep the active route set in the attached backend workspace. ### The Relay catalogue — the `remotes` KV key @@ -252,11 +253,15 @@ two catalogues rather than one shared home-directory file; and KV already has watching, compare-and-swap and a client-facing family, so editing a remote needed no transport of its own. -Managed with `yas remote add/remove/toggle/list`, which now reach the target -server rather than this machine's home directory — `yas --on dev remote add` -edits `dev`'s catalogue — and from the browser's Remotes panel. A server that +Managed with `yas remote add/remove/toggle/list`, which now reach a server +rather than this machine's home directory: the home server by default (not +`yas.target`), or the `--on` server, so `yas --on dev remote add` edits `dev`'s +catalogue. Also managed from the browser's Remotes panel. A server that finds no `remotes` key at startup imports a pre-KV `yas.remotes` file once, if -there is one. +there is one. The CLI resolves a bare target name (`--on NAME`, `YAS_TARGET`, +`yas.target`) by reading this key from the home server (`YAS_SOCK`, else the +default local instance) and dialing the stored URI itself; it no longer reads +the file. **The stored URIs carry credentials, and every client of the server can read them**: a `share:` passphrase, an `ssh:` host reference. That is the trade for @@ -318,7 +323,7 @@ All yas components share a common URI vocabulary for addressing yas server insta | `share:passphrase` | CLI, `yas.remotes` | Native read-only WebRTC share | | `share:passphrase?hub=URL` | `yas.remotes` | WebRTC via custom hub URL | | `proxy:uri` | CLI (`--on`) | Explicitly route through yas proxy-daemon | -| `name` | CLI (`--on`), yas.conf | Named remote from yas.remotes | +| `name` | CLI (`--on`), yas.conf | Named remote on the home server | Set `YAS_PROXY=0` to bypass proxy routing and connect directly for `ssh:`, `tcp:`, `ws:`, `wss:`, and `wt:` URIs. diff --git a/EMBEDDING.md b/EMBEDDING.md index 34f3add6d..e9cff9be5 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -292,7 +292,12 @@ You can also run a `@yas-run/core` client **server-side** (Node/Bun/Deno) to dri local `yas server` over its unix-domain socket — e.g. to script terminals or run headless commands. The non-browser building blocks live under the `@yas-run/core/node` subpath (kept out of the package root so `node:net` and -runtime globals never leak into browser bundles): +runtime globals never leak into browser bundles). The packages are plain ES +modules; install `@yas-run/core` with its matching `@yas-run/browser` peer: + +```bash +npm install @yas-run/core @yas-run/browser +``` ```ts import { YasWorkspace, exitCodeFromStatus, nullLogger } from "@yas-run/core"; @@ -315,6 +320,17 @@ const workspace = new YasWorkspace({ connections: [{ id: "default", transport }], }); +// Families are negotiated after the HELLO round trip; wait before creating. +await new Promise((resolve) => { + const check = () => { + if (!workspace.getSnapshot().ready) return; + unsubscribe(); + resolve(); + }; + const unsubscribe = workspace.subscribe(check); + check(); +}); + const session = await workspace.createSession({ connectionId: "default", rows: 24, @@ -323,10 +339,11 @@ const session = await workspace.createSession({ }); ``` -The unix transport speaks yas's framing protocol (4-byte little-endian -length-prefixed frames) for you — there is no need to re-implement the wire -format. `BunUnixSocketTransport` and `DenoUnixSocketTransport` are the -runtime-native equivalents. +The unix transport carries the YAS byte stream (the preface, then 4-byte +little-endian length-prefixed frames) for you; there is no need to +re-implement the wire format. `BunUnixSocketTransport` and +`DenoUnixSocketTransport` are the runtime-native equivalents; Deno needs +`--allow-read --allow-write` for the socket path. ### Exit status diff --git a/SKILL.md b/SKILL.md index 06bc6de24..db0ce35cc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -21,7 +21,7 @@ description: > yas is a terminal multiplexer and experimental headless Wayland compositor. Every terminal can run both CLI programs (via PTYs) and GUI applications (via the built-in compositor). Surfaces are video-encoded and streamed to browsers; the CLI gives programmatic control over both terminals and graphical windows. Everything the CLI does works locally or against a remote over one wire — -`--on ssh:host`, `--on share:passphrase`, or a named remote — including the +`--on ssh:host`, `--on share:passphrase`, or a named remote from `yas remote` — including the filesystem, git, and language-server commands. Beyond terminals it can: - **Files** — `yas fs cat|find|grep|write|sync|mkdir|mv|rm|ln`. `grep` diff --git a/bin/prepare-release b/bin/prepare-release index b50c67c29..944c3e41f 100755 --- a/bin/prepare-release +++ b/bin/prepare-release @@ -287,9 +287,13 @@ node - "$target_version" "${js_manifests[@]}" <<'NODE' const fs = require("fs"); const [version, ...manifests] = process.argv.slice(2); +// @yas-run/browser is built from crates/browser outside the pnpm workspace, +// so `workspace:*` cannot stamp its published range; it tracks the release. for (const manifest of manifests) { const pkg = JSON.parse(fs.readFileSync(manifest, "utf8")); pkg.version = version; + if (pkg.peerDependencies?.["@yas-run/browser"] !== undefined) + pkg.peerDependencies["@yas-run/browser"] = `^${version}`; fs.writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`); } NODE @@ -341,6 +345,11 @@ for (const manifest of manifests) { console.error(`release: failed to update ${manifest} to version ${expected}`); process.exit(1); } + const browser = pkg.peerDependencies?.["@yas-run/browser"]; + if (browser !== undefined && browser !== `^${expected}`) { + console.error(`release: failed to update ${manifest} @yas-run/browser peer to ^${expected}`); + process.exit(1); + } } NODE diff --git a/crates/alacritty-driver/src/lib.rs b/crates/alacritty-driver/src/lib.rs index d39affca9..f5bf831cc 100644 --- a/crates/alacritty-driver/src/lib.rs +++ b/crates/alacritty-driver/src/lib.rs @@ -924,6 +924,13 @@ impl TerminalDriver { out.evicted = true; } out.start_seq = seq; + // A cursor past the end of the output reads nothing and resumes from + // the end, so a result's start never lies after its next cursor. + if (seq, out.start_col) > (out.next_seq, out.next_col) { + out.start_seq = out.next_seq; + out.start_col = out.next_col; + return out; + } if seq > end_inclusive { return out; } diff --git a/crates/alacritty-driver/tests/seq_cursor.rs b/crates/alacritty-driver/tests/seq_cursor.rs index 9dc7b546f..6db850b79 100644 --- a/crates/alacritty-driver/tests/seq_cursor.rs +++ b/crates/alacritty-driver/tests/seq_cursor.rs @@ -295,3 +295,26 @@ fn growing_with_no_history_does_not_invent_rotation() { assert_eq!(named_lines(&d, 3), before); assert_eq!(d.cursor_seq(), cursor); } + +#[test] +fn a_cursor_past_the_end_reads_nothing_and_resumes_at_the_end() { + let mut d = TerminalDriver::new(10, 40, 1000); + feed_lines(&mut d, 0..3); + d.process(b"prompt> "); + let (seq, col) = d.cursor_seq(); + + for (from_seq, from_col) in [(seq, col + 5), (seq + 2, 0), (seq + 1000, 7)] { + let read = d.seq_text(from_seq, from_col, None, 64 * 1024); + assert_eq!(read.text, ""); + assert!(!read.truncated); + assert_eq!((read.start_seq, read.start_col), (seq, col)); + assert_eq!((read.next_seq, read.next_col), (seq, col)); + } + + let bounded = d.seq_text(seq + 50, 0, Some(seq), 64 * 1024); + assert_eq!(bounded.text, ""); + assert_eq!( + (bounded.start_seq, bounded.start_col), + (bounded.next_seq, bounded.next_col) + ); +} diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 1e7ecf4e2..3fbb0c3c5 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -66,7 +66,8 @@ pub struct Cli { #[derive(Args, Clone)] pub struct ConnectOpts { /// Remote to connect to: a URI (ssh:host, tcp:h:p, socket:/p, share:pass, local[:name]) - /// or a named remote from yas.remotes. Overrides YAS_TARGET and yas.conf `target`. + /// or the name of a remote on the home server (see `yas remote`). Overrides + /// YAS_TARGET and yas.conf `yas.target`. #[arg(long, global = true)] pub on: Option, @@ -397,11 +398,16 @@ pub enum Command { /// Execute a process and connect its standard streams Run(RunArgs), - /// Manage named remotes in yas.remotes + /// Manage the server's named remotes /// /// Named remotes let you refer to frequently-used destinations by a short - /// name instead of a full URI. They are stored in ~/.config/yas/yas.remotes - /// (mode 0o600) and can also be set as the default target via `yas.conf`. + /// name instead of a full URI. They are stored in a server's KV store (the + /// `remotes` key), which is also the catalogue its Relay publishes to the + /// browser. These verbs edit the home server (YAS_SOCK, else the default + /// local instance) unless --on names another; YAS_TARGET and `yas.target` + /// do not redirect them. `--on NAME`, YAS_TARGET, and `yas.target` look + /// names up on the home server. A legacy ~/.config/yas/yas.remotes file is + /// imported once into a server that has no catalogue yet. /// /// Examples: /// yas remote add rabbit ssh:rabbit @@ -421,9 +427,9 @@ pub enum Command { #[command( about = "Open the terminal UI in the browser", long_about = "Open the terminal UI in the browser\n\n\ - Opens the browser with all named remotes from ~/.config/yas/yas.remotes\n\ - plus the local yas server. Manage remotes with `yas remote add/remove`\n\ - or through the Remotes dialog in the browser.\n\n\ + Opens the browser on the local yas server, with that server's named\n\ + remotes reachable through its Relay. Manage remotes with\n\ + `yas remote add/remove` or through the Remotes dialog in the browser.\n\n\ Examples:\n\ yas open # local + all configured remotes\n\ yas remote add rabbit ssh:rabbit\n\ @@ -594,7 +600,8 @@ pub enum Command { fd_channel: Option, /// Export the server socket path as YAS_SOCK in spawned terminals - /// (or set YAS_EXPORT_SOCK=1) + /// (or set YAS_EXPORT_SOCK=1). Without it, terminals still never + /// inherit a YAS_SOCK that names a different server #[arg(long)] export_sock: bool, @@ -1019,9 +1026,11 @@ pub enum TerminalCommand { /// Print one command's output (needs OSC 133 shell integration). /// - /// Defaults to the newest command. With --wait, blocks server-side until - /// the command finishes and exits with its status (124 if the wait timed - /// out), which is how to run something in a live shell and collect the + /// Defaults to the newest command. With --wait and no INDEX, it picks the + /// command that is running, or if none is, the next one to start (so a + /// command sent just before still counts), blocks server-side until that + /// command finishes, and exits with its status (124 if the wait timed + /// out). This is how to run something in a live shell and collect the /// result: /// yas terminal send 3 'cargo test\n' /// yas terminal output 3 --wait 600 @@ -1277,6 +1286,7 @@ pub enum TerminalCommand { /// Send input to a terminal. /// + /// Fails if the terminal has exited. /// Supports C-style escapes: \n \r \t \\ \0 \xHH. /// \n sends CR (Enter), matching real terminal behavior. Use \x0a for literal LF. /// To control interactive programs like vim: @@ -2660,8 +2670,8 @@ pub enum RemoteCommand { }, /// Disable or enable a named remote without removing it. - /// Disabled remotes are kept in yas.remotes (commented out) and excluded - /// from connection resolution until re-enabled. + /// Disabled remotes stay in the catalogue but are not published by the + /// Relay, and `--on NAME` refuses them until re-enabled. Toggle { /// Name of the remote to toggle name: String, @@ -2669,8 +2679,9 @@ pub enum RemoteCommand { /// Set the default remote in yas.conf /// - /// After this, all agent subcommands (list, start, show, …) will connect - /// to this remote by default, without needing --on. + /// Writes `yas.target` in this machine's yas.conf. After this, commands + /// without --on connect to this remote. A name is looked up on the home + /// server at each connection. SetDefault { /// Name or URI to use as the default target. /// Pass an empty string or "local" to reset to local. diff --git a/crates/cli/src/learn.md b/crates/cli/src/learn.md index 4d6b94802..dcb6254b7 100644 --- a/crates/cli/src/learn.md +++ b/crates/cli/src/learn.md @@ -59,6 +59,8 @@ yas terminal send "$ID" "\x03" # Ctrl+C ``` Supports C-style escapes: `\n`, `\t`, `\r`, `\\`, `\0`, `\xHH`. Use `-` to read from stdin. +Sending to an exited terminal fails with a nonzero exit instead of dropping the +input. `\n` sends CR (0x0D), which is what a real terminal sends for Enter. This works regardless of whether the program is in canonical or raw mode. `\r` also sends @@ -142,6 +144,8 @@ yas terminal journal "$ID" --json ``` `--wait` blocks server-side until the command finishes (exit 124 on timeout). +Without an index it waits for the running command, or the next one to start if +none is running, so a finished command is never mistaken for the one just sent. `wait --pattern` matches only output produced after the wait began. ## Terminal lifecycle @@ -226,6 +230,14 @@ yas remote add prod ssh:alice@prod.co yas remote set-default prod ``` +A bare name (from `--on`, `YAS_TARGET`, or `yas.target` in `yas.conf`) is +looked up in the home server's remotes catalogue, the one `yas remote add` +edits: the server at `YAS_SOCK`, else the default local instance, started if +needed. A name may point at another name. Disabled remotes +(`yas remote toggle`) and unknown names fail with an error. `yas remote` +verbs edit the home server too unless `--on` names another server; the +default target does not redirect them. + ## Files All paths are relative to `--root` (default: the client's cwd, resolved @@ -366,6 +378,14 @@ so `run`/`update` options go before the name. `--restart`, `--persist`, `--detach` and `--json` written after the module are refused rather than handed over; put a `--` first if the extension really wants one of them. +An attached `run` prints the attempt's stdout, stderr, and log output, even +when it finishes before the CLI starts following, and exits with the +extension's return code. A trapped or failed attempt prints +`yas: extension NAME trapped: DETAIL` to stderr and exits 1. A detached +transient extension keeps its name for 30 s after it stops +(`YAS_EXT_TERMINAL_RETAIN`) so its output can be replayed; `run` refuses to +reuse that name until then and says so. + `yas ext manage` opens an inline extension picker. Move with arrows or `j`/`k` and use Space or a mouse click to cycle the selected action: install for new extensions, update then uninstall for outdated ones, or uninstall for other diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a085140af..53564c3cf 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1226,7 +1226,13 @@ fn mask_remote_credentials(uri: &str) -> String { /// catalogue is (`crates/cli/src/yas_remotes.rs`). `set-default` stays local: /// which server *this* CLI talks to by default is this machine's business, not /// something a server should hold an opinion about. +/// +/// Without `--on` the verbs edit the home server, not `YAS_TARGET` or +/// `yas.target`: names resolve against the home catalogue, so after +/// `set-default work` a `toggle work` must still reach the catalogue that +/// defines `work`. async fn cmd_remote(cmd: RemoteCommand, on: Option<&str>, hub: &str) -> Result<(), String> { + let on = Some(on.unwrap_or("local")); match cmd { RemoteCommand::List { reveal } => { let entries = yas_remotes::read(on, hub).await?; diff --git a/crates/cli/src/transport.rs b/crates/cli/src/transport.rs index 1d89b1ed1..2f2bd6bfc 100644 --- a/crates/cli/src/transport.rs +++ b/crates/cli/src/transport.rs @@ -677,20 +677,64 @@ pub async fn connect_via_composite_proxy(upstream_uri: &str) -> Result Result { - Box::pin(connect_native_uri_inner( - uri, - hub, - std::collections::HashSet::new(), - )) - .await + if is_target_uri(uri) { + return Box::pin(connect_target_uri(uri, hub)).await; + } + let entries = crate::yas_remotes::read_home() + .await + .map_err(|error| format!("cannot look up remote '{uri}' on the home server: {error}"))?; + let resolved = resolve_remote_name(uri, &entries)?; + Box::pin(connect_target_uri(&resolved, hub)).await } -async fn connect_native_uri_inner( - uri: &str, - hub: &str, - mut visited: std::collections::HashSet, -) -> Result { +const TARGET_SCHEMES: &[&str] = &[ + "proxy:", "ssh:", "tcp:", "wt://", "ws://", "wss://", "uplink:", "socket:", "share:", "local:", +]; + +fn is_target_uri(uri: &str) -> bool { + uri == "local" || TARGET_SCHEMES.iter().any(|scheme| uri.starts_with(scheme)) +} + +/// Follow `name` through the catalogue to a URI. An entry may name another +/// entry; disabled entries do not resolve. +fn resolve_remote_name( + name: &str, + entries: &[yas_webserver::config::RemoteEntry], +) -> Result { + let mut visited = std::collections::HashSet::new(); + let mut current = name; + loop { + if !visited.insert(current) { + return Err(format!("remotes: cycle detected resolving '{name}'")); + } + let Some(entry) = entries.iter().rev().find(|entry| entry.name == current) else { + return Err(if current == name { + format!( + "unknown target '{name}' \ + (expected ssh:, tcp:, ws://, wss://, wt://, socket:, share:, uplink:, proxy:, \ + local[:NAME], or a remote name from `yas remote list` on the home server)" + ) + } else { + format!("remote '{name}' refers to '{current}', which is not a configured remote") + }); + }; + if entry.disabled { + return Err(format!( + "remote '{current}' is disabled; enable it with `yas remote toggle {current}`" + )); + } + if is_target_uri(&entry.uri) { + return Ok(entry.uri.clone()); + } + current = &entry.uri; + } +} + +async fn connect_target_uri(uri: &str, hub: &str) -> Result { if let Some(upstream) = uri.strip_prefix("proxy:") { return connect_via_native_proxy(upstream).await; } @@ -765,9 +809,7 @@ async fn connect_native_uri_inner( }); } if uri == "local" { - let path = yas_webserver::config::default_yas_socket(); - ensure_local_server(&path).await?; - return connect_native_home(&path).await; + return connect_home().await; } if let Some(raw_name) = uri.strip_prefix("local:") { let name: yas_server::ServerName = raw_name @@ -777,19 +819,7 @@ async fn connect_native_uri_inner( ensure_local_server_with_name(&path, Some(&name)).await?; return connect_native_home(&path).await; } - - let entries = yas_webserver::config::read_remotes(); - if let Some((_, target_uri)) = entries.into_iter().find(|(name, _)| name == uri) { - if !visited.insert(uri.to_string()) { - return Err(format!("yas.remotes: cycle detected resolving '{uri}'")); - } - return Box::pin(connect_native_uri_inner(&target_uri, hub, visited)).await; - } - Err(format!( - "unknown target '{uri}' \ - (expected ssh:, tcp:, ws://, wss://, wt://, socket:, share:, proxy:, local[:NAME], \ - or a name from yas.remotes)" - )) + Err(format!("unknown target '{uri}'")) } async fn connect_native_upstream(uri: &str) -> Result { @@ -879,6 +909,11 @@ pub async fn connect_native(on: &Option, hub: &str) -> Result Result { let path = yas_webserver::config::default_yas_socket(); ensure_local_server(&path).await?; connect_native_home(&path).await @@ -1303,4 +1338,79 @@ mod tests { let result = read_frame(&mut server).await.unwrap(); assert_eq!(result, payload); } + + fn remote(name: &str, uri: &str, disabled: bool) -> yas_webserver::config::RemoteEntry { + yas_webserver::config::RemoteEntry { + name: name.into(), + uri: uri.into(), + disabled, + } + } + + #[test] + fn target_uris_are_not_names() { + for uri in [ + "local", + "local:work", + "socket:/tmp/yas.sock", + "ssh:alice@host", + "tcp:127.0.0.1:1", + "ws://h", + "wss://h", + "wt://h", + "share:pass", + "uplink:https://relay#token", + "proxy:ssh:host", + ] { + assert!(is_target_uri(uri), "{uri}"); + } + for name in ["work", "localhost", "rabbit"] { + assert!(!is_target_uri(name), "{name}"); + } + } + + #[test] + fn remote_name_resolves_through_catalogue() { + let entries = [ + remote("work", "socket:/tmp/work.sock", false), + remote("alias", "work", false), + remote("secret", "share:passphrase", false), + ]; + assert_eq!( + resolve_remote_name("work", &entries).unwrap(), + "socket:/tmp/work.sock" + ); + assert_eq!( + resolve_remote_name("alias", &entries).unwrap(), + "socket:/tmp/work.sock" + ); + assert_eq!( + resolve_remote_name("secret", &entries).unwrap(), + "share:passphrase" + ); + } + + #[test] + fn remote_name_errors_are_specific() { + let entries = [ + remote("off", "ssh:host", true), + remote("via-off", "off", false), + remote("a", "b", false), + remote("b", "a", false), + remote("dangling", "missing", false), + remote("secret", "share:passphrase", true), + ]; + let unknown = resolve_remote_name("nope", &entries).unwrap_err(); + assert!(unknown.starts_with("unknown target 'nope'"), "{unknown}"); + let disabled = resolve_remote_name("off", &entries).unwrap_err(); + assert!(disabled.contains("'off' is disabled"), "{disabled}"); + let through = resolve_remote_name("via-off", &entries).unwrap_err(); + assert!(through.contains("'off' is disabled"), "{through}"); + let cycle = resolve_remote_name("a", &entries).unwrap_err(); + assert!(cycle.contains("cycle"), "{cycle}"); + let dangling = resolve_remote_name("dangling", &entries).unwrap_err(); + assert!(dangling.contains("'missing'"), "{dangling}"); + let secret = resolve_remote_name("secret", &entries).unwrap_err(); + assert!(!secret.contains("passphrase"), "{secret}"); + } } diff --git a/crates/cli/src/yas_extension.rs b/crates/cli/src/yas_extension.rs index 4c76ba8ee..11f377793 100644 --- a/crates/cli/src/yas_extension.rs +++ b/crates/cli/src/yas_extension.rs @@ -311,24 +311,44 @@ async fn run(client: &mut NativeClient, args: RunArgs) -> Result { if args.persist { flags |= schema::DEFINITION_PERSISTENT as u16; } - let identity = deploy( - client, - Deploy { - operation_id: operation_id(), - expected_extension_handle: 0, - expected_generation: 0, - expected_definition_revision: 0, - flags, - runtime: Runtime::Auto, - restart_policy: restart_policy(args.restart), - name: args.name, - content_hash: object.hash, - argv: argv.into_iter().map(String::into_bytes).collect(), - runtime_limits: default_runtime_limits(), - extensions: Extensions::default(), - }, - ) - .await?; + let name = args.name.clone(); + let request = Deploy { + operation_id: operation_id(), + expected_extension_handle: 0, + expected_generation: 0, + expected_definition_revision: 0, + flags, + runtime: Runtime::Auto, + restart_policy: restart_policy(args.restart), + name: args.name, + content_hash: object.hash, + argv: argv.into_iter().map(String::into_bytes).collect(), + runtime_limits: default_runtime_limits(), + extensions: Extensions::default(), + }; + let prefix = client + .request_result( + family::EXTENSION, + wire::request_kind::DEPLOY, + request.encode().map_err(wire_error)?, + true, + ) + .await?; + if prefix.status == yas_wire::core::Status::Conflict + && let Some(message) = name_conflict_message(&snapshot(client).await?, &name) + { + return Err(message); + } + if prefix.status != yas_wire::core::Status::Ok { + return Err(format!( + "YAS request {:#06x}/{:#06x} failed with {:?}: {}", + family::EXTENSION, + wire::request_kind::DEPLOY, + prefix.status, + crate::yas_native::format_result_detail(&prefix.detail) + )); + } + let identity = DefinitionIdentity::decode(&prefix.body).map_err(wire_error)?; let record = wait_for_identity(client, &identity).await?; if args.json { render_record(&record, true, "status"); @@ -485,6 +505,9 @@ async fn wait_until_started( } if matches!(current.phase, Phase::Stopped | Phase::Blocked) { render_record(¤t, json, "status"); + if !json && let Some(message) = exit_message(¤t) { + eprintln!("yas: {message}"); + } return Ok(exit_code(¤t).max(1)); } tokio::time::sleep(Duration::from_millis(200)).await; @@ -525,6 +548,15 @@ async fn follow_until_terminal( return Ok(130); } } + } else if matches!(current.phase, Phase::Stopped | Phase::Blocked) { + if json { + render_record(¤t, true, "status"); + } else if let Some(message) = exit_message(¤t) { + eprintln!("yas: {message}"); + } + return Ok(exit_code(¤t)); + } else { + tokio::time::sleep(Duration::from_millis(200)).await; } current = find_identity( &snapshot(client).await?, @@ -532,13 +564,6 @@ async fn follow_until_terminal( current.generation, )? .clone(); - if matches!(current.phase, Phase::Stopped | Phase::Blocked) { - if json { - render_record(¤t, true, "status"); - } - return Ok(exit_code(¤t)); - } - tokio::time::sleep(Duration::from_millis(200)).await; } } @@ -924,6 +949,58 @@ fn exit_code(record: &ExtensionRecord) -> i32 { } } +fn name_conflict_message(records: &[ExtensionRecord], name: &str) -> Option { + let existing = records.iter().find(|record| record.name == name)?; + let id = format_id(existing.extension_handle); + let phase = phase_name(existing.phase); + Some( + if existing.flags & schema::DEFINITION_PERSISTENT as u16 != 0 { + format!( + "a persistent extension named {name} already exists (id:{id}, phase {phase}); \ + use `yas ext update {name}` to replace it" + ) + } else if matches!(existing.phase, Phase::Stopped | Phase::Blocked) { + format!( + "a stopped transient extension named {name} (id:{id}) is still retained for output \ + replay; retry after it expires (YAS_EXT_TERMINAL_RETAIN, 30 s by default) or use \ + another name" + ) + } else { + format!( + "an extension named {name} is already {phase} (id:{id}); stop it with \ + `yas ext stop id:{id}` or use another name" + ) + }, + ) +} + +fn exit_message(record: &ExtensionRecord) -> Option { + let name = &record.name; + match record.last_exit.as_ref() { + Some(exit) if exit.kind == wire::ExitKind::Returned => None, + Some(exit) => { + let kind = match exit.kind { + wire::ExitKind::Returned => "returned", + wire::ExitKind::Trapped => "trapped", + wire::ExitKind::Cancelled => "was cancelled", + wire::ExitKind::Updated => "was replaced by an update", + wire::ExitKind::SlowConsumer => "was stopped as a slow consumer", + wire::ExitKind::ProtocolViolation => "violated the protocol", + wire::ExitKind::HostFailure => "failed in the host", + wire::ExitKind::ServerShutdown => "stopped for server shutdown", + wire::ExitKind::ResourceLimit => "exceeded a resource limit", + }; + if exit.detail.is_empty() { + Some(format!("extension {name} {kind}")) + } else { + Some(format!("extension {name} {kind}: {}", exit.detail)) + } + } + None if record.phase == Phase::Blocked => Some(format!("extension {name} is blocked")), + None => None, + } +} + fn phase_name(phase: Phase) -> &'static str { match phase { Phase::NeedObject => "need-object", @@ -1244,6 +1321,67 @@ fn loopback_url(url: &reqwest::Url) -> bool { mod tests { use super::*; + fn stopped(name: &str, flags: u16, last_exit: Option) -> ExtensionRecord { + ExtensionRecord { + extension_handle: 0x2a, + generation: 1, + definition_revision: 1, + phase: Phase::Stopped, + runtime: Runtime::QuickJs, + restart_policy: wire::RestartPolicy::Never, + flags, + attempt: 1, + last_running_attempt: 1, + task_id: 0, + next_start_unix_ms: 0, + directory_revision: 0, + content_hash: [0; 32], + name: name.into(), + last_exit, + runtime_limits: default_runtime_limits(), + extensions: Extensions::default(), + } + } + + fn exit(kind: wire::ExitKind, detail: &str) -> Option { + Some(wire::ExitRecord { + kind, + code: 0, + attempt: 1, + server_ns: 0, + detail: detail.into(), + extensions: Extensions::default(), + }) + } + + #[test] + fn a_trapped_attempt_names_its_error() { + let record = stopped( + "hello", + 0, + exit(wire::ExitKind::Trapped, "Error: boom at main.js:1"), + ); + assert_eq!( + exit_message(&record).as_deref(), + Some("extension hello trapped: Error: boom at main.js:1") + ); + assert_eq!(exit_code(&record), 1); + let returned = stopped("hello", 0, exit(wire::ExitKind::Returned, "")); + assert_eq!(exit_message(&returned), None); + } + + #[test] + fn a_name_conflict_explains_which_definition_holds_the_name() { + let transient = [stopped("hello", 0, exit(wire::ExitKind::Returned, ""))]; + let message = name_conflict_message(&transient, "hello").unwrap(); + assert!(message.contains("stopped transient"), "{message}"); + assert!(message.contains("000000000000002a"), "{message}"); + let persistent = [stopped("hello", schema::DEFINITION_PERSISTENT as u16, None)]; + let message = name_conflict_message(&persistent, "hello").unwrap(); + assert!(message.contains("yas ext update hello"), "{message}"); + assert_eq!(name_conflict_message(&transient, "other"), None); + } + #[test] fn selectors_never_infer_numeric_names() { assert!(matches!( diff --git a/crates/cli/src/yas_extension_manage.rs b/crates/cli/src/yas_extension_manage.rs index eda4f4094..e7b49e2f9 100644 --- a/crates/cli/src/yas_extension_manage.rs +++ b/crates/cli/src/yas_extension_manage.rs @@ -30,8 +30,9 @@ const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; #[derive(Args, Clone, Debug)] pub(crate) struct ManageArgs { - /// Registry directory containing manifest.json and extension modules - #[arg(long, default_value = DEFAULT_REGISTRY)] + /// Registry base URL serving manifest.json and extension modules + /// (https://, or http:// on a loopback host) + #[arg(long, value_name = "URL", default_value = DEFAULT_REGISTRY)] pub from: String, } diff --git a/crates/cli/src/yas_native.rs b/crates/cli/src/yas_native.rs index 530b9f197..75aef4545 100644 --- a/crates/cli/src/yas_native.rs +++ b/crates/cli/src/yas_native.rs @@ -1388,7 +1388,7 @@ fn wire_error(error: yas_wire::Error) -> String { format!("YAS wire error: {error}") } -fn format_result_detail(detail: &Extensions) -> String { +pub(crate) fn format_result_detail(detail: &Extensions) -> String { if detail.0.is_empty() { "no detail".to_string() } else { diff --git a/crates/cli/src/yas_remotes.rs b/crates/cli/src/yas_remotes.rs index 4cccbdc0e..18d5673fb 100644 --- a/crates/cli/src/yas_remotes.rs +++ b/crates/cli/src/yas_remotes.rs @@ -40,7 +40,10 @@ struct Catalogue { impl Catalogue { async fn open(on: Option<&str>, hub: &str) -> Result { - let mut client = NativeClient::connect(on, hub).await?; + Self::open_client(NativeClient::connect(on, hub).await?).await + } + + async fn open_client(mut client: NativeClient) -> Result { if !client.supports(family::KV, Class::Request, kv::request_kind::OPEN) { return Err( "this server does not offer the KV family, which is where remotes live".to_owned(), @@ -162,3 +165,16 @@ pub(crate) async fn read(on: Option<&str>, hub: &str) -> Result let mut catalogue = Catalogue::open(on, hub).await?; Ok(catalogue.read().await?.0) } + +/// The home server's catalogue, for resolving a bare target name. +/// +/// The home server is the local default instance (`YAS_SOCK`, else the +/// default socket), never the configured target: resolving `yas.target` +/// through itself would recurse, and the home server is where +/// `yas remote add` without `--on` writes. +pub(crate) async fn read_home() -> Result, String> { + let transport = crate::transport::connect_home().await?; + let client = NativeClient::connect_transport(transport, "yas-cli").await?; + let mut catalogue = Catalogue::open_client(client).await?; + Ok(catalogue.read().await?.0) +} diff --git a/crates/cli/src/yas_terminal.rs b/crates/cli/src/yas_terminal.rs index 4728e2f5b..3fd82f06e 100644 --- a/crates/cli/src/yas_terminal.rs +++ b/crates/cli/src/yas_terminal.rs @@ -6,12 +6,17 @@ pub(crate) mod stream; use std::time::Duration; use yas_wire::{ - Class, Decode, Encode, Extension, Extensions, family, + Class, Decode, Encode, Extension, Extensions, + core::Status, + family, state::{Phase, RecordKind, StateAck, StateEvent, Watch, WatchResult}, terminal, }; -use crate::{terminal_args, yas_native::NativeClient}; +use crate::{ + terminal_args, + yas_native::{NativeClient, format_result_detail}, +}; const NS_PER_SECOND: u64 = 1_000_000_000; const QUERY_CREDIT: u64 = 1024 * 1024; @@ -19,6 +24,35 @@ const MAX_QUERY_BYTES: u64 = 64 * 1024 * 1024; const READ_PAGE_BYTES: u32 = 8 * 1024 * 1024; const STATE_CREDIT: u64 = 1024 * 1024; +/// Like `println!`, but a closed stdout (`yas terminal list | head -1`) is +/// not an error: the command still finishes and exits with its own status. +macro_rules! outln { + () => { + write_stdout(b"\n") + }; + ($($arg:tt)*) => { + write_stdout(format!("{}\n", format_args!($($arg)*)).as_bytes()) + }; +} + +fn write_stdout(bytes: &[u8]) { + use std::io::Write; + let mut stdout = std::io::stdout().lock(); + if let Err(error) = stdout.write_all(bytes).and_then(|()| stdout.flush()) + && error.kind() != std::io::ErrorKind::BrokenPipe + { + eprintln!("yas: cannot write to stdout: {error}"); + } +} + +/// OutputResult framing around its text: the fixed fields plus the text length. +const OUTPUT_RESULT_OVERHEAD: u64 = 36; + +/// Collection limit for an OUTPUT reply whose text is capped at `max_bytes`. +fn output_collection_limit(max_bytes: u32) -> u64 { + (u64::from(max_bytes.max(1)) + OUTPUT_RESULT_OVERHEAD).min(MAX_QUERY_BYTES) +} + pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> { let mut client = NativeClient::connect(on, hub).await?; let mut terminals = client @@ -30,7 +64,7 @@ pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> .collect::>(); terminals.sort_by_key(|record| record.terminal_handle); - println!("ID\tTAG\tTITLE\tCOMMAND\tCWD\tSTATUS"); + outln!("ID\tTAG\tTITLE\tCOMMAND\tCWD\tSTATUS"); for record in terminals { let id = terminal_id(record.terminal_handle)?; let tag = text_extension( @@ -55,7 +89,7 @@ pub(crate) async fn cmd_list(on: Option<&str>, hub: &str) -> Result<(), String> .map(|value| String::from_utf8_lossy(value).into_owned()) .unwrap_or_default(); let status = terminal_status(&record)?; - println!("{id}\t{tag}\t{title}\t{command}\t{cwd}\t{status}"); + outln!("{id}\t{tag}\t{title}\t{command}\t{cwd}\t{status}"); } Ok(()) } @@ -82,16 +116,29 @@ pub(crate) async fn cmd_start( extensions: Extensions(create_extensions), }; let mut client = NativeClient::connect(on, hub).await?; - let result: terminal::CreateResult = client - .request_typed( + let prefix = client + .request_result( family::TERMINAL, terminal::request_kind::CREATE, - &create, + create.encode().map_err(wire_error)?, true, ) .await?; + match prefix.status { + Status::Ok => {} + Status::NotFound if !request.shell && !request.command.is_empty() => { + return Err(format!("{}: command not found", request.command[0])); + } + status => { + return Err(format!( + "YAS Terminal CREATE failed with {status:?}: {}", + format_result_detail(&prefix.detail) + )); + } + } + let result = terminal::CreateResult::decode(&prefix.body).map_err(wire_error)?; let id = terminal_id(result.terminal_handle)?; - println!("{id}"); + outln!("{id}"); Ok(id) } @@ -106,12 +153,19 @@ pub(crate) async fn cmd_send( return Ok(()); } let mut client = NativeClient::connect(on, hub).await?; + let record = find_terminal(&mut client, id).await?; + if record.lifecycle == terminal::Lifecycle::Exited { + return Err(format!( + "cannot send to pty {id}: it has {} (`yas terminal restart {id}` re-runs it)", + terminal_status(&record)? + )); + } client .send_typed_event( family::TERMINAL, terminal::event_kind::WRITE, &terminal::Write { - terminal_handle: terminal_handle(id)?, + terminal_handle: record.terminal_handle, data: bytes, }, true, @@ -377,10 +431,9 @@ pub(crate) async fn cmd_cwd(on: Option<&str>, hub: &str, id: u64) -> Result<(), .await?; expect_content(&query, yas_wire::schema::terminal::CONTENT_PATH as u8)?; let bytes = query_bytes(query).await?; - std::io::Write::write_all(&mut std::io::stdout(), &bytes) - .map_err(|error| format!("cannot write terminal cwd: {error}"))?; + write_stdout(&bytes); if !bytes.ends_with(b"\n") { - println!(); + outln!(); } Ok(()) } @@ -396,7 +449,7 @@ pub(crate) async fn cmd_journal( let mut client = NativeClient::connect(on, hub).await?; let record = find_terminal(&mut client, id).await?; if !json { - println!("INDEX\tSTATUS\tEXIT\tMS\tSTART_SEQ\tEND_SEQ\tCOMMAND"); + outln!("INDEX\tSTATUS\tEXIT\tMS\tSTART_SEQ\tEND_SEQ\tCOMMAND"); } let mut cursor = from.unwrap_or(0); let mut tail = from.is_none(); @@ -429,7 +482,7 @@ pub(crate) async fn cmd_journal( terminal::JournalResult::decode(&query_bytes(query).await?).map_err(wire_error)?; for entry in &journal.records { if json { - println!("{}", journal_record_json(entry)); + outln!("{}", journal_record_json(entry)); } else { print_journal_record(entry); } @@ -478,26 +531,43 @@ pub(crate) async fn cmd_output( } else { yas_wire::schema::terminal::WAIT_LATEST_COMMAND as u8 }; - let query = terminal_query( - &mut client, - terminal::request_kind::WAIT, - &terminal::Wait { - terminal_handle: terminal_record.terminal_handle, - generation: terminal_record.generation, - wait_kind, - flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, - cursor_a: index.unwrap_or(0), - cursor_b: 0, - max_bytes, - timeout_ns: timeout.saturating_mul(NS_PER_SECOND).max(1), - needle: Vec::new(), - initial_receive_credit: QUERY_CREDIT, - extensions: Extensions::default(), - }, - MAX_QUERY_BYTES, - Duration::from_secs(timeout.saturating_add(2)), - ) - .await?; + let request = terminal::Wait { + terminal_handle: terminal_record.terminal_handle, + generation: terminal_record.generation, + wait_kind, + flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, + cursor_a: index.unwrap_or(0), + cursor_b: 0, + max_bytes, + timeout_ns: timeout.saturating_mul(NS_PER_SECOND).max(1), + needle: Vec::new(), + initial_receive_credit: QUERY_CREDIT, + extensions: Extensions::default(), + }; + let prefix = client + .request_result_with_timeout( + family::TERMINAL, + terminal::request_kind::WAIT, + request.encode().map_err(wire_error)?, + true, + Duration::from_secs(timeout.saturating_add(2)), + ) + .await?; + match prefix.status { + Status::Ok => {} + Status::Timeout => { + eprintln!("yas: timed out waiting for a command in pty {id}"); + return Ok(124); + } + Status::NotFound => return Err(format!("pty {id}: no such command")), + status => { + return Err(format!( + "YAS Terminal WAIT failed with {status:?}: {}", + format_result_detail(&prefix.detail) + )); + } + } + let query = collect_query(&mut client, &prefix.body, MAX_QUERY_BYTES).await?; expect_content(&query, yas_wire::schema::terminal::CONTENT_JOURNAL as u8)?; let result = terminal::JournalResult::decode(&query_bytes(query).await?).map_err(wire_error)?; @@ -530,7 +600,7 @@ pub(crate) async fn cmd_output( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(max_bytes).min(MAX_QUERY_BYTES), + output_collection_limit(max_bytes), Duration::from_secs(10), ) .await?; @@ -554,12 +624,11 @@ pub(crate) async fn cmd_output( "next_cursor".into(), format_cursor(output.next_seq, output.next_col).into(), ); - println!("{value}"); + outln!("{value}"); } else { - std::io::Write::write_all(&mut std::io::stdout(), &output.text) - .map_err(|error| format!("cannot write terminal output: {error}"))?; + write_stdout(&output.text); if !output.text.is_empty() && !output.text.ends_with(b"\n") { - println!(); + outln!(); } if evicted { eprintln!("yas: output start had scrolled out of the backlog"); @@ -571,6 +640,15 @@ pub(crate) async fn cmd_output( ); } } + if let Some(record) = waited.as_ref().filter(|record| { + journal_running(record) + && record.flags & yas_wire::schema::terminal::JOURNAL_PTY_EXITED as u16 == 0 + }) { + eprintln!( + "yas: timed out waiting for command {} in pty {id}", + record.index + ); + } Ok(waited.as_ref().map_or(0, journal_exit_code)) } @@ -752,7 +830,15 @@ async fn terminal_query( timeout, ) .await?; - let query = terminal::QueryBody::decode(&body).map_err(wire_error)?; + collect_query(client, &body, maximum).await +} + +async fn collect_query( + client: &mut NativeClient, + body: &[u8], + maximum: u64, +) -> Result { + let query = terminal::QueryBody::decode(body).map_err(wire_error)?; query .validate_receive_credit(QUERY_CREDIT) .map_err(wire_error)?; @@ -911,7 +997,7 @@ fn print_query_rows_with_separator( .map_err(|_| "YAS Terminal returned non-UTF-8 text".to_string())? }; if !*first && !text.is_empty() { - println!(); + outln!(); } print!("{text}"); *first = false; @@ -1130,7 +1216,7 @@ fn journal_record_json(record: &terminal::JournalRecord) -> serde_json::Value { fn print_journal_record(record: &terminal::JournalRecord) { let exit = journal_exit(record).map(|value| value.to_string()); let duration = journal_duration(record).map(|value| value.to_string()); - println!( + outln!( "{}\t{}\t{}\t{}\t{}\t{}\t{}", record.index, journal_status(record), @@ -1213,7 +1299,7 @@ async fn cmd_since( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(request_max.max(1)).min(MAX_QUERY_BYTES), + output_collection_limit(request_max), Duration::from_secs(10), ) .await?; @@ -1221,7 +1307,7 @@ async fn cmd_since( let output = terminal::OutputResult::decode(&query_bytes(query).await?).map_err(wire_error)?; let next = format_cursor(output.next_seq, output.next_col); if json { - println!( + outln!( "{}", serde_json::json!({ "pty": id, @@ -1234,10 +1320,9 @@ async fn cmd_since( }) ); } else { - std::io::Write::write_all(&mut std::io::stdout(), &output.text) - .map_err(|error| format!("cannot write terminal history: {error}"))?; + write_stdout(&output.text); if !output.text.is_empty() && !output.text.ends_with(b"\n") { - println!(); + outln!(); } eprintln!("cursor: {next}"); } @@ -1262,7 +1347,7 @@ async fn probe_output_cursor( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - 1, + output_collection_limit(1), Duration::from_secs(10), ) .await?; @@ -1299,7 +1384,7 @@ async fn wait_for_pattern( initial_receive_credit: QUERY_CREDIT, extensions: Extensions::default(), }, - u64::from(terminal_args::OUTPUT_MAX_BYTES), + output_collection_limit(terminal_args::OUTPUT_MAX_BYTES), Duration::from_secs(10), ) .await?; @@ -1308,7 +1393,7 @@ async fn wait_for_pattern( (sequence, column) = (output.next_seq, output.next_col); pending.push_str(&String::from_utf8_lossy(&output.text)); if let Some(line) = first_matching_line(&mut pending, regex) { - println!("{line}"); + outln!("{line}"); return Ok(0); } if output.flags & yas_wire::schema::terminal::OUTPUT_TRUNCATED as u16 != 0 { @@ -1346,7 +1431,7 @@ fn first_matching_line(pending: &mut String, regex: ®ex::Regex) -> Option Result { - println!("{}", terminal_status(record)?); + outln!("{}", terminal_status(record)?); terminal_exit_code(record) } @@ -1711,4 +1796,24 @@ mod tests { assert_eq!(launch.environment[1].key, b"Z"); assert_eq!(launch.extensions.0[0].value, 3_000_000_000u64.to_le_bytes()); } + + #[test] + fn output_collection_limit_covers_result_framing() { + for text_len in [0usize, 1, 4096] { + let encoded = terminal::OutputResult { + generation: 1, + flags: 0, + start_seq: u64::MAX / 2, + start_col: 7, + next_seq: u64::MAX / 2, + next_col: 7 + text_len as u32, + text: vec![b'x'; text_len], + } + .encode() + .unwrap(); + let limit = output_collection_limit(text_len as u32); + assert!(encoded.len() as u64 <= limit, "{} > {limit}", encoded.len()); + } + assert_eq!(output_collection_limit(u32::MAX), MAX_QUERY_BYTES); + } } diff --git a/crates/compositor/tests/direct_touch.rs b/crates/compositor/tests/direct_touch.rs index bee09217c..e348d5fa2 100644 --- a/crates/compositor/tests/direct_touch.rs +++ b/crates/compositor/tests/direct_touch.rs @@ -18,7 +18,7 @@ use wayland_client::protocol::{ wl_compositor, wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_registry, wl_seat, wl_surface, wl_touch, }; -use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, delegate_noop}; +use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum, delegate_noop}; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; use yas_compositor::{ @@ -26,6 +26,9 @@ use yas_compositor::{ spawn_compositor_without_renderer, }; +/// Generous because CI runners are loaded; a passing run returns long before it. +const DISPATCH_TIMEOUT: Duration = Duration::from_secs(10); + /// A `wl_touch` event, reduced to what these tests pin. #[derive(Debug, PartialEq, Clone)] enum T { @@ -49,6 +52,7 @@ struct App { /// `time` of each motion, which is what an app differentiates to get a fling /// velocity. motion_times: Vec, + touch_capable: bool, } impl Dispatch for App { @@ -153,7 +157,23 @@ impl Dispatch for App { delegate_noop!(App: ignore wl_compositor::WlCompositor); delegate_noop!(App: ignore wl_surface::WlSurface); -delegate_noop!(App: ignore wl_seat::WlSeat); +impl Dispatch for App { + fn event( + state: &mut Self, + _: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + state.touch_capable = capabilities.contains(wl_seat::Capability::Touch); + } + } +} delegate_noop!(App: ignore xdg_toplevel::XdgToplevel); delegate_noop!(App: ignore wl_data_device_manager::WlDataDeviceManager); delegate_noop!(App: ignore wl_data_device::WlDataDevice); @@ -220,8 +240,14 @@ impl Fixture { .send(CompositorCommand::SetTouchEnabled { enabled: true }) .expect("enable touch"); handle.wake(); - std::thread::sleep(Duration::from_millis(50)); - queue.roundtrip(&mut app).expect("capabilities roundtrip"); + // A wl_touch requested before the seat advertises touch never receives + // events, so wait for the capability rather than a fixed delay. + let deadline = Instant::now() + DISPATCH_TIMEOUT; + while !app.touch_capable { + assert!(Instant::now() < deadline, "seat never advertised touch"); + std::thread::sleep(Duration::from_millis(10)); + queue.roundtrip(&mut app).expect("capabilities roundtrip"); + } let touch = seat.get_touch(&qh, ()); queue.roundtrip(&mut app).expect("get_touch roundtrip"); @@ -281,7 +307,7 @@ impl Fixture { } fn dispatch_until(&mut self, done: impl Fn(&App) -> bool) { - let deadline = Instant::now() + Duration::from_secs(2); + let deadline = Instant::now() + DISPATCH_TIMEOUT; while !done(&self.app) { let remaining = deadline.saturating_duration_since(Instant::now()); assert!(!remaining.is_zero(), "timed out waiting for touch events"); @@ -584,8 +610,7 @@ fn direct_touch_has_its_own_browser_clock() { } handle.wake(); - std::thread::sleep(Duration::from_millis(80)); - f.queue.roundtrip(&mut f.app).expect("roundtrip"); + f.dispatch_until(|app| app.motion_times.len() >= 5); let deltas: Vec = f .app .motion_times diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 14906cc8e..8c817423a 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -462,8 +462,9 @@ pub struct Config { /// Skip compositor initialization (e.g. for share-only mode). pub skip_compositor: bool, /// Export the server's IPC path as `YAS_SOCK` in spawned terminals so - /// `yas` invocations inside them target this server. Off by default: - /// `YAS_*` is otherwise stripped from child environments. + /// `yas` invocations inside them target this server. Off by default, + /// though an inherited `YAS_SOCK` naming another server is dropped either + /// way. pub export_sock: bool, /// Append the directory holding the running server binary to `PATH` in /// spawned terminals, so `yas` is callable inside them (Unix only; the @@ -1075,6 +1076,10 @@ struct Pty { /// this a terminal created with an argv, an environment, or both came back /// from a restart as a bare login shell. spec: pty::OwnedChildSpec, + /// The native YAS launch this terminal was created or last replaced + /// with. Terminal Restart replays it for any client that can see the + /// terminal, not only the connection that created it. + native_launch: Option, /// Working directory last reported by the shell via OSC 7, already /// validated by `parse_osc7_url` (docs/protocol.md, "Working directory /// tracking"). Last write wins; None until shell integration first diff --git a/crates/server/src/pty/pty_unix.rs b/crates/server/src/pty/pty_unix.rs index 2e3daf8fa..2bbca4d55 100644 --- a/crates/server/src/pty/pty_unix.rs +++ b/crates/server/src/pty/pty_unix.rs @@ -41,9 +41,10 @@ pub struct ChildSpec<'a> { /// /// `Derived` preserves the default YAS terminal behavior. `Server` and /// `Empty` are the native YAS launch variants. `Server` receives the live -/// session environment before the request's exact entries; `Empty` remains -/// byte-for-byte exact. Neither receives terminal, PATH, locale, or YAS -/// variables unless the launch explicitly supplies them. +/// session environment and the server's `YAS_SOCK`/`PATH` rules (see +/// [`build_child_env`]) before the request's exact entries; `Empty` remains +/// byte-for-byte exact. Neither receives terminal or locale variables unless +/// the launch explicitly supplies them. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ChildEnvironmentBase { #[default] @@ -114,9 +115,16 @@ impl ChildSpec<'_> { /// client entry always wins, whichever layer would otherwise have set the key. /// This is the precedence the process family already documents for /// `PROCESS_SPAWN` (`command_for` in `process.rs`). +/// +/// `yas_sock` is set only when the server exports its socket; `own_sock` is +/// the path it listens on either way. A `Server` base never forwards an +/// inherited `YAS_SOCK` naming some other server, since `yas` inside the +/// terminal would then drive that server instead of this one. +#[allow(clippy::too_many_arguments)] fn build_child_env( session_env: Option<&crate::app_env::SessionEnv>, yas_sock: Option<&str>, + own_sock: Option<&str>, path_dir: Option<&str>, overrides: &[(String, String)], removals: &[String], @@ -158,6 +166,9 @@ fn build_child_env( env.push((key.as_bytes().to_vec(), value.as_bytes().to_vec())); } } + if base == ChildEnvironmentBase::Server { + apply_server_yas_env(&mut env, yas_sock, own_sock, path_dir); + } // The request is the final authority, including over session values. for (key, value) in entries { env.retain(|(candidate, _)| candidate != key); @@ -254,6 +265,78 @@ fn build_child_env( .collect() } +/// The `YAS_SOCK` and `PATH` rules for a `Server` base environment; see +/// [`build_child_env`]. +fn apply_server_yas_env( + env: &mut Vec<(Vec, Vec)>, + yas_sock: Option<&str>, + own_sock: Option<&str>, + path_dir: Option<&str>, +) { + env.retain(|(key, value)| { + key.as_slice() != b"YAS_SOCK" + || own_sock.is_some_and(|own| value.as_slice() == own.as_bytes()) + }); + if let Some(sock) = yas_sock { + env.retain(|(key, _)| key.as_slice() != b"YAS_SOCK"); + env.push((b"YAS_SOCK".to_vec(), sock.as_bytes().to_vec())); + } + if let Some(dir) = path_dir { + match env.iter_mut().find(|(key, _)| key.as_slice() == b"PATH") { + Some((_, value)) => { + if !value + .split(|byte| *byte == b':') + .any(|entry| entry == dir.as_bytes()) + { + if !value.is_empty() { + value.push(b':'); + } + value.extend_from_slice(dir.as_bytes()); + } + } + None => env.push((b"PATH".to_vec(), dir.as_bytes().to_vec())), + } + } +} + +/// The environment a terminal child of this server is started with. +fn spawn_child_env( + spec: &ChildSpec<'_>, + state: &AppState, + session_env: Option<&crate::app_env::SessionEnv>, +) -> Vec { + let yas_sock = state + .config + .export_sock + .then(|| state.config.ipc_path.as_str()); + let path_dir = state.config.inject_path.then(exe_dir).flatten(); + build_child_env( + session_env, + yas_sock, + Some(state.config.ipc_path.as_str()), + path_dir, + spec.env, + spec.env_remove, + spec.environment_base, + spec.exact_env, + ) +} + +/// Whether `spec` names an argv program that cannot be found on the PATH the +/// child would run with, so a failed spawn can be reported as not found +/// rather than as a generic failure. +pub fn launch_program_missing( + spec: ChildSpec<'_>, + state: &AppState, + session_env: Option<&crate::app_env::SessionEnv>, +) -> bool { + let Some(program) = spec.argv.and_then(|argv| argv.first()) else { + return false; + }; + let env = spawn_child_env(&spec, state, session_env); + resolve_in_path(program, child_path(&env).as_deref()).is_none() +} + /// Everything the child needs to `execve`, built entirely before `fork()`. /// /// Nothing here may be deferred to the child: after fork in a multi-threaded @@ -1002,20 +1085,7 @@ pub fn spawn_pty( // Build the child's environment before fork() to avoid calling // set_var/remove_var after fork in a multi-threaded process (UB per POSIX). - let yas_sock = state - .config - .export_sock - .then(|| state.config.ipc_path.as_str()); - let path_dir = state.config.inject_path.then(exe_dir).flatten(); - let child_env = build_child_env( - session_env, - yas_sock, - path_dir, - spec.env, - spec.env_remove, - spec.environment_base, - spec.exact_env, - ); + let child_env = spawn_child_env(&spec, &state, session_env); let child_envp: Vec<*const libc::c_char> = child_env .iter() .map(|c| c.as_ptr()) @@ -1144,6 +1214,7 @@ pub fn spawn_pty( exit_status: yas_terminal_model::EXIT_STATUS_UNKNOWN, command: list_command.map(str::to_owned), spec: spec.to_owned_spec(), + native_launch: None, osc7_cwd: None, journal: crate::journal::CommandJournal::default(), osc_carry: Vec::new(), @@ -1184,20 +1255,7 @@ pub fn respawn_child( } // Build the child's environment before fork() (same rationale as spawn_pty). - let yas_sock = state - .config - .export_sock - .then(|| state.config.ipc_path.as_str()); - let path_dir = state.config.inject_path.then(exe_dir).flatten(); - let child_env = build_child_env( - session_env, - yas_sock, - path_dir, - spec.env, - spec.env_remove, - spec.environment_base, - spec.exact_env, - ); + let child_env = spawn_child_env(&spec, &state, session_env); let child_envp: Vec<*const libc::c_char> = child_env .iter() .map(|c| c.as_ptr()) @@ -1286,8 +1344,8 @@ pub fn respawn_child( #[cfg(test)] mod tests { use super::{ - ChildEnvironmentBase, ChildSpec, PtyHandle, build_child_env, child_path, - collect_exit_status, plan_exec, reap_zombies, resolve_in_path, + ChildEnvironmentBase, ChildSpec, PtyHandle, apply_server_yas_env, build_child_env, + child_path, collect_exit_status, plan_exec, reap_zombies, resolve_in_path, }; use std::collections::HashMap; use std::ffi::CString; @@ -1476,6 +1534,7 @@ mod tests { build_child_env( Some(&session_env), yas_sock, + None, path_dir, &[], &[], @@ -1873,6 +1932,7 @@ mod tests { Some(&session_env), None, None, + None, &[], &[], ChildEnvironmentBase::Empty, @@ -1906,6 +1966,7 @@ mod tests { Some(&session_env), None, None, + None, &[], &[], ChildEnvironmentBase::Server, @@ -1951,6 +2012,7 @@ mod tests { Some(&session_env), Some("/tmp/yas-test/ipc.sock"), None, + None, &overrides(&[ // A plain addition. ("YAS_PROBE", "hello"), @@ -1991,6 +2053,7 @@ mod tests { None, None, None, + None, &overrides(&[("PATH", dir.to_str().unwrap())]), &[], ChildEnvironmentBase::Derived, @@ -2152,6 +2215,75 @@ mod tests { ); } + fn server_env( + inherited: &[(&str, &str)], + yas_sock: Option<&str>, + own_sock: Option<&str>, + path_dir: Option<&str>, + ) -> HashMap { + let mut env = inherited + .iter() + .map(|(key, value)| (key.as_bytes().to_vec(), value.as_bytes().to_vec())) + .collect(); + apply_server_yas_env(&mut env, yas_sock, own_sock, path_dir); + env.into_iter() + .map(|(key, value)| { + ( + String::from_utf8(key).unwrap(), + String::from_utf8(value).unwrap(), + ) + }) + .collect() + } + + #[test] + fn server_env_drops_a_yas_sock_naming_another_server() { + let env = server_env( + &[("YAS_SOCK", "/run/other.sock")], + None, + Some("/run/own.sock"), + None, + ); + assert!(!env.contains_key("YAS_SOCK")); + + let env = server_env( + &[("YAS_SOCK", "/run/own.sock")], + None, + Some("/run/own.sock"), + None, + ); + assert_eq!( + env.get("YAS_SOCK").map(String::as_str), + Some("/run/own.sock") + ); + + let env = server_env( + &[("YAS_SOCK", "/run/other.sock")], + Some("/run/own.sock"), + Some("/run/own.sock"), + None, + ); + assert_eq!( + env.get("YAS_SOCK").map(String::as_str), + Some("/run/own.sock") + ); + } + + #[test] + fn server_env_appends_the_binary_dir_to_path_once() { + let env = server_env(&[("PATH", "/usr/bin:/bin")], None, None, Some("/opt/yas")); + assert_eq!( + env.get("PATH").map(String::as_str), + Some("/usr/bin:/bin:/opt/yas") + ); + let env = server_env(&[("PATH", "/opt/yas:/bin")], None, None, Some("/opt/yas")); + assert_eq!(env.get("PATH").map(String::as_str), Some("/opt/yas:/bin")); + let env = server_env(&[], None, None, Some("/opt/yas")); + assert_eq!(env.get("PATH").map(String::as_str), Some("/opt/yas")); + let env = server_env(&[("PATH", "/bin")], None, None, None); + assert_eq!(env.get("PATH").map(String::as_str), Some("/bin")); + } + #[test] fn child_env_exports_yas_sock_only_when_requested() { let env = child_env_map(session_child_env(None, None, None, None, None, None, None)); diff --git a/crates/server/src/pty/pty_windows.rs b/crates/server/src/pty/pty_windows.rs index 36a8db838..a76116da3 100644 --- a/crates/server/src/pty/pty_windows.rs +++ b/crates/server/src/pty/pty_windows.rs @@ -513,6 +513,16 @@ fn exact_environment_block(spec: &ChildSpec<'_>) -> Option> { Some(block) } +/// Windows resolves the program inside `CreateProcessW`, so a missing one is +/// not distinguished from any other spawn failure. +pub fn launch_program_missing( + _spec: ChildSpec<'_>, + _state: &AppState, + _session_env: Option<&crate::app_env::SessionEnv>, +) -> bool { + false +} + #[allow(clippy::too_many_arguments)] pub fn spawn_pty( shell: &str, @@ -692,6 +702,7 @@ pub fn spawn_pty( exit_status: yas_terminal_model::EXIT_STATUS_UNKNOWN, command: list_command.map(str::to_owned), spec: spec.to_owned_spec(), + native_launch: None, osc7_cwd: None, journal: crate::journal::CommandJournal::default(), osc_carry: Vec::new(), diff --git a/crates/server/src/yas.rs b/crates/server/src/yas.rs index 7f6d527de..148bb0638 100644 --- a/crates/server/src/yas.rs +++ b/crates/server/src/yas.rs @@ -3849,9 +3849,6 @@ impl TerminalCatalogue { struct TerminalRuntime { catalogue: TerminalCatalogue, app_handles: HashMap, - /// Exact native launch records. Terminals created outside this connection - /// predate its launch catalogue and intentionally have no replayable launch. - launches: HashMap, operations: HashMap<[u8; 16], TerminalOperationReplay>, next_view_id: u32, views: HashMap, @@ -4956,7 +4953,6 @@ impl TerminalRuntime { Self { catalogue, app_handles: HashMap::new(), - launches: HashMap::new(), operations: HashMap::new(), next_view_id: 1, views: HashMap::new(), @@ -6066,6 +6062,7 @@ const fn terminal_query_status(error: &super::yas_terminal_queries::Error) -> St super::yas_terminal_queries::Error::NotFound => Status::NotFound, super::yas_terminal_queries::Error::Stale => Status::Stale, super::yas_terminal_queries::Error::TooLarge => Status::ResourceExhausted, + super::yas_terminal_queries::Error::Timeout => Status::Timeout, super::yas_terminal_queries::Error::Internal => Status::Internal, } } @@ -14472,35 +14469,50 @@ impl Session { // engine initializes this field at zero, so creation sets // the first public generation explicitly. pty.generation = 1; + pty.native_launch = Some(request.launch.clone()); pty.deadline = prepared.deadline_after_ns.and_then(|ns| { std::time::Instant::now().checked_add(Duration::from_nanos(ns)) }); } - pty.and_then(|pty| { - let lifecycle = session.note_terminal_created(pty_id, pty.generation)?; - debug_assert_eq!(lifecycle.backend_id(), pty_id); - debug_assert_eq!(lifecycle.generation(), pty.generation); - let handle = lifecycle.terminal_handle(); - session.ptys.insert(pty_id, pty); - Some((handle, pty_id)) - }) + match pty { + Some(pty) => session + .note_terminal_created(pty_id, pty.generation) + .map(|lifecycle| { + debug_assert_eq!(lifecycle.backend_id(), pty_id); + debug_assert_eq!(lifecycle.generation(), pty.generation); + let handle = lifecycle.terminal_handle(); + session.ptys.insert(pty_id, pty); + (handle, pty_id) + }) + .ok_or(Status::ResourceExhausted), + None if super::pty::launch_program_missing( + prepared.spec.borrowed(&argv), + &state, + session_env.as_ref(), + ) => + { + Err(Status::NotFound) + } + None => Err(Status::ResourceExhausted), + } } else { - None + Err(Status::ResourceExhausted) } }; - let Some((terminal_handle, pty_id)) = created else { - self.record_terminal_replay( - yas_wire::schema::terminal::request::CREATE, - request.operation_id, - fingerprint, - Status::ResourceExhausted, - Vec::new(), - None, - None, - ); - return self - .send_sensitive_result(&frame, Status::ResourceExhausted, Vec::new()) - .await; + let (terminal_handle, pty_id) = match created { + Ok(created) => created, + Err(status) => { + self.record_terminal_replay( + yas_wire::schema::terminal::request::CREATE, + request.operation_id, + fingerprint, + status, + Vec::new(), + None, + None, + ); + return self.send_sensitive_result(&frame, status, Vec::new()).await; + } }; let initial_view_result = if let Some(initial_view) = initial_view { match self @@ -14536,7 +14548,6 @@ impl Session { }; self.refresh_terminal_catalogue().await; let terminal = self.native.as_mut().ok_or(())?; - terminal.launches.insert(terminal_handle, request.launch); if let Some(app_handle) = app_handle { terminal.app_handles.insert(terminal_handle, app_handle); } @@ -14658,11 +14669,14 @@ impl Session { return self.send_result(&frame, Status::NotFound, Vec::new()).await; }; let launch = match request.launch_mode { - yas_terminal::LaunchMode::Replay => self - .native - .as_ref() - .and_then(|terminal| terminal.launches.get(&request.terminal_handle)) - .cloned(), + yas_terminal::LaunchMode::Replay => { + let state = self.native.as_ref().ok_or(())?.state.clone(); + let session = state.session.lock().await; + session + .ptys + .get(&pty_id) + .and_then(|pty| pty.native_launch.clone()) + } yas_terminal::LaunchMode::Replace => request.launch.take(), }; let Some(mut launch) = launch else { @@ -14755,7 +14769,7 @@ impl Session { .send_sensitive_result(&frame, Status::NotFound, Vec::new()) .await; } - let (succeeded, invalidated_views) = { + let (failure, invalidated_views) = { let mut session = state.session.lock().await; let current = session.ptys.get(&pty_id).ok_or(())?; let (rows, cols) = current.driver.size(); @@ -14806,15 +14820,25 @@ impl Session { .ok_or(())?; debug_assert_eq!(lifecycle.backend_id(), pty_id); debug_assert_eq!(lifecycle.generation(), generation); - (true, true) + (None, true) } else { let invalidated = request.cutover_mode == yas_terminal::CutoverMode::StopThenStart; if invalidated { session.native_terminal_views.refresh_backend(pty_id); } - (false, invalidated) + let status = if super::pty::launch_program_missing( + prepared.spec.borrowed(&argv), + &state, + session_env.as_ref(), + ) { + Status::NotFound + } else { + Status::Io + }; + (Some(status), invalidated) } }; + let succeeded = failure.is_none(); let receipts = if invalidated_views { let terminal = self.native.as_mut().ok_or(())?; terminal @@ -14833,7 +14857,7 @@ impl Session { Vec::new() }; self.drain_terminal_frame_receipts(receipts).await?; - if !succeeded { + if let Some(status) = failure { if request.cutover_mode == yas_terminal::CutoverMode::StopThenStart && self.refresh_terminal_catalogue().await { @@ -14843,20 +14867,20 @@ impl Session { yas_wire::schema::terminal::request::RESTART, request.operation_id, fingerprint, - Status::Io, + status, Vec::new(), None, Some(request.terminal_handle), ); - return self - .send_sensitive_result(&frame, Status::Io, Vec::new()) - .await; + return self.send_sensitive_result(&frame, status, Vec::new()).await; + } + if request.launch_mode == yas_terminal::LaunchMode::Replace + && let Some(pty) = state.session.lock().await.ptys.get_mut(&pty_id) + { + pty.native_launch = Some(launch); } self.refresh_terminal_catalogue().await; let terminal = self.native.as_mut().ok_or(())?; - if request.launch_mode == yas_terminal::LaunchMode::Replace { - terminal.launches.insert(request.terminal_handle, launch); - } match app_handle { Some(app_handle) => { terminal @@ -14989,7 +15013,6 @@ impl Session { return self.send_result(&frame, Status::NotFound, Vec::new()).await; } if let Some(terminal) = self.native.as_mut() { - terminal.launches.remove(&request.terminal_handle); terminal.app_handles.remove(&request.terminal_handle); } if self.refresh_terminal_catalogue().await { @@ -27591,9 +27614,6 @@ impl Session { terminal .app_handles .retain(|handle, _| snapshot.records.contains_key(handle)); - terminal - .launches - .retain(|handle, _| snapshot.records.contains_key(handle)); // And the deduplication entries that named those terminals: the // replay guarantee runs to the end of the affected resource's // lifetime, which this is. @@ -52087,6 +52107,143 @@ mod tests { delivery.abort(); } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn terminal_output_past_end_is_empty_and_command_wait_times_out() { + let state = super::super::tests::process_transport::test_state( + super::super::process::Server::new(false, true), + ); + let delivery_state = state.clone(); + let delivery = tokio::spawn(async move { + loop { + tokio::select! { + _ = delivery_state.delivery_notify.notified() => {} + _ = tokio::time::sleep(Duration::from_millis(10)) => {} + } + let _ = super::super::tick(&delivery_state).await; + } + }); + let (mut client, codec, _, server_task) = + start_registered_session(state, &[family::TRANSFER, family::TERMINAL]).await; + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 10, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x43; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"/bin/sh".to_vec(), + b"-c".to_vec(), + b"printf 'ready\\n'; exec cat".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + let mut frames = Vec::new(); + let created = next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CREATE, + 10, + &mut frames, + ) + .await; + assert_eq!(created.status, Status::Ok); + let created = yas_terminal::CreateResult::decode(&created.body).unwrap(); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::OUTPUT, + 11, + &yas_terminal::Output { + terminal_handle: created.terminal_handle, + generation: created.generation, + cursor_kind: yas_wire::schema::terminal::OUTPUT_CURSOR_SEQUENCE as u8, + flags: yas_wire::schema::terminal::OUTPUT_REQUEST_FLAGS as u8, + cursor_a: 1_000_000, + cursor_b: 0, + max_bytes: 64 * 1024, + initial_receive_credit: 64 * 1024, + extensions: Extensions::default(), + }, + ) + .await; + let output = next_sensitive_result( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::OUTPUT, + 11, + ) + .await; + assert_eq!(output.status, Status::Ok); + let yas_terminal::QueryDelivery::Inline(bytes) = + yas_terminal::QueryBody::decode(&output.body) + .unwrap() + .delivery + else { + panic!("an empty OUTPUT answer is inline"); + }; + let output = yas_terminal::OutputResult::decode(&bytes).unwrap(); + assert!(output.text.is_empty()); + assert!(output.next_seq < 1_000_000); + assert_eq!( + (output.start_seq, output.start_col), + (output.next_seq, output.next_col) + ); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::WAIT, + 12, + &yas_terminal::Wait { + terminal_handle: created.terminal_handle, + generation: created.generation, + wait_kind: yas_wire::schema::terminal::WAIT_LATEST_COMMAND as u8, + flags: yas_wire::schema::terminal::WAIT_FLAGS as u8, + cursor_a: 0, + cursor_b: 0, + max_bytes: 64 * 1024, + timeout_ns: 200_000_000, + needle: Vec::new(), + initial_receive_credit: 64 * 1024, + extensions: Extensions::default(), + }, + ) + .await; + assert_eq!( + next_sensitive_result( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::WAIT, + 12, + ) + .await + .status, + Status::Timeout, + ); + + drop(client); + timeout(TEST_TIMEOUT, server_task).await.unwrap().unwrap(); + delivery.abort(); + } + #[cfg(unix)] #[tokio::test(flavor = "multi_thread")] async fn terminal_cwd_rejects_under_credit_and_remains_live_at_exact_credit() { @@ -53475,7 +53632,7 @@ mod tests { ) .await .status, - Status::Io, + Status::NotFound, ); { let shared = state.session.lock().await; @@ -53544,7 +53701,7 @@ mod tests { ) .await .status, - Status::Io, + Status::NotFound, ); { let shared = state.session.lock().await; @@ -53678,6 +53835,148 @@ mod tests { process_service.shutdown().await; } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn terminal_restart_replays_across_connections_and_missing_programs_are_not_found() { + let process_service = super::super::process::Server::new(false, true); + let state = super::super::tests::process_transport::test_state(process_service.clone()); + let (mut creator, creator_codec, _, creator_task) = + start_registered_session(state.clone(), &[family::TERMINAL]).await; + let mut creator_frames = Vec::new(); + write_request( + &mut creator, + &creator_codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 10, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x5a; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"/bin/sh".to_vec(), + b"-c".to_vec(), + b"exit 3".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + let created = next_terminal_result( + &mut creator, + &creator_codec, + yas_wire::schema::terminal::request::CREATE, + 10, + &mut creator_frames, + ) + .await; + assert_eq!(created.status, Status::Ok); + let created = yas_terminal::CreateResult::decode(&created.body).unwrap(); + drop(creator); + timeout(TEST_TIMEOUT, creator_task).await.unwrap().unwrap(); + + let (mut client, codec, _, task) = + start_registered_session(state.clone(), &[family::TERMINAL]).await; + let mut frames = Vec::new(); + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::RESTART, + 11, + &yas_terminal::Restart { + terminal_handle: created.terminal_handle, + operation_id: [0x5b; 16], + launch_mode: yas_terminal::LaunchMode::Replay, + cutover_mode: yas_terminal::CutoverMode::StopThenStart, + launch: None, + extensions: Extensions::default(), + }, + ) + .await; + let restarted = next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::RESTART, + 11, + &mut frames, + ) + .await; + assert_eq!(restarted.status, Status::Ok); + let restarted = yas_terminal::RestartResult::decode(&restarted.body).unwrap(); + assert!(restarted.generation > created.generation); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CREATE, + 13, + &yas_terminal::Create { + rows: 24, + cols: 80, + operation_id: [0x5d; 16], + launch: yas_terminal::Launch { + command: yas_terminal::Command::Argv(vec![ + b"definitely-not-a-yas-terminal-program".to_vec(), + ]), + cwd: yas_terminal::Cwd::ServerDefault, + environment_base: yas_terminal::EnvironmentBase::Server, + environment: Vec::new(), + extensions: Extensions::default(), + }, + extensions: Extensions::default(), + }, + ) + .await; + assert_eq!( + next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CREATE, + 13, + &mut frames, + ) + .await + .status, + Status::NotFound, + ); + + write_request( + &mut client, + &codec, + family::TERMINAL, + yas_wire::schema::terminal::request::CLOSE, + 12, + &yas_terminal::Close { + terminal_handle: created.terminal_handle, + operation_id: [0x5c; 16], + }, + ) + .await; + assert_eq!( + next_terminal_result( + &mut client, + &codec, + yas_wire::schema::terminal::request::CLOSE, + 12, + &mut frames, + ) + .await + .status, + Status::Ok, + ); + drop(client); + timeout(TEST_TIMEOUT, task).await.unwrap().unwrap(); + process_service.shutdown().await; + } + /// A CREATE deduplication entry lasts exactly as long as the terminal it /// created. /// diff --git a/crates/server/src/yas_process.rs b/crates/server/src/yas_process.rs index 05b55eab3..c0993e3b8 100644 --- a/crates/server/src/yas_process.rs +++ b/crates/server/src/yas_process.rs @@ -410,9 +410,17 @@ impl Session { self.remove_route(process_id); return Err(Error::Closed("mismatched Process SPAWN reply".to_owned())); } - route - .process_handle - .store(started.process_handle, Ordering::Release); + { + // Serialized with the Exit handler through `exits`: a child that + // exits before its handle is stored still leaves a WAIT replay. + let mut exits = self.inner.exits.lock().unwrap(); + route + .process_handle + .store(started.process_handle, Ordering::Release); + if let Some(exit) = route.exit.borrow().clone() { + exits.insert(started.process_handle, exit); + } + } Ok(Attachment { session: self.clone(), route, @@ -494,6 +502,16 @@ impl Session { let (mut exit, temporary) = if let Some(route) = self.route_by_handle(request.process_handle) { (route.exit.subscribe(), None) + } else if let Some(exit) = self + .inner + .exits + .lock() + .unwrap() + .get(request.process_handle) + .cloned() + { + // The route retires only after its replay is recorded. + return Ok(exit); } else { match self .watch_process(request.process_handle, false, true) @@ -874,18 +892,19 @@ fn dispatch_outbound(inner: &Arc, event: process::NativeEvent) -> .routes .lock() .unwrap() - .remove(&process_id) + .get(&process_id) + .cloned() .ok_or_else(|| Error::Closed("exit for unknown Process binding".to_owned()))?; let exit = native_exit_info(exit); - let process_handle = route.process_handle.load(Ordering::Acquire); - if process_handle != 0 { - inner - .exits - .lock() - .unwrap() - .insert(process_handle, exit.clone()); + { + let mut exits = inner.exits.lock().unwrap(); + route.exit.send_replace(Some(exit.clone())); + let process_handle = route.process_handle.load(Ordering::Acquire); + if process_handle != 0 { + exits.insert(process_handle, exit.clone()); + } } - route.exit.send_replace(Some(exit.clone())); + inner.routes.lock().unwrap().remove(&process_id); let _ = route.events.try_send(Event::Exit(exit)); Ok(()) } @@ -1182,6 +1201,44 @@ mod tests { server.shutdown().await; } + #[tokio::test] + async fn wait_after_a_fast_exit_returns_the_exit_record() { + let server = Server::new(false, true); + let runtime = Runtime::new(server.clone()); + let session = runtime.session([4; 16], None).unwrap(); + for _ in 0..50 { + let mut attachment = session + .spawn( + &spawn_request( + vec![executable("sh"), b"-c".to_vec(), b"exit 7".to_vec()], + Vec::new(), + ), + None, + ) + .await + .unwrap(); + let handle = attachment.process_handle; + while !matches!( + tokio::time::timeout(Duration::from_secs(5), attachment.next()) + .await + .unwrap() + .unwrap(), + Event::Exit(_) + ) {} + let exit = session + .wait(&wire::Wait { + process_handle: handle, + timeout_ns: 5_000_000_000, + extensions: Extensions::default(), + }) + .await + .unwrap(); + assert_eq!(exit.code, 7); + } + session.shutdown().await; + server.shutdown().await; + } + #[tokio::test] async fn empty_environment_is_exact_and_wait_observes_exit() { let server = Server::new(false, true); diff --git a/crates/server/src/yas_terminal_queries.rs b/crates/server/src/yas_terminal_queries.rs index 14db4914d..e51bcf395 100644 --- a/crates/server/src/yas_terminal_queries.rs +++ b/crates/server/src/yas_terminal_queries.rs @@ -49,6 +49,9 @@ pub(crate) enum Error { NotFound, Stale, TooLarge, + /// A command wait ran out of time before any command it could report on + /// had started. + Timeout, Internal, } @@ -983,8 +986,13 @@ fn poll_command_wait(pty: &Pty, index: u64, timed_out: bool) -> Result Vec<(String, String)> { read_remotes_full() @@ -714,18 +714,13 @@ pub fn read_remotes() -> Vec<(String, String)> { /// Read `yas.remotes` including disabled entries. pub fn read_remotes_full() -> Vec { - let path = remotes_path(); - let contents = match std::fs::read_to_string(&path) { + read_remotes_at(&remotes_path()) +} + +fn read_remotes_at(path: &std::path::Path) -> Vec { + let contents = match std::fs::read_to_string(path) { Ok(c) => c, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - let default = vec![RemoteEntry { - name: "local".to_string(), - uri: "local".to_string(), - disabled: false, - }]; - write_remotes(&default); - return default; - } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return vec![], Err(e) => { eprintln!("yas: could not read {}: {e}", path.display()); return vec![]; @@ -1157,6 +1152,14 @@ fn parse_config_str(contents: &str) -> HashMap { mod tests { use super::*; + #[test] + fn reading_missing_remotes_file_does_not_create_it() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("yas.remotes"); + assert!(read_remotes_at(&path).is_empty()); + assert!(!path.exists()); + } + #[cfg(unix)] #[test] fn packaged_system_socket_prefers_private_user_directory() { diff --git a/docs/design/net.md b/docs/design/net.md index c9957fc8e..173b1b853 100644 --- a/docs/design/net.md +++ b/docs/design/net.md @@ -308,7 +308,7 @@ expectation. ### A named list: `yas.forwards` -The same shape as `yas.remotes` ([../README.md](../../README.md), +The same shape as the legacy `yas.remotes` file ([../README.md](../../README.md), [crates/webserver/src/config.rs](../../crates/webserver/src/config.rs)): its own ordered file at `~/.config/yas/yas.forwards`, `name = spec` per line, `#`-prefixed lines meaning **disabled but preserved**, mode 0600. `yas.conf` is a flat key→value map and cannot hold an ordered diff --git a/docs/design/term-journal.md b/docs/design/term-journal.md index 303d83ca9..51e3afeb0 100644 --- a/docs/design/term-journal.md +++ b/docs/design/term-journal.md @@ -199,9 +199,15 @@ yas terminal history ID --since CURSOR [--max-bytes N] [--json] `journal` prints the newest 20 records by default. `output` defaults to the newest command; `--wait` blocks server-side and exits with that command's -status (124 on timeout). `history --since` takes `SEQ`, `SEQ:COL`, `now`, or -`start`; the reply prints the next cursor so it can be fed back in. Default -`--max-bytes` is 256 KiB. +status (124 on timeout). Without an index, `--wait` targets the running command +or, when none is running, the next one to start, so `send` followed by +`output --wait` cannot race to the previous command. A command wait that times +out before any such command starts answers `TIMEOUT`. + +`history --since` takes `SEQ`, `SEQ:COL`, `now`, or `start`; the reply prints +the next cursor so it can be fed back in. A cursor past the end prints nothing +and reports the current end as the next cursor. Default `--max-bytes` is +256 KiB. `wait --pattern` probes the cursor first and then matches only subsequent `OUTPUT` text. The native client requires the selected Terminal v1 contract; diff --git a/docs/design/yas.md b/docs/design/yas.md index c07f47f52..0dac3132a 100644 --- a/docs/design/yas.md +++ b/docs/design/yas.md @@ -1564,6 +1564,9 @@ future REPLAY operations. REPLACE does not merge with the old command, cwd, environment, deadline, or application association. It can therefore restart a terminal with any argv or shell command, any valid cwd, and any environment the server OS can represent. REPLAY has no launch bytes; REPLACE requires them. +The stored launch record belongs to the terminal, not to the connection that +created it, so any client that can see the terminal can REPLAY it. A terminal +that was never started from a launch record answers REPLAY with `UNSUPPORTED`. RESTART is valid for RUNNING and EXITED terminals. For an EXITED terminal both cutover modes simply start the new generation. For a RUNNING terminal the @@ -2010,13 +2013,18 @@ index; with it, `from_index` is the number of records skipped back from and PROBE 3. COMMAND uses `a=command_index,b=column`, LATEST_COMMAND requires `a=0`, and SEQUENCE/PROBE use `a=sequence,b=column`; OUTPUT flags are zero. Every OUTPUT next cursor is normalized to SEQUENCE with `a=next_seq` and -`b=next_col`, including COMMAND, LATEST_COMMAND, and PROBE results. +`b=next_col`, including COMMAND, LATEST_COMMAND, and PROBE results. A SEQUENCE +cursor past the end of the output returns empty text whose start and next +cursor are both the current end. WAIT kinds are OUTPUT 0, COMMAND 1, and LATEST_COMMAND 2, with zero flags. OUTPUT uses `a=sequence,b=column`, requires a nonempty needle, and returns an OutputResult. COMMAND uses `a=command_index,b=0` and an empty needle; -LATEST_COMMAND requires `a=b=0` and an empty needle. Both command waits return -a JournalResult containing exactly one command record. Timeouts and byte +LATEST_COMMAND requires `a=b=0` and an empty needle; it selects the running +command, or the next command to start when none is running. Both command waits +return a JournalResult containing exactly one command record. A command wait +whose timeout expires before its command exists fails with `TIMEOUT`; one whose +terminal has exited without it fails with `NOT_FOUND`. Timeouts and byte limits are nonzero. COPY_RANGE rows are inclusive, oldest-retained-relative when nonnegative, and diff --git a/docs/server.md b/docs/server.md index bc7173851..2a6280587 100644 --- a/docs/server.md +++ b/docs/server.md @@ -78,6 +78,10 @@ connections that are no longer needed. | `YAS_NOTIFICATION_TIMEOUT_MIN_MS` | `1000` | Lower clamp for positive application notification timeouts | | `YAS_NOTIFICATION_TIMEOUT_MAX_MS` | `86400000` | Upper clamp for positive application notification timeouts | +A terminal never inherits a `YAS_SOCK` from the server's own environment that +names a different server; `YAS_EXPORT_SOCK` replaces it with this server's +socket. + ### Named instances Every server has a name. `yas server` uses `default`; `yas server --name NAME` diff --git a/js/core/package.json b/js/core/package.json index b749b58ef..140c280b1 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -108,7 +108,7 @@ "test:watch": "vitest" }, "peerDependencies": { - "@yas-run/browser": ">=0.9.0" + "@yas-run/browser": "^0.3.0" }, "dependencies": { "tweetnacl": "^1.0.3" diff --git a/js/core/src/AudioPlayer.ts b/js/core/src/AudioPlayer.ts index a7cd24c01..88d17a541 100644 --- a/js/core/src/AudioPlayer.ts +++ b/js/core/src/AudioPlayer.ts @@ -21,7 +21,7 @@ * from a Blob URL — no external file needed. */ -import { claimPlaybackAudioSession } from "./audioSession"; +import { claimPlaybackAudioSession } from "./audioSession.js"; /** * Maximum pre-worklet staging depth in decoded frames (~20 ms each). diff --git a/js/core/src/SurfaceStore.ts b/js/core/src/SurfaceStore.ts index b8dd9cb8d..82dff74ab 100644 --- a/js/core/src/SurfaceStore.ts +++ b/js/core/src/SurfaceStore.ts @@ -4,9 +4,9 @@ import { videoColorSpace, SDR_COLOR, rejectSurfaceColorProfile, -} from "./surfaceColor"; -import type { YasSurfaceColorSpace } from "./yas/packed"; -import type { YasSurface, ConnectionId, SurfaceId } from "./types"; +} from "./surfaceColor.js"; +import type { YasSurfaceColorSpace } from "./yas/packed.js"; +import type { YasSurface, ConnectionId, SurfaceId } from "./types.js"; import { SURFACE_FRAME_FLAG_KEYFRAME, SURFACE_FRAME_CODEC_MASK, @@ -15,11 +15,11 @@ import { CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; +} from "./surfaceModel.js"; // Shared with the codec probe rather than duplicated: the probe answers // for what this browser accepts at a given level, and a decoder configured // here at a different one would be asking a question nobody answered. -import { av1LevelString, av1SequenceProfile } from "./videoCodec"; +import { av1LevelString, av1SequenceProfile } from "./videoCodec.js"; /** Configure from the encoded stream, not the pane or logical window. Without * size hints Chromium guesses 1280×720 when initializing hardware decoding. diff --git a/js/core/src/TerminalStore.ts b/js/core/src/TerminalStore.ts index 487d59e80..25078dcfd 100644 --- a/js/core/src/TerminalStore.ts +++ b/js/core/src/TerminalStore.ts @@ -1,12 +1,12 @@ import type { Terminal } from "@yas-run/browser"; -import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from "./types"; -import type { ConnectionStatus, TerminalId, TerminalPalette } from "./types"; +import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from "./types.js"; +import type { ConnectionStatus, TerminalId, TerminalPalette } from "./types.js"; import { createGlRenderer, createCanvas2dRenderer, type GlRenderer, -} from "./gl-renderer"; -import { createWebGpuRenderer } from "./webgpu-renderer"; +} from "./gl-renderer.js"; +import { createWebGpuRenderer } from "./webgpu-renderer.js"; const DISPLAY_FPS_SAMPLE_COUNT = 120; diff --git a/js/core/src/YasNativeWorkspaceConnection.ts b/js/core/src/YasNativeWorkspaceConnection.ts index b95fec79f..28680cf4b 100644 --- a/js/core/src/YasNativeWorkspaceConnection.ts +++ b/js/core/src/YasNativeWorkspaceConnection.ts @@ -2,19 +2,19 @@ import { detectSurfaceColorCapabilities, surfaceColorExtensions, onSurfaceColorChange, -} from "./surfaceColor"; -import { plannedDropExtension, plannedDropName } from "./surfaceDrop"; -import { AudioPlayer } from "./AudioPlayer"; -import { serverPlatform, type YasPlatform } from "./yas/core"; -import { DesktopStore } from "./desktopModel"; -import { EXIT_STATUS_UNKNOWN } from "./exit-status"; -import { MediaStore, MprisStore } from "./mediaModel"; -import { SurfaceStore, type SurfaceFrameAckToken } from "./SurfaceStore"; -import { TerminalStore, type YasWasmModule } from "./TerminalStore"; +} from "./surfaceColor.js"; +import { plannedDropExtension, plannedDropName } from "./surfaceDrop.js"; +import { AudioPlayer } from "./AudioPlayer.js"; +import { serverPlatform, type YasPlatform } from "./yas/core.js"; +import { DesktopStore } from "./desktopModel.js"; +import { EXIT_STATUS_UNKNOWN } from "./exit-status.js"; +import { MediaStore, MprisStore } from "./mediaModel.js"; +import { SurfaceStore, type SurfaceFrameAckToken } from "./SurfaceStore.js"; +import { TerminalStore, type YasWasmModule } from "./TerminalStore.js"; import { currentBrowserClipboardEpoch, noteBrowserClipboardMayHaveChanged as noteBrowserClipboardChange, -} from "./clipboardAuthority"; +} from "./clipboardAuthority.js"; import type { ConnectionId, ConnectionStatus, @@ -26,7 +26,7 @@ import type { YasSearchResult, YasSession, YasClientList, -} from "./types"; +} from "./types.js"; import { CODEC_SUPPORT_AV1, CODEC_SUPPORT_AV1_444, @@ -36,18 +36,18 @@ import { SURFACE_FRAME_CODEC_H264, SURFACE_FRAME_CODEC_PNG, SURFACE_FRAME_FLAG_KEYFRAME, -} from "./surfaceModel"; -import { getCodecSupport } from "./YasSurfaceCanvas"; +} from "./surfaceModel.js"; +import { getCodecSupport } from "./YasSurfaceCanvas.js"; import type { AwaitSessionExitOptions, CreateSessionOptions, SurfaceTarget, -} from "./workspaceConnectionTypes"; +} from "./workspaceConnectionTypes.js"; import type { SurfaceAxisEvent, SurfaceDragItem, SurfaceTouchPoint, -} from "./input"; +} from "./input.js"; import { YAS_FAMILY_SELECTION, YAS_FAMILY_FONT, @@ -156,58 +156,58 @@ import { YAS_STATUS_UNAVAILABLE, YAS_STATUS_UNSUPPORTED, YAS_TERMINAL_COPY_RANGE, -} from "./yas/generated"; -import * as yasGenerated from "./yas/generated"; -import { YasFontClient, YasFontProtocol } from "./yas/font"; -import { YasNativeDesktopClientLifecycle } from "./yas/nativeDesktopMedia"; +} from "./yas/generated.js"; +import * as yasGenerated from "./yas/generated.js"; +import { YasFontClient, YasFontProtocol } from "./yas/font.js"; +import { YasNativeDesktopClientLifecycle } from "./yas/nativeDesktopMedia.js"; import { YasNativeExtensionFacade, type YasNativeExtensionInstallRequest, -} from "./yas/nativeExtensionFacade"; -import type { YasExtensionRecord } from "./yas/extension"; +} from "./yas/nativeExtensionFacade.js"; +import type { YasExtensionRecord } from "./yas/extension.js"; import { YasNativeChannelFacade, type YasNativeChannelHandle, type YasNativeChannelNamesWatch, type YasNativeChannelOpenOptions, -} from "./yas/nativeChannelFacade"; -import { YasNativeProductFamilies } from "./yas/nativeProductFamilies"; -import { decodeSurfaceCodecPayload } from "./yas/packed"; +} from "./yas/nativeChannelFacade.js"; +import { YasNativeProductFamilies } from "./yas/nativeProductFamilies.js"; +import { decodeSurfaceCodecPayload } from "./yas/packed.js"; import { YasNativeWorkspaceFs, type YasNativeFsSyncHandle, type YasNativeFsSyncOptions, -} from "./yas/nativeWorkspaceFs"; +} from "./yas/nativeWorkspaceFs.js"; import { YasNativeWorkspaceGit, type YasNativeGitDiscoverOptions, type YasNativeGitFoundRepo, type YasNativeGitOpenOptions, type YasNativeGitRepoHandle, -} from "./yas/nativeWorkspaceGit"; +} from "./yas/nativeWorkspaceGit.js"; import { YasNativeWorkspaceLsp, type YasNativeLspHandle, type YasNativeLspOpenOptions, -} from "./yas/nativeWorkspaceLsp"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel"; +} from "./yas/nativeWorkspaceLsp.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel.js"; import type { WorkspaceSessionKvDeleteOptions, WorkspaceSessionKvPutOptions, WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import { YasSelectionClient, selectionDragDropItemsExtension, type YasSelectionGet, type YasSelectionSlotRecord, -} from "./yas/selection"; +} from "./yas/selection.js"; import { YasConnection as NativeYasSession, type YasInvalidation, -} from "./yas/session"; +} from "./yas/session.js"; import { YasSurfaceClient, surfaceActivationRevision, @@ -219,7 +219,7 @@ import { type YasSurfaceFrame, type YasSurfaceRecord, type YasSurfaceView, -} from "./yas/surface"; +} from "./yas/surface.js"; import { YasTerminalClient, decodeTerminalGridV1, @@ -228,9 +228,9 @@ import { type YasTerminalRecord, type YasTerminalViewConfiguration, type YasTerminalView, -} from "./yas/terminal"; -import { encodeBrowserTerminalGrid } from "./yas/terminalRenderer"; -import { equalBytes, YasResultError, YasWriter } from "./yas/wire"; +} from "./yas/terminal.js"; +import { encodeBrowserTerminalGrid } from "./yas/terminalRenderer.js"; +import { equalBytes, YasResultError, YasWriter } from "./yas/wire.js"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); diff --git a/js/core/src/YasSurfaceCanvas.ts b/js/core/src/YasSurfaceCanvas.ts index 464fa930b..957edab38 100644 --- a/js/core/src/YasSurfaceCanvas.ts +++ b/js/core/src/YasSurfaceCanvas.ts @@ -3,28 +3,28 @@ import { releaseSurfaceCanvas, surface2DContext, SurfaceHdrPresenter, -} from "./surfaceColor"; +} from "./surfaceColor.js"; -import { plannedDropExtension, plannedDropName } from "./surfaceDrop"; -import type { ConnectionId, SurfaceId, YasSurface } from "./types"; -import type { YasNativeFsSyncHandle } from "./yas/nativeWorkspaceFs"; +import { plannedDropExtension, plannedDropName } from "./surfaceDrop.js"; +import type { ConnectionId, SurfaceId, YasSurface } from "./types.js"; +import type { YasNativeFsSyncHandle } from "./yas/nativeWorkspaceFs.js"; import { CODEC_SUPPORT_H264, CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; -import type { YasWorkspace } from "./YasWorkspace"; -import type { YasWorkspaceConnection } from "./YasWorkspace"; +} from "./surfaceModel.js"; +import type { YasWorkspace } from "./YasWorkspace.js"; +import type { YasWorkspaceConnection } from "./YasWorkspace.js"; import type { RemoteSurfaceInput, SurfaceCursorImage, SurfaceCursorRect, SurfaceFramePresentationSize, SurfaceTextInputEvent, -} from "./SurfaceStore"; -import { placeImeTarget } from "./imeTarget"; -import { av1LevelString } from "./videoCodec"; +} from "./SurfaceStore.js"; +import { placeImeTarget } from "./imeTarget.js"; +import { av1LevelString } from "./videoCodec.js"; import { SURFACE_POINTER_DOWN, SURFACE_POINTER_UP, @@ -37,7 +37,7 @@ import { SURFACE_TOUCH_DOWN, SURFACE_TOUCH_MOTION, SURFACE_TOUCH_UP, -} from "./input"; +} from "./input.js"; import { SCROLL_STOP_MS, WHEEL_DETENT_PX, @@ -45,16 +45,16 @@ import { WHEEL_LINES_PER_DETENT, WHEEL_MODE_LINE, WHEEL_MODE_PAGE, -} from "./wheel"; +} from "./wheel.js"; import { devicePixelBox, drawHalved, halve, halvings, octaveCeil, -} from "./downscale"; +} from "./downscale.js"; -export { av1LevelString } from "./videoCodec"; +export { av1LevelString } from "./videoCodec.js"; /** Cached codec support bitmask. Computed once, reused for all resize messages. */ let _codecSupport: number | null = null; @@ -1483,7 +1483,7 @@ export class YasSurfaceCanvas { private _hasPresentedFirstFrame = false; /** Cached store reference so we can keep the frame listener alive * even when the connection is temporarily unavailable. */ - private _store: import("./SurfaceStore").SurfaceStore | null = null; + private _store: import("./SurfaceStore.js").SurfaceStore | null = null; private _connection: YasWorkspaceConnection | null; private _releasingConnection = false; private _workspaceUnsub: (() => void) | null = null; @@ -2598,8 +2598,9 @@ export class YasSurfaceCanvas { /** Register this visible mount with the connection exactly once. */ private serverSubscribe( conn: YasWorkspaceConnection | null = this.getConn(), - store: import("./SurfaceStore").SurfaceStore | null = conn?.surfaceStore ?? - this._store, + store: + | import("./SurfaceStore.js").SurfaceStore + | null = conn?.surfaceStore ?? this._store, ): void { if ( !this._live || @@ -2780,7 +2781,9 @@ export class YasSurfaceCanvas { } /** Copy the shared backing canvas onto our visible canvas. */ - private presentFromStore(store: import("./SurfaceStore").SurfaceStore): void { + private presentFromStore( + store: import("./SurfaceStore.js").SurfaceStore, + ): void { const src = store.getCanvas(this._surfaceId); const canvas = this.canvas; if (!src || !canvas) return; diff --git a/js/core/src/YasTerminalSurface.ts b/js/core/src/YasTerminalSurface.ts index 69206bf62..7f0cd37a0 100644 --- a/js/core/src/YasTerminalSurface.ts +++ b/js/core/src/YasTerminalSurface.ts @@ -1,11 +1,15 @@ import type { Terminal } from "@yas-run/browser"; -import type { YasWorkspace } from "./YasWorkspace"; -import type { YasWorkspaceConnection } from "./YasWorkspace"; -import type { TerminalPalette, ConnectionStatus, SessionId } from "./types"; -import { DEFAULT_FONT, DEFAULT_FONT_SIZE, DEFAULT_TEXT_GAMMA } from "./types"; -import { cancelFrame, scheduleFrame } from "./frameScheduler"; -import { measureCell, cssFontFamily, type CellMetrics } from "./measure"; -import type { GlRenderer } from "./gl-renderer"; +import type { YasWorkspace } from "./YasWorkspace.js"; +import type { YasWorkspaceConnection } from "./YasWorkspace.js"; +import type { TerminalPalette, ConnectionStatus, SessionId } from "./types.js"; +import { + DEFAULT_FONT, + DEFAULT_FONT_SIZE, + DEFAULT_TEXT_GAMMA, +} from "./types.js"; +import { cancelFrame, scheduleFrame } from "./frameScheduler.js"; +import { measureCell, cssFontFamily, type CellMetrics } from "./measure.js"; +import type { GlRenderer } from "./gl-renderer.js"; import { keyToBytes, encoder, @@ -13,14 +17,14 @@ import { refreshKeyboardLayout, encodeTerminalText, REPORT_ALL, -} from "./keyboard"; -import { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input"; -import { YAS_TERMINAL_WHEEL_SOURCE_FINGER } from "./yas/generated"; -import { assessUrl, openUrlSafely, type UrlAssessment } from "./urlSecurity"; -import { devicePixelBox, drawHalved, halve, halvings } from "./downscale"; -import { gridCaretRect, placeChip, placeImeTarget } from "./imeTarget"; -import { captureDelta } from "./prediction"; -import { WheelDetents, notchedRows } from "./wheel"; +} from "./keyboard.js"; +import { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input.js"; +import { YAS_TERMINAL_WHEEL_SOURCE_FINGER } from "./yas/generated.js"; +import { assessUrl, openUrlSafely, type UrlAssessment } from "./urlSecurity.js"; +import { devicePixelBox, drawHalved, halve, halvings } from "./downscale.js"; +import { gridCaretRect, placeChip, placeImeTarget } from "./imeTarget.js"; +import { captureDelta } from "./prediction.js"; +import { WheelDetents, notchedRows } from "./wheel.js"; /** One screen row's slice of a hyperlink's extent, inclusive of both columns. */ interface LinkSegment { diff --git a/js/core/src/YasWorkspace.ts b/js/core/src/YasWorkspace.ts index 6627db509..9d0eeab42 100644 --- a/js/core/src/YasWorkspace.ts +++ b/js/core/src/YasWorkspace.ts @@ -1,6 +1,6 @@ -import { YasUplinkTransport } from "./transports/uplink"; -import { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection"; -import type { AwaitSessionExitOptions } from "./workspaceConnectionTypes"; +import { YasUplinkTransport } from "./transports/uplink.js"; +import { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection.js"; +import type { AwaitSessionExitOptions } from "./workspaceConnectionTypes.js"; import type { YasConnectionSnapshot, YasSearchResult, @@ -11,35 +11,35 @@ import type { SessionId, SurfaceId, TransportConfig, -} from "./types"; -import type { YasWasmModule } from "./TerminalStore"; -import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel"; +} from "./types.js"; +import type { YasWasmModule } from "./TerminalStore.js"; +import type { FsFileIndex, FsGrepOptions, FsGrepResult } from "./fsModel.js"; import type { YasNativeFsSyncHandle, YasNativeFsSyncOptions, -} from "./yas/nativeWorkspaceFs"; +} from "./yas/nativeWorkspaceFs.js"; import type { YasNativeGitDiscoverOptions, YasNativeGitFoundRepo, YasNativeGitOpenOptions, YasNativeGitRepoHandle, -} from "./yas/nativeWorkspaceGit"; +} from "./yas/nativeWorkspaceGit.js"; import type { WorkspaceSessionKvDeleteOptions, WorkspaceSessionKvPutOptions, WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import type { YasNativeLspHandle, YasNativeLspOpenOptions, -} from "./yas/nativeWorkspaceLsp"; -import { createShareTransport } from "./transports/webrtc-share"; -import { YasActivityStore } from "./activity"; -import { YasConnection as NativeYasConnection } from "./yas/session"; -import { YasEdgeWebSocketTransport } from "./yas/edge"; -import { yasBrowserConnectionOptions } from "./yas/defaults"; -import { retainBrowserClipboardObserver } from "./clipboardAuthority"; +} from "./yas/nativeWorkspaceLsp.js"; +import { createShareTransport } from "./transports/webrtc-share.js"; +import { YasActivityStore } from "./activity.js"; +import { YasConnection as NativeYasConnection } from "./yas/session.js"; +import { YasEdgeWebSocketTransport } from "./yas/edge.js"; +import { yasBrowserConnectionOptions } from "./yas/defaults.js"; +import { retainBrowserClipboardObserver } from "./clipboardAuthority.js"; export interface AddYasConnectionOptions { id: ConnectionId; diff --git a/js/core/src/__tests__/unix-transport.test.ts b/js/core/src/__tests__/unix-transport.test.ts index c9c5a930b..3cbb7a0ec 100644 --- a/js/core/src/__tests__/unix-transport.test.ts +++ b/js/core/src/__tests__/unix-transport.test.ts @@ -8,6 +8,8 @@ import { BunUnixSocketTransport } from "../transports/unix-bun"; import { NodeUnixSocketTransport } from "../transports/unix"; import type { AbstractUnixSocketTransport } from "../transports/unix-base"; import type { UnixSocketTransportOptions } from "../transports/unix-base"; +import { YasConnection } from "../yas/session"; +import { YAS_PREFACE } from "../yas/wire"; // These tests exercise real AF_UNIX sockets on POSIX. The transport is // intentionally local-only, so we skip on platforms where @@ -45,24 +47,17 @@ for (const { name, skip, make } of suites) { let sockPath: string; let server: Server; let lastClient: Socket | null = null; - const clientMessages: Buffer[] = []; + let clientBytes = Buffer.alloc(0); beforeEach(async () => { tmp = mkdtempSync(join(tmpdir(), "yas-unix-transport-")); sockPath = join(tmp, "yas.sock"); - clientMessages.length = 0; + clientBytes = Buffer.alloc(0); lastClient = null; server = createServer((socket) => { lastClient = socket; - let buf = Buffer.alloc(0); socket.on("data", (chunk: Buffer) => { - buf = buf.byteLength === 0 ? chunk : Buffer.concat([buf, chunk]); - while (buf.byteLength >= 4) { - const len = buf.readUInt32LE(0); - if (buf.byteLength < 4 + len) break; - clientMessages.push(Buffer.from(buf.subarray(4, 4 + len))); - buf = buf.subarray(4 + len); - } + clientBytes = Buffer.concat([clientBytes, chunk]); }); }); await new Promise((resolve) => server.listen(sockPath, resolve)); @@ -73,13 +68,6 @@ for (const { name, skip, make } of suites) { rmSync(tmp, { recursive: true, force: true }); }); - function framed(payload: Uint8Array): Buffer { - const out = Buffer.alloc(4 + payload.byteLength); - out.writeUInt32LE(payload.byteLength, 0); - out.set(payload, 4); - return out; - } - async function waitFor( probe: () => T | null | undefined, timeoutMs = 1000, @@ -110,54 +98,59 @@ for (const { name, skip, make } of suites) { t.close(); }); - it("sends length-prefixed frames", async () => { + it("is a YAS byte-stream transport", () => { const t = make(sockPath); - t.connect(); - await waitFor(() => t.status === "connected"); - t.send(new Uint8Array([1, 2, 3])); - await waitFor(() => clientMessages.length > 0); - expect(Array.from(clientMessages[0]!)).toEqual([1, 2, 3]); + expect(t.yasFraming).toBe("stream"); t.close(); }); - it("decodes length-prefixed frames from the server", async () => { + it("writes bytes unchanged", async () => { const t = make(sockPath); - const received: ArrayBuffer[] = []; - t.addEventListener("message", (d) => received.push(d)); t.connect(); await waitFor(() => t.status === "connected"); - await waitFor(() => lastClient !== null); - lastClient!.write(framed(new Uint8Array([0xaa, 0xbb]))); - lastClient!.write(framed(new Uint8Array([0xcc]))); - await waitFor(() => received.length === 2); - expect(Array.from(new Uint8Array(received[0]!))).toEqual([0xaa, 0xbb]); - expect(Array.from(new Uint8Array(received[1]!))).toEqual([0xcc]); + t.send(new Uint8Array([1, 2, 3])); + t.send(new Uint8Array([4])); + await waitFor(() => clientBytes.byteLength === 4); + expect(Array.from(clientBytes)).toEqual([1, 2, 3, 4]); t.close(); }); - it("handles split reads across frame boundaries", async () => { + it("delivers server bytes unchanged across chunk boundaries", async () => { const t = make(sockPath); - const received: ArrayBuffer[] = []; - t.addEventListener("message", (d) => received.push(d)); + const received: number[] = []; + t.addEventListener("message", (d) => received.push(...new Uint8Array(d))); t.connect(); await waitFor(() => t.status === "connected"); await waitFor(() => lastClient !== null); - const whole = framed(new Uint8Array([1, 2, 3, 4, 5])); - // Deliver one byte at a time — the framer must wait for the full frame. - for (let i = 0; i < whole.byteLength; i++) { - lastClient!.write(whole.subarray(i, i + 1)); - } - await waitFor(() => received.length === 1); - expect(Array.from(new Uint8Array(received[0]!))).toEqual([1, 2, 3, 4, 5]); + const whole = [0xaa, 0xbb, 0xcc, 0xdd, 0xee]; + for (const byte of whole) lastClient!.write(Uint8Array.of(byte)); + await waitFor(() => received.length === whole.length); + expect(received).toEqual(whole); t.close(); }); + it("starts a YAS session with the raw preface and a framed HELLO", async () => { + const t = make(sockPath, { reconnect: false }); + const connection = new YasConnection(t); + const hello = connection.connect(); + hello.catch(() => {}); + await waitFor(() => clientBytes.byteLength > YAS_PREFACE.length + 4); + expect(Array.from(clientBytes.subarray(0, YAS_PREFACE.length))).toEqual( + Array.from(YAS_PREFACE), + ); + const frameLength = clientBytes.readUInt32LE(YAS_PREFACE.length); + await waitFor( + () => clientBytes.byteLength === YAS_PREFACE.length + 4 + frameLength, + ); + connection.close(); + }); + it("send() is a no-op before connected", async () => { const t = make(sockPath); // Intentionally do not call connect(). t.send(new Uint8Array([1])); await new Promise((r) => setTimeout(r, 20)); - expect(clientMessages).toHaveLength(0); + expect(clientBytes.byteLength).toBe(0); t.close(); }); diff --git a/js/core/src/desktopModel.ts b/js/core/src/desktopModel.ts index bcca0d0ce..c56b5e47d 100644 --- a/js/core/src/desktopModel.ts +++ b/js/core/src/desktopModel.ts @@ -1,4 +1,4 @@ -import { Notifier, type ReactiveStore } from "./reactive"; +import { Notifier, type ReactiveStore } from "./reactive.js"; export const DESKTOP_SUBSCRIBE_TRAY = 1 << 0; export const DESKTOP_SUBSCRIBE_NOTIFICATIONS = 1 << 1; diff --git a/js/core/src/downscale.ts b/js/core/src/downscale.ts index 2534a1465..9b5d9b01b 100644 --- a/js/core/src/downscale.ts +++ b/js/core/src/downscale.ts @@ -1,4 +1,4 @@ -import { surface2DContext } from "./surfaceColor"; +import { surface2DContext } from "./surfaceColor.js"; /** * Box-filtered minification for canvases the browser would otherwise shrink * on its own. diff --git a/js/core/src/fsModel.ts b/js/core/src/fsModel.ts index b7ddffa9e..f0700e187 100644 --- a/js/core/src/fsModel.ts +++ b/js/core/src/fsModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; /** Presentation flags shared by the native FS client and Workspace UI. */ export const FS_ENTRY_TYPE_MASK = 0b11; diff --git a/js/core/src/gitModel.ts b/js/core/src/gitModel.ts index ccd618cb7..18a19dbe2 100644 --- a/js/core/src/gitModel.ts +++ b/js/core/src/gitModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export const GIT_STATUS_OK = 0; export const GIT_STATUS_UNKNOWN_ID = 1; diff --git a/js/core/src/gl-renderer.ts b/js/core/src/gl-renderer.ts index 90503c417..b69b717e0 100644 --- a/js/core/src/gl-renderer.ts +++ b/js/core/src/gl-renderer.ts @@ -1,4 +1,4 @@ -import type { CellMetrics } from "./measure"; +import type { CellMetrics } from "./measure.js"; const MAX_BATCH_VERTS = 65532; const GLYPH_FLOATS_PER_VERT = 8; diff --git a/js/core/src/index.ts b/js/core/src/index.ts index 48e571c52..2bdfa9f6d 100644 --- a/js/core/src/index.ts +++ b/js/core/src/index.ts @@ -1,35 +1,39 @@ -export { YasWorkspace, consoleLogger, nullLogger } from "./YasWorkspace"; -export type { YasLogger, YasWorkspaceConnection } from "./YasWorkspace"; -export { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection"; -export { noteBrowserClipboardMayHaveChanged } from "./clipboardAuthority"; -export { YasNativeRelayTransport } from "./yas/nativeRelayTransport"; -export { YasActivityStore } from "./activity"; +export { YasWorkspace, consoleLogger, nullLogger } from "./YasWorkspace.js"; +export type { YasLogger, YasWorkspaceConnection } from "./YasWorkspace.js"; +export { YasNativeWorkspaceConnection } from "./YasNativeWorkspaceConnection.js"; +export { noteBrowserClipboardMayHaveChanged } from "./clipboardAuthority.js"; +export { YasNativeRelayTransport } from "./yas/nativeRelayTransport.js"; +export { YasActivityStore } from "./activity.js"; export type { YasActivity, YasActivityHandle, YasActivityUpdate, -} from "./activity"; +} from "./activity.js"; export { YAS_TERMINAL_CATALOG_SEARCH_SOURCE_TITLE as SEARCH_SOURCE_TITLE, YAS_TERMINAL_CATALOG_SEARCH_SOURCE_VISIBLE as SEARCH_SOURCE_VISIBLE, YAS_TERMINAL_CATALOG_SEARCH_SOURCE_SCROLLBACK as SEARCH_SOURCE_SCROLLBACK, YAS_STATUS_RESOURCE_EXHAUSTED, -} from "./yas/generated"; -export { YasResultError } from "./yas/wire"; +} from "./yas/generated.js"; +export { YasResultError } from "./yas/wire.js"; export type { AwaitSessionExitOptions, CreateSessionOptions, SurfaceTarget, -} from "./workspaceConnectionTypes"; +} from "./workspaceConnectionTypes.js"; -export type { YasWasmModule } from "./TerminalStore"; -export { AudioPlayer } from "./AudioPlayer"; +export type { YasWasmModule } from "./TerminalStore.js"; +export { AudioPlayer } from "./AudioPlayer.js"; export { releaseRecordingAudioSession, retainRecordingAudioSession, -} from "./audioSession"; -export { NumberRing, SurfaceFrameHistory, SurfaceStore } from "./SurfaceStore"; +} from "./audioSession.js"; +export { + NumberRing, + SurfaceFrameHistory, + SurfaceStore, +} from "./SurfaceStore.js"; export type { SurfaceFrameCallback, SurfaceEventCallback, @@ -41,36 +45,40 @@ export type { SurfaceCursorRect, SurfaceTextInputEvent, SurfaceTextInputState, -} from "./SurfaceStore"; +} from "./SurfaceStore.js"; export { estimateSourceToReceiveMs, sourceTimestampDelta, wrappingTimestampDelta, -} from "./SurfaceStore"; +} from "./SurfaceStore.js"; -export { clampZoom, driveSurfaceResize } from "./surfaceResize"; -export type { SurfaceResizeTarget, SurfaceZoom } from "./surfaceResize"; +export { clampZoom, driveSurfaceResize } from "./surfaceResize.js"; +export type { SurfaceResizeTarget, SurfaceZoom } from "./surfaceResize.js"; -export { measureCell, cssFontFamily } from "./measure"; -export type { CellMetrics } from "./measure"; +export { measureCell, cssFontFamily } from "./measure.js"; +export type { CellMetrics } from "./measure.js"; -export { assessUrl, escapeUrlForDisplay, openUrlSafely } from "./urlSecurity"; -export type { UrlAssessment, UrlVerdict, UrlReason } from "./urlSecurity"; +export { + assessUrl, + escapeUrlForDisplay, + openUrlSafely, +} from "./urlSecurity.js"; +export type { UrlAssessment, UrlVerdict, UrlReason } from "./urlSecurity.js"; -export { createShareTransport } from "./transports/webrtc-share"; +export { createShareTransport } from "./transports/webrtc-share.js"; /** YAS v1 session, Transfer, Relay, Font, and browser-edge clients. */ -export * from "./yas"; -export * from "./workspaceSessionKv"; +export * from "./yas/index.js"; +export * from "./workspaceSessionKv.js"; /** HTTP/1.1 over a relayed stream, for the preview service worker. */ -export * from "./http1"; +export * from "./http1.js"; /** Preview targets and the /x/ bootstrap prefix. */ -export * from "./preview"; +export * from "./preview.js"; /** Durable backend workspaces. */ -export * from "./workspaceSessions"; +export * from "./workspaceSessions.js"; /** Durable per-device workspace attachment ordering. */ -export * from "./workspaceSessionDevices"; +export * from "./workspaceSessionDevices.js"; // Product-model types and presentation helpers retained by the UI. export { @@ -85,7 +93,7 @@ export { TRAY_MENU_OK, TRAY_STATUS_NEEDS_ATTENTION, TRAY_STATUS_PASSIVE, -} from "./desktopModel"; +} from "./desktopModel.js"; export type { DesktopId, DesktopImage, @@ -94,7 +102,7 @@ export type { TrayItem, TrayMenu, TrayMenuNode, -} from "./desktopModel"; +} from "./desktopModel.js"; export { ACTIVE_CAMERA, ACTIVE_MICROPHONE, @@ -119,7 +127,7 @@ export { cameraCodecProbeReport, probeCameraCodecs, probeOpusMicrophone, -} from "./mediaModel"; +} from "./mediaModel.js"; export type { CameraCodecProbeOutcome, CameraQuality, @@ -130,7 +138,7 @@ export type { PortalChoiceValue, PortalRequest, ScreenCastState, -} from "./mediaModel"; +} from "./mediaModel.js"; export { FS_ENTRY_DIR, FS_ENTRY_FILE, @@ -140,13 +148,13 @@ export { FS_ENTRY_TYPE_MASK, FS_ENTRY_UNREADABLE, FS_ENTRY_UNSTABLE, -} from "./fsModel"; +} from "./fsModel.js"; export type { FsFileIndex, FsGrepFile, FsGrepOptions, FsGrepResult, -} from "./fsModel"; +} from "./fsModel.js"; export { GIT_CLOSED_CLIENT_REQUEST, GIT_CLOSED_CONNECTION_LOST, @@ -185,9 +193,9 @@ export { gitOidFromHex, gitOidHex, gitStatusText, -} from "./gitModel"; -export type { GitOid, GitPatchRecord, GitWorktreeRecord } from "./gitModel"; -export { GitStateMirror, GitStatusError } from "./gitModel"; +} from "./gitModel.js"; +export type { GitOid, GitPatchRecord, GitWorktreeRecord } from "./gitModel.js"; +export { GitStateMirror, GitStatusError } from "./gitModel.js"; export { LSP_COMPLETION_DEPRECATED, LSP_COMPLETION_PRESELECT, @@ -204,32 +212,36 @@ export { LSP_STATUS_OK, LSP_STATUS_WARMING, lspStatusText, -} from "./lspModel"; +} from "./lspModel.js"; export type { YasNativeChannelHandle as ChannelHandle, YasNativeChannelNamesWatch as ChannelNamesWatch, YasNativeChannelOpenOptions as ChannelOpenOptions, -} from "./yas/nativeChannelFacade"; -export type { NetOpenOptions, NetStream } from "./netModel"; -export { formatExtensionId, parseModuleDigest } from "./extensionModel"; +} from "./yas/nativeChannelFacade.js"; +export type { NetOpenOptions, NetStream } from "./netModel.js"; +export { formatExtensionId, parseModuleDigest } from "./extensionModel.js"; -export { DEFAULT_FONT, DEFAULT_FONT_SIZE, DEFAULT_TEXT_GAMMA } from "./types"; +export { + DEFAULT_FONT, + DEFAULT_FONT_SIZE, + DEFAULT_TEXT_GAMMA, +} from "./types.js"; export { CODEC_SUPPORT_H264, CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264_444, CODEC_SUPPORT_AV1_444, -} from "./surfaceModel"; -export type { YasTransportMessage } from "./types"; +} from "./surfaceModel.js"; +export type { YasTransportMessage } from "./types.js"; export { EXIT_STATUS_UNKNOWN, exitCodeFromStatus, formatExitStatus, -} from "./exit-status"; +} from "./exit-status.js"; -export { Notifier } from "./reactive"; -export type { ReactiveStore } from "./reactive"; +export { Notifier } from "./reactive.js"; +export type { ReactiveStore } from "./reactive.js"; export type { YasConnectionSnapshot, @@ -254,7 +266,7 @@ export type { TerminalId, TerminalPalette, TransportConfig, -} from "./types"; +} from "./types.js"; export { SURFACE_POINTER_DOWN, @@ -262,26 +274,26 @@ export { SURFACE_POINTER_MOVE, CLIENT_DISCONNECT_REASON_MAX_BYTES, clientDisconnectReasonByteLength, -} from "./input"; +} from "./input.js"; -export { PALETTES } from "./palettes"; +export { PALETTES } from "./palettes.js"; -export { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input"; -export { keyToBytes, ctrlCharToByte, encoder } from "./keyboard"; +export { MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE } from "./input.js"; +export { keyToBytes, ctrlCharToByte, encoder } from "./keyboard.js"; -export type { GlRenderer, RendererBackend } from "./gl-renderer"; -export { createWebGpuRenderer } from "./webgpu-renderer"; +export type { GlRenderer, RendererBackend } from "./gl-renderer.js"; +export { createWebGpuRenderer } from "./webgpu-renderer.js"; export { YasTerminalSurface, isIOS, terminalSurfaceForInput, -} from "./YasTerminalSurface"; +} from "./YasTerminalSurface.js"; export type { YasTerminalSurfaceOptions, YasTerminalSurfaceHandle, LinkHover, -} from "./YasTerminalSurface"; +} from "./YasTerminalSurface.js"; export { YAS_SURFACE_TEXT_INPUT_EVENT, @@ -293,12 +305,12 @@ export { getProbedCodecSupport, setAllowedCodecSupport, getMaxDecodeSize, -} from "./YasSurfaceCanvas"; +} from "./YasSurfaceCanvas.js"; export type { YasSurfaceTextInputEvent, YasSurfaceCanvasOptions, SurfaceTouchMode, -} from "./YasSurfaceCanvas"; +} from "./YasSurfaceCanvas.js"; export { LAYOUT_MAX_DEPTH, @@ -307,13 +319,13 @@ export { validateLayoutNode, validateWorkspaceLayout, sameLayoutTree, -} from "./layout/model"; +} from "./layout/model.js"; export type { LayoutNode, LayoutSplit, LayoutChild, LayoutLeaf, -} from "./layout/model"; +} from "./layout/model.js"; export { enumeratePanes, @@ -321,17 +333,17 @@ export { buildCandidateOrder, reconcileAssignments, adjustWeights, -} from "./layout/tree"; +} from "./layout/tree.js"; export type { WorkspaceLayout, LayoutPane, LayoutAssignments, -} from "./layout/tree"; +} from "./layout/tree.js"; -export { YasUplinkTransport } from "./transports/uplink"; -export { YasNoiseTransport } from "./transports/noise"; -export type { YasNoiseTransportOptions } from "./transports/noise"; +export { YasUplinkTransport } from "./transports/uplink.js"; +export { YasNoiseTransport } from "./transports/noise.js"; +export type { YasNoiseTransportOptions } from "./transports/noise.js"; export { generateUplinkKeyPair, uplinkPublicKey, -} from "./transports/uplink-crypto"; +} from "./transports/uplink-crypto.js"; diff --git a/js/core/src/keyboard.ts b/js/core/src/keyboard.ts index bb0e9290f..0f67db0a6 100644 --- a/js/core/src/keyboard.ts +++ b/js/core/src/keyboard.ts @@ -5,12 +5,12 @@ import { REPORT_ALL, REPORT_EVENTS, type TerminalKey, -} from "./keyboardProtocol"; +} from "./keyboardProtocol.js"; export { encodeTerminalText, REPORT_ALL, REPORT_EVENTS, -} from "./keyboardProtocol"; +} from "./keyboardProtocol.js"; export const encoder = new TextEncoder(); export function ctrlCharToByte(char: string): Uint8Array | null { diff --git a/js/core/src/layout/index.ts b/js/core/src/layout/index.ts index 74dd64b85..ebb0813a2 100644 --- a/js/core/src/layout/index.ts +++ b/js/core/src/layout/index.ts @@ -5,7 +5,7 @@ export { sameLayoutTree, LAYOUT_MAX_DEPTH, LAYOUT_MAX_PANES, -} from "./model"; +} from "./model.js"; export type { LayoutNode, LayoutSplit, @@ -13,7 +13,7 @@ export type { LayoutLeaf, LayoutRect, LayoutDirection, -} from "./model"; +} from "./model.js"; export { enumeratePanes, @@ -41,11 +41,11 @@ export { isWebAssignment, parseWebAssignment, isContentAssignment, -} from "./tree"; +} from "./tree.js"; export type { WorkspaceLayout, LayoutPane, LayoutAssignments, TileAssignment, DiffSide, -} from "./tree"; +} from "./tree.js"; diff --git a/js/core/src/layout/tree.ts b/js/core/src/layout/tree.ts index e076c9056..0d75d7a6e 100644 --- a/js/core/src/layout/tree.ts +++ b/js/core/src/layout/tree.ts @@ -1,13 +1,13 @@ -import type { SurfaceId } from "../types"; +import type { SurfaceId } from "../types.js"; import type { LayoutNode, LayoutSplit, LayoutChild, LayoutLeaf, LayoutRect, -} from "./model"; +} from "./model.js"; -export type { WorkspaceLayout } from "./model"; +export type { WorkspaceLayout } from "./model.js"; export interface LayoutPane { id: string; diff --git a/js/core/src/lspModel.ts b/js/core/src/lspModel.ts index 6b1be1b8b..ede5fcc83 100644 --- a/js/core/src/lspModel.ts +++ b/js/core/src/lspModel.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export const LSP_STATUS_OK = 0; export const LSP_STATUS_UNKNOWN_ID = 1; diff --git a/js/core/src/mediaModel.ts b/js/core/src/mediaModel.ts index a552bca27..f9319d771 100644 --- a/js/core/src/mediaModel.ts +++ b/js/core/src/mediaModel.ts @@ -1,10 +1,10 @@ import { releaseRecordingAudioSession, retainRecordingAudioSession, -} from "./audioSession"; -import { Notifier, type ReactiveStore } from "./reactive"; -import type { SurfaceId } from "./types"; -import { av1LevelString } from "./videoCodec"; +} from "./audioSession.js"; +import { Notifier, type ReactiveStore } from "./reactive.js"; +import type { SurfaceId } from "./types.js"; +import { av1LevelString } from "./videoCodec.js"; export const RUNTIME_PIPEWIRE = 1 << 0; export const RUNTIME_MICROPHONE = 1 << 1; diff --git a/js/core/src/node-wasm.ts b/js/core/src/node-wasm.ts index cac30ed43..0d3294f70 100644 --- a/js/core/src/node-wasm.ts +++ b/js/core/src/node-wasm.ts @@ -1,9 +1,7 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; -import init from "@yas-run/browser"; - -import type { YasWasmModule } from "./TerminalStore"; +import type { YasWasmModule } from "./TerminalStore.js"; /** * Initialise the `@yas-run/browser` WASM module in a non-browser runtime @@ -30,7 +28,7 @@ import type { YasWasmModule } from "./TerminalStore"; */ export async function loadYasWasm(wasmPath?: string): Promise { const mod = (await import("@yas-run/browser")) as unknown as YasWasmModule & { - default?: unknown; + default?: (options: { module_or_path: unknown }) => Promise; }; // A self-initializing build (`--target nodejs`/`bundler`) has already @@ -45,8 +43,6 @@ export async function loadYasWasm(wasmPath?: string): Promise { ? fileURLToPath(location) : location; const bytes = await readFile(path); - await init({ - module_or_path: bytes as unknown as Parameters[0], - }); + await mod.default({ module_or_path: bytes }); return mod; } diff --git a/js/core/src/node.ts b/js/core/src/node.ts index 3e875ea45..40de8f400 100644 --- a/js/core/src/node.ts +++ b/js/core/src/node.ts @@ -10,9 +10,9 @@ * `@yas-run/core` / `@yas-run/core/transports` instead. */ -export { NodeUnixSocketTransport } from "./transports/unix"; -export { BunUnixSocketTransport } from "./transports/unix-bun"; -export { DenoUnixSocketTransport } from "./transports/unix-deno"; -export type { UnixSocketTransportOptions } from "./transports/unix-base"; +export { NodeUnixSocketTransport } from "./transports/unix.js"; +export { BunUnixSocketTransport } from "./transports/unix-bun.js"; +export { DenoUnixSocketTransport } from "./transports/unix-deno.js"; +export type { UnixSocketTransportOptions } from "./transports/unix-base.js"; -export { loadYasWasm } from "./node-wasm"; +export { loadYasWasm } from "./node-wasm.js"; diff --git a/js/core/src/palettes.ts b/js/core/src/palettes.ts index 81457658a..3bd3731b7 100644 --- a/js/core/src/palettes.ts +++ b/js/core/src/palettes.ts @@ -1,4 +1,4 @@ -import type { TerminalPalette } from "./types"; +import type { TerminalPalette } from "./types.js"; export const PALETTES: TerminalPalette[] = [ { diff --git a/js/core/src/surfaceColor.ts b/js/core/src/surfaceColor.ts index 914c50dec..dda68cec1 100644 --- a/js/core/src/surfaceColor.ts +++ b/js/core/src/surfaceColor.ts @@ -1,6 +1,6 @@ -import { SURFACE_HDR_PROBE } from "./surfaceHdrProbe"; -import type { YasSurfaceColorSpace } from "./yas/packed"; -import type { YasExtension } from "./yas/wire"; +import { SURFACE_HDR_PROBE } from "./surfaceHdrProbe.js"; +import type { YasSurfaceColorSpace } from "./yas/packed.js"; +import type { YasExtension } from "./yas/wire.js"; import { YAS_SURFACE_COLOR_CAP_DISPLAY_P3, YAS_SURFACE_COLOR_CAP_HDR10_AV1, @@ -8,7 +8,7 @@ import { YAS_SURFACE_COLOR_CAP_AV1_444, YAS_SURFACE_COLOR_CAP_H264_444, YAS_SURFACE_VIEW_COLOR_CAPABILITIES_EXTENSION, -} from "./yas/generated"; +} from "./yas/generated.js"; export const SDR_COLOR: VideoColorSpaceInit = { primaries: "bt709", diff --git a/js/core/src/transports/browser.ts b/js/core/src/transports/browser.ts index b878e5f39..392d69b36 100644 --- a/js/core/src/transports/browser.ts +++ b/js/core/src/transports/browser.ts @@ -1,14 +1,14 @@ -export { createWebRtcDataChannelTransport } from "./webrtc"; -export type { WebRtcDataChannelTransportOptions } from "./webrtc"; +export { createWebRtcDataChannelTransport } from "./webrtc.js"; +export type { WebRtcDataChannelTransportOptions } from "./webrtc.js"; -export { createShareTransport } from "./webrtc-share"; +export { createShareTransport } from "./webrtc-share.js"; -export { YasWebTransportTransport } from "./webtransport"; -export type { YasWebTransportOptions } from "./webtransport"; +export { YasWebTransportTransport } from "./webtransport.js"; +export type { YasWebTransportOptions } from "./webtransport.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; -export { YasUplinkTransport } from "./uplink"; -export { YasNoiseTransport } from "./noise"; -export type { YasNoiseTransportOptions } from "./noise"; -export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto"; +export { YasUplinkTransport } from "./uplink.js"; +export { YasNoiseTransport } from "./noise.js"; +export type { YasNoiseTransportOptions } from "./noise.js"; +export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto.js"; diff --git a/js/core/src/transports/index.ts b/js/core/src/transports/index.ts index 922ae783d..9411a4f1b 100644 --- a/js/core/src/transports/index.ts +++ b/js/core/src/transports/index.ts @@ -1,17 +1,17 @@ -export { createWebRtcDataChannelTransport } from "./webrtc"; -export type { WebRtcDataChannelTransportOptions } from "./webrtc"; +export { createWebRtcDataChannelTransport } from "./webrtc.js"; +export type { WebRtcDataChannelTransportOptions } from "./webrtc.js"; -export { createShareTransport } from "./webrtc-share"; +export { createShareTransport } from "./webrtc-share.js"; -export { YasWebTransportTransport } from "./webtransport"; -export type { YasWebTransportOptions } from "./webtransport"; +export { YasWebTransportTransport } from "./webtransport.js"; +export type { YasWebTransportOptions } from "./webtransport.js"; -export { NodeUnixSocketTransport } from "./unix"; -export { BunUnixSocketTransport } from "./unix-bun"; -export { DenoUnixSocketTransport } from "./unix-deno"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export { NodeUnixSocketTransport } from "./unix.js"; +export { BunUnixSocketTransport } from "./unix-bun.js"; +export { DenoUnixSocketTransport } from "./unix-deno.js"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; -export { YasUplinkTransport } from "./uplink"; -export { YasNoiseTransport } from "./noise"; -export type { YasNoiseTransportOptions } from "./noise"; -export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto"; +export { YasUplinkTransport } from "./uplink.js"; +export { YasNoiseTransport } from "./noise.js"; +export type { YasNoiseTransportOptions } from "./noise.js"; +export { generateUplinkKeyPair, uplinkPublicKey } from "./uplink-crypto.js"; diff --git a/js/core/src/transports/noise.ts b/js/core/src/transports/noise.ts index 750c0ecc4..dab69b4ec 100644 --- a/js/core/src/transports/noise.ts +++ b/js/core/src/transports/noise.ts @@ -4,8 +4,8 @@ import type { YasTransportEventMap, YasTransportMessage, YasTransportOptions, -} from "../types"; -import { YAS_EVENT_HEADER_BYTES } from "../yas/generated"; +} from "../types.js"; +import { YAS_EVENT_HEADER_BYTES } from "../yas/generated.js"; import { type Bytes, NoiseCipher, @@ -19,7 +19,7 @@ import { equal, importIdentity, publicKey, -} from "./uplink-crypto"; +} from "./uplink-crypto.js"; const QUEUE_LIMIT = 4 * 1024 * 1024; const DATAGRAM_IN_FLIGHT = 64; diff --git a/js/core/src/transports/unix-base.ts b/js/core/src/transports/unix-base.ts index dbfe61300..7894f2551 100644 --- a/js/core/src/transports/unix-base.ts +++ b/js/core/src/transports/unix-base.ts @@ -2,14 +2,15 @@ import type { YasTransport, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; export interface UnixSocketTransportOptions extends YasTransportOptions {} /** - * Shared implementation of the yas local-IPC framing protocol - * (little-endian `u32` length prefix, see `crates/server/src/lib.rs` - * `read_frame`/`write_frame`). + * Shared implementation of the YAS local-IPC byte stream. The socket + * carries the raw preface followed by `u32` little-endian length-prefixed + * frames (see `docs/design/yas.md`, "Transport framing"); the transport + * passes bytes through unchanged and {@link YasConnection} does the framing. * * Concrete subclasses plug in a socket backend (Node's `net` module, * Bun's `Bun.connect`, ...) via {@link openRawSocket}. They receive @@ -17,6 +18,7 @@ export interface UnixSocketTransportOptions extends YasTransportOptions {} * via {@link onRawConnect}, {@link onRawClose} and {@link onRawError}. */ export abstract class AbstractUnixSocketTransport implements YasTransport { + readonly yasFraming = "stream" as const; readonly maxDatagramSize = 0; private _status: ConnectionStatus = "disconnected"; private reconnectTimer: ReturnType | null = null; @@ -29,8 +31,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { authRejected = false; lastError: string | null = null; - /** Incoming byte accumulator. */ - private recvBuf = new Uint8Array(0); /** Sentinel that tracks which socket attempt events belong to. */ protected currentAttempt: symbol | null = null; @@ -57,13 +57,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { send(data: Uint8Array): void { if (this._status !== "connected") return; - const header = new Uint8Array(4); - const len = data.byteLength; - header[0] = len & 0xff; - header[1] = (len >> 8) & 0xff; - header[2] = (len >> 16) & 0xff; - header[3] = (len >>> 24) & 0xff; - this.writeRaw(header); this.writeRaw(data); } @@ -73,7 +66,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.setStatus("closed"); } @@ -83,7 +75,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.currentDelay = this.initialDelay; this.setStatus("disconnected"); } @@ -94,7 +85,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { this.clearConnectTimer(); this.currentAttempt = null; this.destroyRawSocket(); - this.recvBuf = new Uint8Array(0); this.currentDelay = this.initialDelay; this.setStatus("disconnected"); this.connect(); @@ -159,7 +149,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { return; } this.setStatus("connecting"); - this.recvBuf = new Uint8Array(0); const attempt = Symbol("unix-attempt"); this.currentAttempt = attempt; @@ -211,25 +200,9 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { protected ingestChunk(attempt: symbol, chunk: Uint8Array): void { if (this.currentAttempt !== attempt || this.disposed) return; if (chunk.byteLength === 0) return; - if (this.recvBuf.byteLength === 0) { - const copy = new Uint8Array(chunk.byteLength); - copy.set(chunk); - this.recvBuf = copy; - } else { - const merged = new Uint8Array(this.recvBuf.byteLength + chunk.byteLength); - merged.set(this.recvBuf, 0); - merged.set(chunk, this.recvBuf.byteLength); - this.recvBuf = merged; - } - while (this.recvBuf.byteLength >= 4) { - const b = this.recvBuf; - const len = b[0]! | (b[1]! << 8) | (b[2]! << 16) | (b[3]! * 0x01000000); - if (this.recvBuf.byteLength < 4 + len) break; - const ab = new ArrayBuffer(len); - new Uint8Array(ab).set(this.recvBuf.subarray(4, 4 + len)); - this.recvBuf = this.recvBuf.subarray(4 + len); - for (const l of this.messageListeners) l(ab); - } + const ab = new ArrayBuffer(chunk.byteLength); + new Uint8Array(ab).set(chunk); + for (const l of this.messageListeners) l(ab); } protected onRawError(attempt: symbol, message: string): void { @@ -242,7 +215,6 @@ export abstract class AbstractUnixSocketTransport implements YasTransport { if (this.currentAttempt !== attempt || this.disposed) return; this.clearConnectTimer(); this.currentAttempt = null; - this.recvBuf = new Uint8Array(0); this.setStatus("disconnected"); this.scheduleReconnect(); } diff --git a/js/core/src/transports/unix-bun.ts b/js/core/src/transports/unix-bun.ts index 385b769a3..0d80637a8 100644 --- a/js/core/src/transports/unix-bun.ts +++ b/js/core/src/transports/unix-bun.ts @@ -3,9 +3,9 @@ import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; type BunSocket = import("bun").Socket<{ attempt: symbol }>; @@ -18,6 +18,8 @@ type BunSocket = import("bun").Socket<{ attempt: symbol }>; */ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { private socket: BunSocket | null = null; + /** Bytes Bun's non-buffering `write` did not accept yet, flushed on drain. */ + private pending: Uint8Array[] = []; constructor(path: string, options?: UnixSocketTransportOptions) { super(path, options); @@ -40,9 +42,15 @@ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { this.onRawConnect(attempt); }, data: (_sock, chunk: Uint8Array) => this.ingestChunk(attempt, chunk), + drain: (sock) => { + if (this.socket === sock) this.flush(); + }, error: (_sock, err: Error) => this.onRawError(attempt, err.message), close: (sock) => { - if (this.socket === sock) this.socket = null; + if (this.socket === sock) { + this.socket = null; + this.pending = []; + } this.onRawClose(attempt); }, }, @@ -54,10 +62,26 @@ export class BunUnixSocketTransport extends AbstractUnixSocketTransport { } protected writeRaw(data: Uint8Array): void { - this.socket?.write(data); + if (!this.socket) return; + this.pending.push(data); + if (this.pending.length === 1) this.flush(); + } + + private flush(): void { + const socket = this.socket; + while (socket && this.pending.length > 0) { + const head = this.pending[0]!; + const written = socket.write(head); + if (written < head.byteLength) { + this.pending[0] = head.subarray(Math.max(written, 0)); + return; + } + this.pending.shift(); + } } protected destroyRawSocket(): void { + this.pending = []; const socket = this.socket; if (!socket) return; this.socket = null; diff --git a/js/core/src/transports/unix-deno.ts b/js/core/src/transports/unix-deno.ts index 9c25e6ffc..8ff02ef10 100644 --- a/js/core/src/transports/unix-deno.ts +++ b/js/core/src/transports/unix-deno.ts @@ -1,9 +1,9 @@ import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; // Minimal structural typing for the subset of the Deno global we use, so // this file compiles under Node/Bun tsconfigs without @types/deno. diff --git a/js/core/src/transports/unix.ts b/js/core/src/transports/unix.ts index 9d72df0d6..8450fb1e4 100644 --- a/js/core/src/transports/unix.ts +++ b/js/core/src/transports/unix.ts @@ -3,9 +3,9 @@ import { connect as netConnect, type Socket } from "node:net"; import { AbstractUnixSocketTransport, type UnixSocketTransportOptions, -} from "./unix-base"; +} from "./unix-base.js"; -export type { UnixSocketTransportOptions } from "./unix-base"; +export type { UnixSocketTransportOptions } from "./unix-base.js"; /** * Node.js unix-domain-socket transport for `yas server` IPC. diff --git a/js/core/src/transports/uplink.ts b/js/core/src/transports/uplink.ts index b625c79d4..93b17000c 100644 --- a/js/core/src/transports/uplink.ts +++ b/js/core/src/transports/uplink.ts @@ -1,6 +1,6 @@ -import type { YasTransportOptions } from "../types"; -import { UplinkEvents, YasNoiseTransport } from "./noise"; -import { decodeKey, publicKey } from "./uplink-crypto"; +import type { YasTransportOptions } from "../types.js"; +import { UplinkEvents, YasNoiseTransport } from "./noise.js"; +import { decodeKey, publicKey } from "./uplink-crypto.js"; interface UplinkTarget { attach: string; diff --git a/js/core/src/transports/webrtc-share.ts b/js/core/src/transports/webrtc-share.ts index e9cca2520..3bbb0c603 100644 --- a/js/core/src/transports/webrtc-share.ts +++ b/js/core/src/transports/webrtc-share.ts @@ -8,14 +8,14 @@ */ import nacl from "tweetnacl"; -import { createWebRtcDataChannelTransport } from "./webrtc"; +import { createWebRtcDataChannelTransport } from "./webrtc.js"; import { noopDebug, type YasDebug, type YasTransport, type YasTransportMessage, type ConnectionStatus, -} from "../types"; +} from "../types.js"; const PBKDF2_ROUNDS = 100_000; // The producer gathers its non-trickle candidates before answering (up to diff --git a/js/core/src/transports/webrtc.ts b/js/core/src/transports/webrtc.ts index d99cbdb0d..4abc43e2f 100644 --- a/js/core/src/transports/webrtc.ts +++ b/js/core/src/transports/webrtc.ts @@ -2,7 +2,7 @@ import type { YasTransport, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; export interface WebRtcDataChannelTransportOptions extends YasTransportOptions { /** Data channel label. Default: the canonical YAS v1 selector. */ diff --git a/js/core/src/transports/webtransport.ts b/js/core/src/transports/webtransport.ts index 2e69eea3b..0e841084d 100644 --- a/js/core/src/transports/webtransport.ts +++ b/js/core/src/transports/webtransport.ts @@ -4,7 +4,7 @@ import type { YasTransportEventMap, YasTransportMessage, YasTransportOptions, -} from "../types"; +} from "../types.js"; const YAS_TRANSPORT_DATAGRAM_HARD_MAX = 65_536; const AUTH_MAX_BYTES = 4_096; diff --git a/js/core/src/webgpu-renderer.ts b/js/core/src/webgpu-renderer.ts index 9b1ade754..278d9ddfd 100644 --- a/js/core/src/webgpu-renderer.ts +++ b/js/core/src/webgpu-renderer.ts @@ -1,5 +1,5 @@ -import type { CellMetrics } from "./measure"; -import { rgbLuma, type GlRenderer } from "./gl-renderer"; +import type { CellMetrics } from "./measure.js"; +import { rgbLuma, type GlRenderer } from "./gl-renderer.js"; // --------------------------------------------------------------------------- // WGSL shaders diff --git a/js/core/src/workspaceConnectionTypes.ts b/js/core/src/workspaceConnectionTypes.ts index e8f9b76cc..f97ace50e 100644 --- a/js/core/src/workspaceConnectionTypes.ts +++ b/js/core/src/workspaceConnectionTypes.ts @@ -1,4 +1,4 @@ -import type { SessionId } from "./types"; +import type { SessionId } from "./types.js"; export interface CreateSessionOptions { rows: number; diff --git a/js/core/src/workspaceSessionDevices.ts b/js/core/src/workspaceSessionDevices.ts index c4383cee4..2ee6edd63 100644 --- a/js/core/src/workspaceSessionDevices.ts +++ b/js/core/src/workspaceSessionDevices.ts @@ -9,7 +9,7 @@ import { type WorkspaceSessionStoreStatus, isWorkspaceSessionId, workspaceSessionKey, -} from "./workspaceSessions"; +} from "./workspaceSessions.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, @@ -17,9 +17,9 @@ import { type WorkspaceSessionHash, type WorkspaceSessionKvEntry, type WorkspaceSessionKvWatch, -} from "./workspaceSessionKv"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { YasConnection } from "./yas/session"; +} from "./workspaceSessionKv.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { YasConnection } from "./yas/session.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/workspaceSessions.ts b/js/core/src/workspaceSessions.ts index 723c30858..5100dd43b 100644 --- a/js/core/src/workspaceSessions.ts +++ b/js/core/src/workspaceSessions.ts @@ -6,21 +6,21 @@ import { validateWorkspaceLayout, type LayoutRect, type WorkspaceLayout, -} from "./layout/model"; +} from "./layout/model.js"; import type { WorkspaceSessionHash, WorkspaceSessionKv, WorkspaceSessionKvEntry, WorkspaceSessionKvWatch, WorkspaceSessionOwnedKv, -} from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, workspaceSessionHashesEqual, -} from "./workspaceSessionKv"; -import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv"; -import type { YasConnection } from "./yas/session"; +} from "./workspaceSessionKv.js"; +import { YasNativeWorkspaceKv } from "./yas/nativeWorkspaceKv.js"; +import type { YasConnection } from "./yas/session.js"; export type { WorkspaceSessionHash, @@ -32,8 +32,8 @@ export type { WorkspaceSessionKvWatch, WorkspaceSessionKvWatchOptions, WorkspaceSessionOwnedKv, -} from "./workspaceSessionKv"; -export { WorkspaceSessionKvConflictError } from "./workspaceSessionKv"; +} from "./workspaceSessionKv.js"; +export { WorkspaceSessionKvConflictError } from "./workspaceSessionKv.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/channel.ts b/js/core/src/yas/channel.ts index 0011272cb..9eb0a2013 100644 --- a/js/core/src/yas/channel.ts +++ b/js/core/src/yas/channel.ts @@ -21,8 +21,8 @@ import { YAS_FAMILY_CHANNEL, YAS_FAMILY_TRANSFER, YAS_TRANSFER_RESET, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -38,7 +38,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -48,7 +48,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YAS_STATUS_CANCELLED, YAS_STATUS_RESOURCE_EXHAUSTED, @@ -61,7 +61,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_CHANNEL_ACCEPT, @@ -75,7 +75,7 @@ export { YAS_CHANNEL_UNWATCH, YAS_CHANNEL_VERSION, YAS_CHANNEL_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasChannelListenerRecord { listenerHandle: bigint; diff --git a/js/core/src/yas/client.ts b/js/core/src/yas/client.ts index 752b7f561..b429e37f3 100644 --- a/js/core/src/yas/client.ts +++ b/js/core/src/yas/client.ts @@ -19,8 +19,8 @@ import { YAS_CLIENT_VERSION, YAS_CLIENT_WATCH, YAS_FAMILY_CLIENT, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -38,7 +38,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YasCursor, YasProtocolError, @@ -47,7 +47,7 @@ import { decodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_CLIENT_ACTIVE_SUBSCRIPTIONS_EXTENSION, @@ -68,7 +68,7 @@ export { YAS_CLIENT_VERSION, YAS_CLIENT_WATCH, YAS_FAMILY_CLIENT, -} from "./generated"; +} from "./generated.js"; export type YasClientOrigin = | { diff --git a/js/core/src/yas/core.ts b/js/core/src/yas/core.ts index d5a8d9b80..d2f135644 100644 --- a/js/core/src/yas/core.ts +++ b/js/core/src/yas/core.ts @@ -13,7 +13,7 @@ import { encodeExtensions, validateExtensionBody, type YasExtension, -} from "./wire"; +} from "./wire.js"; import { YAS_CORE_CANCEL, YAS_CORE_CLIENT_UPDATE, @@ -49,7 +49,7 @@ import { YAS_OPERATION_DIRECTION_MASKS, YAS_RELAY_LIMIT_MAX_LINKS_PER_SESSION, YAS_RELAY_LIMIT_MAX_PENDING_CONNECTS, -} from "./generated"; +} from "./generated.js"; export { YAS_CORE_CANCEL, @@ -66,7 +66,7 @@ export { YAS_FAMILY_FONT, YAS_FAMILY_RELAY, YAS_FAMILY_TRANSFER, -} from "./generated"; +} from "./generated.js"; export const YAS_RUNTIME_AVAILABLE = YAS_CORE_RUNTIME_AVAILABLE; export const YAS_RUNTIME_DEGRADED = YAS_CORE_RUNTIME_DEGRADED; diff --git a/js/core/src/yas/datagram.ts b/js/core/src/yas/datagram.ts index 6eab5b1ac..9b4f7bbf0 100644 --- a/js/core/src/yas/datagram.ts +++ b/js/core/src/yas/datagram.ts @@ -15,11 +15,11 @@ import { YAS_SURFACE_FRAME_DISCARDABLE, YAS_SURFACE_FRAME_END_OF_STREAM, YAS_SURFACE_FRAME_KEYFRAME, -} from "./generated"; -import { decodeMediaFrame, type YasMediaFrame } from "./media"; -import { decodeNetDatagram, type YasNetDatagram } from "./net"; -import { decodeSurfaceFrame, type YasSurfaceFrame } from "./surface"; -import { YasProtocolError, yasOperationPolicy, type YasFrame } from "./wire"; +} from "./generated.js"; +import { decodeMediaFrame, type YasMediaFrame } from "./media.js"; +import { decodeNetDatagram, type YasNetDatagram } from "./net.js"; +import { decodeSurfaceFrame, type YasSurfaceFrame } from "./surface.js"; +import { YasProtocolError, yasOperationPolicy, type YasFrame } from "./wire.js"; export type YasValidatedDatagram = | { diff --git a/js/core/src/yas/defaults.ts b/js/core/src/yas/defaults.ts index d306870ac..be02d2f18 100644 --- a/js/core/src/yas/defaults.ts +++ b/js/core/src/yas/defaults.ts @@ -39,8 +39,8 @@ import { YAS_FAMILY_SURFACE, YAS_FAMILY_TERMINAL, YAS_FAMILY_TRANSFER, -} from "./generated"; -import type { YasConnectionOptions } from "./session"; +} from "./generated.js"; +import type { YasConnectionOptions } from "./session.js"; /** * Browser receive inventory. Surface views keep a 16-frame window so a 120 Hz diff --git a/js/core/src/yas/desktop.ts b/js/core/src/yas/desktop.ts index 1f9a9ae0b..4f74efb00 100644 --- a/js/core/src/yas/desktop.ts +++ b/js/core/src/yas/desktop.ts @@ -1,7 +1,7 @@ /** YAS Desktop family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -27,7 +27,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -36,7 +36,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; const desktopNotificationExtensionTags: ReadonlySet = new Set([ g.YAS_DESKTOP_NOTIFICATION_IMAGE_HASH_EXTENSION, @@ -104,7 +104,7 @@ export { YAS_DESKTOP_WATCH_NOTIFICATIONS, YAS_DESKTOP_WATCH_TRAY, YAS_FAMILY_DESKTOP, -} from "./generated"; +} from "./generated.js"; export interface YasDesktopMenuNode { nodeHandle: bigint; diff --git a/js/core/src/yas/edge.ts b/js/core/src/yas/edge.ts index 0225525e9..ce808cf46 100644 --- a/js/core/src/yas/edge.ts +++ b/js/core/src/yas/edge.ts @@ -3,13 +3,13 @@ import type { YasTransportMessage, YasTransportOptions, ConnectionStatus, -} from "../types"; +} from "../types.js"; import { YasConnection, type YasConnectionOptions, type YasTransport, -} from "./session"; -import { YAS_WEBSOCKET_SUBPROTOCOL } from "./generated"; +} from "./session.js"; +import { YAS_WEBSOCKET_SUBPROTOCOL } from "./generated.js"; /** Exact browser contract for `/edge`: authenticate, then one YAS frame/message. */ export class YasEdgeWebSocketTransport implements YasTransport { diff --git a/js/core/src/yas/env.ts b/js/core/src/yas/env.ts index 0aceebc73..84376d918 100644 --- a/js/core/src/yas/env.ts +++ b/js/core/src/yas/env.ts @@ -14,8 +14,8 @@ import { YAS_ENV_VERSION, YAS_FAMILY_ENV, YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -23,7 +23,7 @@ import { encodeTransferDescriptor, transfersFor, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -31,7 +31,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_ENV_DELIVERY_INLINE, @@ -45,7 +45,7 @@ export { YAS_ENV_MAX_VALUE_BYTES, YAS_ENV_SNAPSHOT_CONTENT_KIND, YAS_ENV_VERSION, -} from "./generated"; +} from "./generated.js"; export interface YasEnvEntry { key: Uint8Array; diff --git a/js/core/src/yas/events.ts b/js/core/src/yas/events.ts index d5a2a6d04..024446e3d 100644 --- a/js/core/src/yas/events.ts +++ b/js/core/src/yas/events.ts @@ -1,7 +1,7 @@ /** YAS process-wide binary event-journal family v1. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -10,7 +10,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YAS_MAX_DECODED_FRAME, YasCursor, @@ -22,7 +22,7 @@ import { encodeExtensions, equalBytes, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_EVENTS_DUMP, @@ -38,7 +38,7 @@ export { YAS_EVENTS_STREAM_STOPPED, YAS_EVENTS_VERSION, YAS_FAMILY_EVENTS, -} from "./generated"; +} from "./generated.js"; export interface YasEventsConfig { revision: bigint; diff --git a/js/core/src/yas/extension.ts b/js/core/src/yas/extension.ts index 39d35966f..e9a195cbf 100644 --- a/js/core/src/yas/extension.ts +++ b/js/core/src/yas/extension.ts @@ -1,7 +1,7 @@ /** YAS extension-supervisor family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -17,7 +17,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -29,7 +29,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -39,7 +39,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_EXTENSION_ATTEMPT_CONTEXT, @@ -55,7 +55,7 @@ export { YAS_EXTENSION_VERSION, YAS_EXTENSION_WATCH, YAS_FAMILY_EXTENSION, -} from "./generated"; +} from "./generated.js"; export interface YasExtensionRuntimeLimits { memoryBytes: bigint; diff --git a/js/core/src/yas/font.ts b/js/core/src/yas/font.ts index 620748dd7..4daac4b50 100644 --- a/js/core/src/yas/font.ts +++ b/js/core/src/yas/font.ts @@ -1,4 +1,4 @@ -import { YAS_FAMILY_FONT } from "./core"; +import { YAS_FAMILY_FONT } from "./core.js"; import { YAS_FONT_DESCRIBE, YAS_FONT_DELIVERY_INLINE, @@ -39,8 +39,8 @@ import { YAS_FONT_UNWATCH, YAS_FONT_VERSION, YAS_FONT_WATCH, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -56,13 +56,13 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, decodeTransferDescriptor, transfersFor, -} from "./transfer"; +} from "./transfer.js"; import { equalBytes, YasCursor, @@ -72,7 +72,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FONT_DESCRIBE, @@ -94,7 +94,7 @@ export { YAS_FONT_UNWATCH, YAS_FONT_VERSION, YAS_FONT_WATCH, -} from "./generated"; +} from "./generated.js"; export const YAS_FONT_FORMAT_TRUETYPE = YAS_FONT_FORMAT_SFNT_TRUETYPE; export const YAS_FONT_FORMAT_CFF = YAS_FONT_FORMAT_SFNT_CFF; diff --git a/js/core/src/yas/fs.ts b/js/core/src/yas/fs.ts index 8c12cf26c..29350a690 100644 --- a/js/core/src/yas/fs.ts +++ b/js/core/src/yas/fs.ts @@ -1,7 +1,7 @@ /** YAS FS family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -20,7 +20,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -35,7 +35,7 @@ import { type YasInlineOrTransfer, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -44,7 +44,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_FS, @@ -63,7 +63,7 @@ export { YAS_FS_UNWATCH, YAS_FS_VERSION, YAS_FS_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasFsPath { components: readonly Uint8Array[]; diff --git a/js/core/src/yas/git.ts b/js/core/src/yas/git.ts index 1043adf86..673ff4fe7 100644 --- a/js/core/src/yas/git.ts +++ b/js/core/src/yas/git.ts @@ -1,8 +1,8 @@ /** YAS Git family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; -import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; +import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -21,7 +21,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -31,7 +31,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -41,7 +41,7 @@ import { encodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_GIT, @@ -60,7 +60,7 @@ export { YAS_GIT_VERSION, YAS_GIT_WATCH, YAS_GIT_WATCH_QUERY, -} from "./generated"; +} from "./generated.js"; export interface YasGitObjectId { algorithm: number; diff --git a/js/core/src/yas/index.ts b/js/core/src/yas/index.ts index bdc30f2d6..89d1ff804 100644 --- a/js/core/src/yas/index.ts +++ b/js/core/src/yas/index.ts @@ -1,36 +1,36 @@ -export * from "./generated"; -export * from "./wire"; -export * from "./core"; -export * from "./env"; -export * from "./kv"; -export * from "./session"; -export * from "./transfer"; -export * from "./state"; -export * from "./relay"; -export * from "./font"; -export * from "./terminal"; -export * from "./recording"; -export * from "./client"; -export * from "./channel"; -export * from "./surface"; -export * from "./selection"; -export * from "./desktop"; -export * from "./media"; -export * from "./process"; -export * from "./net"; -export * from "./datagram"; -export * from "./fs"; -export * from "./events"; -export * from "./lsp"; -export * from "./git"; -export * from "./extension"; -export * from "./packed"; -export * from "./defaults"; -export * from "./edge"; -export * from "./nativeProductFamilies"; -export * from "./nativeWorkspaceFs"; -export * from "./nativeWorkspaceGit"; -export * from "./nativeWorkspaceLsp"; -export * from "./nativeWorkspaceKv"; -export * from "./nativeChannelFacade"; -export * from "./nativeExtensionFacade"; +export * from "./generated.js"; +export * from "./wire.js"; +export * from "./core.js"; +export * from "./env.js"; +export * from "./kv.js"; +export * from "./session.js"; +export * from "./transfer.js"; +export * from "./state.js"; +export * from "./relay.js"; +export * from "./font.js"; +export * from "./terminal.js"; +export * from "./recording.js"; +export * from "./client.js"; +export * from "./channel.js"; +export * from "./surface.js"; +export * from "./selection.js"; +export * from "./desktop.js"; +export * from "./media.js"; +export * from "./process.js"; +export * from "./net.js"; +export * from "./datagram.js"; +export * from "./fs.js"; +export * from "./events.js"; +export * from "./lsp.js"; +export * from "./git.js"; +export * from "./extension.js"; +export * from "./packed.js"; +export * from "./defaults.js"; +export * from "./edge.js"; +export * from "./nativeProductFamilies.js"; +export * from "./nativeWorkspaceFs.js"; +export * from "./nativeWorkspaceGit.js"; +export * from "./nativeWorkspaceLsp.js"; +export * from "./nativeWorkspaceKv.js"; +export * from "./nativeChannelFacade.js"; +export * from "./nativeExtensionFacade.js"; diff --git a/js/core/src/yas/kv.ts b/js/core/src/yas/kv.ts index 578d99ef5..db452000c 100644 --- a/js/core/src/yas/kv.ts +++ b/js/core/src/yas/kv.ts @@ -1,7 +1,7 @@ /** YAS persistent key/value family codecs and client primitives. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_PATCH, @@ -11,7 +11,7 @@ import { encodeWatch, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { decodeInlineOrTransfer, decodeTransferDescriptor, @@ -21,7 +21,7 @@ import { transfersFor, type YasInlineOrTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -31,7 +31,7 @@ import { encodeExtensions, equalBytes, type YasExtension, -} from "./wire"; +} from "./wire.js"; export interface YasKvOpen { prefix: Uint8Array; diff --git a/js/core/src/yas/lsp.ts b/js/core/src/yas/lsp.ts index 85fb528c0..73099e69f 100644 --- a/js/core/src/yas/lsp.ts +++ b/js/core/src/yas/lsp.ts @@ -1,8 +1,8 @@ /** YAS LSP family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; -import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; +import { decodeFsPath, encodeFsPath, type YasFsPath } from "./fs.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -31,7 +31,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -41,7 +41,7 @@ import { encodeTypedRecord, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_LSP, @@ -59,7 +59,7 @@ export { YAS_LSP_UNWATCH, YAS_LSP_VERSION, YAS_LSP_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasLspPosition { line: number; diff --git a/js/core/src/yas/media.ts b/js/core/src/yas/media.ts index 8e19282a9..6df95584d 100644 --- a/js/core/src/yas/media.ts +++ b/js/core/src/yas/media.ts @@ -1,7 +1,7 @@ /** YAS Media family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -19,7 +19,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -27,7 +27,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -36,7 +36,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder("utf-8", { fatal: true }); @@ -61,7 +61,7 @@ export { YAS_MEDIA_UNWATCH, YAS_MEDIA_VERSION, YAS_MEDIA_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasMediaFormat { codec: number; diff --git a/js/core/src/yas/nativeChannelFacade.ts b/js/core/src/yas/nativeChannelFacade.ts index e32b54f2e..254496d51 100644 --- a/js/core/src/yas/nativeChannelFacade.ts +++ b/js/core/src/yas/nativeChannelFacade.ts @@ -13,17 +13,17 @@ import { YAS_STATUS_INVALID, YAS_STATUS_OK, YAS_STATUS_UNAVAILABLE, -} from "./generated"; +} from "./generated.js"; import { type YasChannelConnection, type YasChannelListenerRecord, type YasChannelSnapshot, YasChannelClient, -} from "./channel"; -import type { YasConnection } from "./session"; -import type { YasWatchOptions } from "./state"; -import type { YasTransfer } from "./transfer"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./channel.js"; +import type { YasConnection } from "./session.js"; +import type { YasWatchOptions } from "./state.js"; +import type { YasTransfer } from "./transfer.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: false }); diff --git a/js/core/src/yas/nativeDesktopMedia.ts b/js/core/src/yas/nativeDesktopMedia.ts index a8d049f42..7f3dd87de 100644 --- a/js/core/src/yas/nativeDesktopMedia.ts +++ b/js/core/src/yas/nativeDesktopMedia.ts @@ -15,8 +15,8 @@ import { type DesktopRevision, type NativeDesktopController, type TrayItem, -} from "../desktopModel"; -import { AudioPlayer } from "../AudioPlayer"; +} from "../desktopModel.js"; +import { AudioPlayer } from "../AudioPlayer.js"; import { ACTIVE_CAMERA, ACTIVE_MICROPHONE, @@ -50,26 +50,26 @@ import { type NativeMprisController, type PortalRequest, type PortalChoiceValue, -} from "../mediaModel"; +} from "../mediaModel.js"; import type { YasClientAuxSubscription, YasClientInfo, YasClientList, YasClientOrigin as ProductClientOrigin, -} from "../types"; -import * as g from "./generated"; +} from "../types.js"; +import * as g from "./generated.js"; import { YasClientClient, type YasClientOrigin, type YasClientRecord, type YasClientSnapshot, -} from "./client"; +} from "./client.js"; import { YasDesktopClient, type YasDesktopNotificationRecord, type YasDesktopSnapshot, type YasDesktopTrayRecord, -} from "./desktop"; +} from "./desktop.js"; import { YasMediaClient, mediaPlayerActive, @@ -82,9 +82,14 @@ import { type YasMediaPortalRequest, type YasMediaSnapshot, type YasMediaStreamStatus, -} from "./media"; -import type { YasConnection } from "./session"; -import { YasCursor, YasProtocolError, YasResultError, YasWriter } from "./wire"; +} from "./media.js"; +import type { YasConnection } from "./session.js"; +import { + YasCursor, + YasProtocolError, + YasResultError, + YasWriter, +} from "./wire.js"; const EMPTY_IMAGE: DesktopImage = { width: 0, diff --git a/js/core/src/yas/nativeExtensionFacade.ts b/js/core/src/yas/nativeExtensionFacade.ts index a6af77be2..3801ac4b9 100644 --- a/js/core/src/yas/nativeExtensionFacade.ts +++ b/js/core/src/yas/nativeExtensionFacade.ts @@ -4,7 +4,7 @@ * flags, and 32-byte content hash. */ -import * as g from "./generated"; +import * as g from "./generated.js"; import { type YasExtensionDefinitionIdentity, type YasExtensionRecord, @@ -12,9 +12,9 @@ import { type YasExtensionSnapshot, YasExtensionClient, extensionLimitsFromExtensions, -} from "./extension"; -import type { YasConnection } from "./session"; -import { YasDisconnectedError, YasProtocolError } from "./wire"; +} from "./extension.js"; +import type { YasConnection } from "./session.js"; +import { YasDisconnectedError, YasProtocolError } from "./wire.js"; const encoder = new TextEncoder(); diff --git a/js/core/src/yas/nativeProductFamilies.ts b/js/core/src/yas/nativeProductFamilies.ts index 630bbfe51..cc7b8dec5 100644 --- a/js/core/src/yas/nativeProductFamilies.ts +++ b/js/core/src/yas/nativeProductFamilies.ts @@ -28,19 +28,19 @@ import { YAS_CHANNEL_VERSION, YAS_STATUS_UNAVAILABLE, YAS_STATUS_UNSUPPORTED, -} from "./generated"; -import { YasChannelClient } from "./channel"; -import { YasEnvClient } from "./env"; -import { YasEventsClient } from "./events"; -import { YasExtensionClient } from "./extension"; -import { YasFsClient } from "./fs"; -import { YasGitClient } from "./git"; -import { YasKvClient } from "./kv"; -import { YasLspClient } from "./lsp"; -import { YasNetClient } from "./net"; -import { YasProcessClient } from "./process"; -import type { YasConnection } from "./session"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./generated.js"; +import { YasChannelClient } from "./channel.js"; +import { YasEnvClient } from "./env.js"; +import { YasEventsClient } from "./events.js"; +import { YasExtensionClient } from "./extension.js"; +import { YasFsClient } from "./fs.js"; +import { YasGitClient } from "./git.js"; +import { YasKvClient } from "./kv.js"; +import { YasLspClient } from "./lsp.js"; +import { YasNetClient } from "./net.js"; +import { YasProcessClient } from "./process.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; interface FamilyClientFactories { fs(connection: YasConnection): YasFsClient; diff --git a/js/core/src/yas/nativeRelayTransport.ts b/js/core/src/yas/nativeRelayTransport.ts index 7a914b975..1bf26afad 100644 --- a/js/core/src/yas/nativeRelayTransport.ts +++ b/js/core/src/yas/nativeRelayTransport.ts @@ -2,14 +2,14 @@ import type { ConnectionStatus, YasTransportEventMap, YasTransportMessage, -} from "../types"; +} from "../types.js"; import { YasRelayClient, type YasRelayLink, type YasRelayRoute, type YasRelayTunnelTransport, -} from "./relay"; -import type { YasTransport } from "./session"; +} from "./relay.js"; +import type { YasTransport } from "./session.js"; /** Reconnectable raw YAS tunnel for one Relay route. * diff --git a/js/core/src/yas/nativeWorkspaceFs.ts b/js/core/src/yas/nativeWorkspaceFs.ts index a79a54274..24fa04954 100644 --- a/js/core/src/yas/nativeWorkspaceFs.ts +++ b/js/core/src/yas/nativeWorkspaceFs.ts @@ -15,10 +15,10 @@ import { type FsGrepOptions, type FsGrepResult, type FsSyncOptions, -} from "../fsModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; +} from "../fsModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; import { YasFsClient, decodeFsConflictDetail, @@ -35,8 +35,8 @@ import { type YasFsQueryRecord, type YasFsRoot, type YasFsRootSource, -} from "./fs"; -import type { YasConnection } from "./session"; +} from "./fs.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -48,8 +48,8 @@ import { YAS_STATE_SNAPSHOT_END, YAS_STATE_SNAPSHOT_RECORDS, type YasStateBatch, -} from "./state"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./state.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/nativeWorkspaceGit.ts b/js/core/src/yas/nativeWorkspaceGit.ts index 0c79f2509..3eda7fdac 100644 --- a/js/core/src/yas/nativeWorkspaceGit.ts +++ b/js/core/src/yas/nativeWorkspaceGit.ts @@ -1,10 +1,10 @@ /** Workspace-facing Git operations backed directly by typed YAS Git. */ -import * as model from "../gitModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; -import { decodeFsPath, type YasFsPath } from "./fs"; +import * as model from "../gitModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; +import { decodeFsPath, type YasFsPath } from "./fs.js"; import { YasGitClient, type YasGitContentRecord, @@ -17,9 +17,9 @@ import { type YasGitRepositorySource, type YasGitSnapshot, type YasGitWatchedQuery, -} from "./git"; -import type { YasConnection } from "./session"; -import { YasProtocolError, YasResultError } from "./wire"; +} from "./git.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError, YasResultError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); @@ -772,7 +772,7 @@ class NativeGitLogSubscription implements YasNativeGitLogSubscription { } private async deliver( - update: import("./git").YasGitWatchedQueryUpdate, + update: import("./git.js").YasGitWatchedQueryUpdate, ): Promise { if (this.closed) return; if (!update.page) { diff --git a/js/core/src/yas/nativeWorkspaceKv.ts b/js/core/src/yas/nativeWorkspaceKv.ts index 447641443..0c3e9ac7d 100644 --- a/js/core/src/yas/nativeWorkspaceKv.ts +++ b/js/core/src/yas/nativeWorkspaceKv.ts @@ -4,7 +4,7 @@ import { YAS_FAMILY_KV, YAS_KV_MAX_INLINE_BYTES, YAS_KV_MAX_KEY_BYTES, -} from "./generated"; +} from "./generated.js"; import { YasKvClient, YasKvConflictError, @@ -12,16 +12,16 @@ import { type YasKvPrecondition, type YasKvStateChange, type YasKvStateUpdate, -} from "./kv"; -import type { YasConnection } from "./session"; +} from "./kv.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_DELTA, YAS_STATE_RESET, YAS_STATE_SNAPSHOT_BEGIN, YAS_STATE_SNAPSHOT_END, YAS_STATE_SNAPSHOT_RECORDS, -} from "./state"; -import { YasProtocolError } from "./wire"; +} from "./state.js"; +import { YasProtocolError } from "./wire.js"; import { WorkspaceSessionKvConflictError, copyWorkspaceSessionHash, @@ -32,7 +32,7 @@ import { type WorkspaceSessionKvWatch, type WorkspaceSessionKvWatchOptions, type WorkspaceSessionOwnedKv, -} from "../workspaceSessionKv"; +} from "../workspaceSessionKv.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/nativeWorkspaceLsp.ts b/js/core/src/yas/nativeWorkspaceLsp.ts index 3b81ce7c1..e5fb05e4b 100644 --- a/js/core/src/yas/nativeWorkspaceLsp.ts +++ b/js/core/src/yas/nativeWorkspaceLsp.ts @@ -15,11 +15,11 @@ import { LSP_STATUS_WARMING, LSP_STATUS_WRONG_TYPE, type LspOpenOptions, -} from "../lspModel"; -import { Notifier } from "../reactive"; -import type { SessionId } from "../types"; -import * as g from "./generated"; -import type { YasFsPath } from "./fs"; +} from "../lspModel.js"; +import { Notifier } from "../reactive.js"; +import type { SessionId } from "../types.js"; +import * as g from "./generated.js"; +import type { YasFsPath } from "./fs.js"; import { YasLspClient, decodeLspNoBackendDetail, @@ -29,9 +29,9 @@ import { type YasLspQueryRecord, type YasLspSnapshot, type YasLspWorkspace, -} from "./lsp"; -import type { YasConnection } from "./session"; -import { YasProtocolError } from "./wire"; +} from "./lsp.js"; +import type { YasConnection } from "./session.js"; +import { YasProtocolError } from "./wire.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8", { fatal: true }); diff --git a/js/core/src/yas/net.ts b/js/core/src/yas/net.ts index 60fee7bd2..4b7a33ad4 100644 --- a/js/core/src/yas/net.ts +++ b/js/core/src/yas/net.ts @@ -1,7 +1,7 @@ /** YAS Net family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_MODE_MESSAGE, @@ -10,7 +10,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasDisconnectedError, @@ -20,7 +20,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_NET, @@ -29,7 +29,7 @@ export { YAS_NET_DATAGRAM_STATS, YAS_NET_OPEN, YAS_NET_VERSION, -} from "./generated"; +} from "./generated.js"; export type YasNetAddress = | { kind: "tcp"; host: string; port: number } diff --git a/js/core/src/yas/packed.ts b/js/core/src/yas/packed.ts index 4c4804488..65cbe23ed 100644 --- a/js/core/src/yas/packed.ts +++ b/js/core/src/yas/packed.ts @@ -1,12 +1,12 @@ /** Validators for TOML-generated packed codecs. */ -import * as g from "./generated"; -import { decodeEventsBatch, encodeEventsBatch } from "./events"; +import * as g from "./generated.js"; +import { decodeEventsBatch, encodeEventsBatch } from "./events.js"; import { decodeTerminalGridV1, type YasTerminalGridState, -} from "./terminal-grid"; -import { YasCursor, YasProtocolError, YasWriter } from "./wire"; +} from "./terminal-grid.js"; +import { YasCursor, YasProtocolError, YasWriter } from "./wire.js"; export interface YasSurfaceColorSpace { primaries: number; diff --git a/js/core/src/yas/process.ts b/js/core/src/yas/process.ts index 0b24706a1..d6d7afd7c 100644 --- a/js/core/src/yas/process.ts +++ b/js/core/src/yas/process.ts @@ -1,7 +1,7 @@ /** YAS Process family v1 codecs and browser client. */ -import * as g from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +import * as g from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -18,7 +18,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -28,7 +28,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -37,7 +37,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_PROCESS, @@ -50,7 +50,7 @@ export { YAS_PROCESS_VERSION, YAS_PROCESS_WAIT, YAS_PROCESS_WATCH, -} from "./generated"; +} from "./generated.js"; export type YasProcessCwd = | { kind: "server-default" } diff --git a/js/core/src/yas/recording.ts b/js/core/src/yas/recording.ts index 1f8a160b3..bb5574af9 100644 --- a/js/core/src/yas/recording.ts +++ b/js/core/src/yas/recording.ts @@ -2,9 +2,9 @@ import { YAS_HARD_MAX_WIRE_FRAME, YAS_TERMINAL_FRAME_KEYFRAME, YAS_TERMINAL_GRID_CODEC_V1, -} from "./generated"; -import { decodeTerminalFrame, type YasTerminalFrameEvent } from "./terminal"; -import { YasCursor, YasProtocolError } from "./wire"; +} from "./generated.js"; +import { decodeTerminalFrame, type YasTerminalFrameEvent } from "./terminal.js"; +import { YasCursor, YasProtocolError } from "./wire.js"; const YASREC1_MAGIC = new Uint8Array([ 0x59, 0x41, 0x53, 0x52, 0x45, 0x43, 0x31, 0x0a, diff --git a/js/core/src/yas/relay.ts b/js/core/src/yas/relay.ts index 463b1c3ff..255c698dc 100644 --- a/js/core/src/yas/relay.ts +++ b/js/core/src/yas/relay.ts @@ -2,8 +2,8 @@ import type { YasTransportEventMap, YasTransportMessage, ConnectionStatus, -} from "../types"; -import { YAS_FAMILY_RELAY } from "./core"; +} from "../types.js"; +import { YAS_FAMILY_RELAY } from "./core.js"; import { YAS_RELAY_AVAILABILITY_AVAILABLE, YAS_RELAY_AVAILABILITY_DEGRADED, @@ -38,12 +38,12 @@ import { YAS_RELAY_UNWATCH, YAS_RELAY_VERSION, YAS_RELAY_WATCH, -} from "./generated"; +} from "./generated.js"; import { YasConnection, type YasConnectionOptions, type YasTransport, -} from "./session"; +} from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -59,7 +59,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -67,7 +67,7 @@ import { decodeTransferDescriptor, transfersFor, type YasTransfer, -} from "./transfer"; +} from "./transfer.js"; import { YAS_STATUS_CANCELLED, YAS_STATUS_RESOURCE_EXHAUSTED, @@ -78,7 +78,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_RELAY_AVAILABILITY_AVAILABLE, @@ -100,7 +100,7 @@ export { YAS_RELAY_UNWATCH, YAS_RELAY_VERSION, YAS_RELAY_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasRelayRoute { handle: bigint; diff --git a/js/core/src/yas/selection.ts b/js/core/src/yas/selection.ts index 5566942e4..f375d1c64 100644 --- a/js/core/src/yas/selection.ts +++ b/js/core/src/yas/selection.ts @@ -40,8 +40,8 @@ import { YAS_SELECTION_WATCH, YAS_STATUS_NOT_FOUND, YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, -} from "./generated"; -import type { YasConnection } from "./session"; +} from "./generated.js"; +import type { YasConnection } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -58,7 +58,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_RECEIVER_TO_SENDER, @@ -70,7 +70,7 @@ import { transfersFor, type YasTransfer, type YasTransferDescriptor, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -81,7 +81,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_SELECTION, @@ -119,7 +119,7 @@ export { YAS_SELECTION_UNWATCH, YAS_SELECTION_VERSION, YAS_SELECTION_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasSelectionInlineItem { mime: string; diff --git a/js/core/src/yas/session.ts b/js/core/src/yas/session.ts index d832669d0..87955ef2f 100644 --- a/js/core/src/yas/session.ts +++ b/js/core/src/yas/session.ts @@ -2,7 +2,7 @@ import type { YasTransport as BaseYasTransport, YasTransportMessage, ConnectionStatus, -} from "../types"; +} from "../types.js"; import { YAS_CORE_CANCEL, YAS_CORE_FAMILY_UPDATE, @@ -35,10 +35,10 @@ import { type YasFamilyDescriptor, type YasGoAway, type YasServerHello, -} from "./core"; -import type { YasExtension } from "./wire"; -import { YAS_FAMILY_DEPENDENCIES } from "./generated"; -import { validateYasDatagramFrame } from "./datagram"; +} from "./core.js"; +import type { YasExtension } from "./wire.js"; +import { YAS_FAMILY_DEPENDENCIES } from "./generated.js"; +import { validateYasDatagramFrame } from "./datagram.js"; import { YAS_CLASS_EVENT, YAS_CLASS_REQUEST, @@ -64,7 +64,7 @@ import { encodeYasFrame, frameForByteStream, type YasFrame, -} from "./wire"; +} from "./wire.js"; export interface YasTransport extends BaseYasTransport { /** Browser transports use messages; Relay tunnels expose a raw byte stream. */ diff --git a/js/core/src/yas/state.ts b/js/core/src/yas/state.ts index 7afa63588..1676e53db 100644 --- a/js/core/src/yas/state.ts +++ b/js/core/src/yas/state.ts @@ -1,5 +1,5 @@ -import type { YasConnection, YasReceiveBudgetLease } from "./session"; -import type { ConnectionStatus } from "../types"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; +import type { ConnectionStatus } from "../types.js"; import { YAS_STATE_MODE_REPLAY, YAS_STATE_MODE_SNAPSHOT, @@ -13,7 +13,7 @@ import { YAS_STATE_RECORD_REMOVE, YAS_STATE_RECORD_REPLACE, YAS_STATE_WATCH_RESUME, -} from "./generated"; +} from "./generated.js"; import { YasCursor, YasProtocolError, @@ -23,7 +23,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export const YAS_STATE_ADD = YAS_STATE_RECORD_ADD; export const YAS_STATE_REPLACE = YAS_STATE_RECORD_REPLACE; diff --git a/js/core/src/yas/surface.ts b/js/core/src/yas/surface.ts index 152e72a02..3d92a484a 100644 --- a/js/core/src/yas/surface.ts +++ b/js/core/src/yas/surface.ts @@ -6,7 +6,7 @@ import { YAS_SURFACE_COLOR_CAP_HDR10_AV1_444, YAS_SURFACE_COLOR_CAP_AV1_444, YAS_SURFACE_COLOR_CAP_H264_444, -} from "./generated"; +} from "./generated.js"; /** YAS Surface family v1 codecs and browser client. */ import { @@ -80,8 +80,8 @@ import { YAS_SURFACE_TEXT_INPUT_REQUESTED, YAS_SURFACE_TOUCH_PHASE_CANCEL, YAS_SURFACE_TOUCH_PHASE_FRAME, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -99,7 +99,7 @@ import { negotiatedStateLimitU32, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -107,7 +107,7 @@ import { transfersFor, type YasInlineOrTransfer, type YasTransfer, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YasProtocolError, @@ -116,7 +116,7 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; export { YAS_FAMILY_SURFACE, @@ -144,7 +144,7 @@ export { YAS_SURFACE_UNWATCH, YAS_SURFACE_VERSION, YAS_SURFACE_WATCH, -} from "./generated"; +} from "./generated.js"; export interface YasSurfaceRecord { surfaceHandle: bigint; diff --git a/js/core/src/yas/terminal-grid.ts b/js/core/src/yas/terminal-grid.ts index 3371c63c3..014b8ced2 100644 --- a/js/core/src/yas/terminal-grid.ts +++ b/js/core/src/yas/terminal-grid.ts @@ -1,4 +1,4 @@ -import { lz4Decompress } from "../lz4"; +import { lz4Decompress } from "../lz4.js"; import { YAS_TERMINAL_FRAME_CODEC_COMPRESSED, YAS_TERMINAL_FRAME_COMPONENTS, @@ -14,9 +14,9 @@ import { YAS_TERMINAL_GRID_CODEC_V1, YAS_TERMINAL_CELL_BYTES, YAS_TERMINAL_COMPONENT_KEYBOARD_FLAGS, -} from "./generated"; -import type { YasTerminalFrameEvent } from "./terminal"; -import { YasCursor, YasProtocolError } from "./wire"; +} from "./generated.js"; +import type { YasTerminalFrameEvent } from "./terminal.js"; +import { YasCursor, YasProtocolError } from "./wire.js"; const FRAME_KNOWN_FLAGS = YAS_TERMINAL_FRAME_KEYFRAME | diff --git a/js/core/src/yas/terminal.ts b/js/core/src/yas/terminal.ts index 530a178b2..63bcbc4df 100644 --- a/js/core/src/yas/terminal.ts +++ b/js/core/src/yas/terminal.ts @@ -1,4 +1,4 @@ -import * as g from "./generated"; +import * as g from "./generated.js"; import { YAS_CLASS_EVENT, YAS_FAMILY_TERMINAL, @@ -90,8 +90,8 @@ import { YAS_TERMINAL_WHEEL_AT, YAS_TERMINAL_WHEEL_SOURCE_CONTINUOUS, YAS_TERMINAL_WRITE, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_STATE_ADD, YAS_STATE_DELTA, @@ -109,7 +109,7 @@ import { estimateStateRetainedBytes, type YasStateBatch, type YasWatchOptions, -} from "./state"; +} from "./state.js"; import { YAS_TRANSFER_MODE_BYTE, YAS_TRANSFER_SENDER_TO_RECEIVER, @@ -119,7 +119,7 @@ import { type YasTransfer, type YasTransferDescriptor, type YasTransferManager, -} from "./transfer"; +} from "./transfer.js"; import { YasCursor, YAS_MAX_BULK_CHUNK, @@ -129,9 +129,9 @@ import { encodeExtensions, type YasExtension, type YasTypedRecord, -} from "./wire"; +} from "./wire.js"; -export * from "./terminal-grid"; +export * from "./terminal-grid.js"; export { YAS_FAMILY_TERMINAL, YAS_TERMINAL_CLOSE, @@ -166,7 +166,7 @@ export { YAS_TERMINAL_WATCH, YAS_TERMINAL_WHEEL, YAS_TERMINAL_WRITE, -} from "./generated"; +} from "./generated.js"; const terminalStateTags = new Set([ YAS_TERMINAL_STATE_TITLE_EXTENSION, diff --git a/js/core/src/yas/terminalRenderer.ts b/js/core/src/yas/terminalRenderer.ts index a68961f49..ec96cb284 100644 --- a/js/core/src/yas/terminalRenderer.ts +++ b/js/core/src/yas/terminalRenderer.ts @@ -1,6 +1,6 @@ -import { lz4Compress } from "../lz4"; -import type { YasTerminalGridState } from "./terminal"; -import { YasWriter } from "./wire"; +import { lz4Compress } from "../lz4.js"; +import type { YasTerminalGridState } from "./terminal.js"; +import { YasWriter } from "./wire.js"; // Browser renderer ingest codec. This is an internal WASM boundary, not a YAS // transport frame: native handles and family frames never enter this byte diff --git a/js/core/src/yas/transfer.ts b/js/core/src/yas/transfer.ts index 617465047..6629c816f 100644 --- a/js/core/src/yas/transfer.ts +++ b/js/core/src/yas/transfer.ts @@ -1,5 +1,5 @@ -import { YAS_FAMILY_TRANSFER } from "./core"; -import type { ConnectionStatus } from "../types"; +import { YAS_FAMILY_TRANSFER } from "./core.js"; +import type { ConnectionStatus } from "../types.js"; import { YAS_TRANSFER_BYTE_DATA, YAS_TRANSFER_CLOSE, @@ -18,8 +18,8 @@ import { YAS_TRANSFER_SENSITIVE_CONTENT_EXTENSION, YAS_TRANSFER_UPLOAD_STAGE_EXTENSION, YAS_TRANSFER_VERSION, -} from "./generated"; -import type { YasConnection, YasReceiveBudgetLease } from "./session"; +} from "./generated.js"; +import type { YasConnection, YasReceiveBudgetLease } from "./session.js"; import { YAS_MAX_BULK_CHUNK, YAS_STATUS_CANCELLED, @@ -32,7 +32,7 @@ import { decodeExtensions, encodeExtensions, type YasExtension, -} from "./wire"; +} from "./wire.js"; export { YAS_TRANSFER_BYTE_DATA, @@ -43,7 +43,7 @@ export { YAS_TRANSFER_MODE_MESSAGE, YAS_TRANSFER_RESET, YAS_TRANSFER_VERSION, -} from "./generated"; +} from "./generated.js"; export const YAS_TRANSFER_RECEIVER_TO_SENDER = YAS_TRANSFER_DIRECTION_RECEIVER_TO_SENDER; diff --git a/js/core/src/yas/wire.ts b/js/core/src/yas/wire.ts index b5c3081db..ba2ca7bf8 100644 --- a/js/core/src/yas/wire.ts +++ b/js/core/src/yas/wire.ts @@ -18,7 +18,7 @@ import { YAS_PRE_HELLO_MAX_FRAME, YAS_STATUS_OK, YAS_STREAM_LENGTH_BYTES, -} from "./generated"; +} from "./generated.js"; export { YAS_CLASS_EVENT, @@ -40,7 +40,7 @@ export { YAS_STATUS_STALE, YAS_STATUS_IO, YAS_STATUS_INTERNAL, -} from "./generated"; +} from "./generated.js"; export const YAS_PREFACE = Uint8Array.from( YAS_PREFACE_HEX.match(/../g)!.map((byte) => Number.parseInt(byte, 16)), diff --git a/js/core/tsconfig.json b/js/core/tsconfig.json index cc34ddfb5..1e95c4018 100644 --- a/js/core/tsconfig.json +++ b/js/core/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "paths": { "@yas-run/browser": ["../../crates/browser/pkg/yas_browser.d.ts"] }, diff --git a/js/react/src/YasSurfaceView.tsx b/js/react/src/YasSurfaceView.tsx index 9709aee2c..98aec5fd0 100644 --- a/js/react/src/YasSurfaceView.tsx +++ b/js/react/src/YasSurfaceView.tsx @@ -16,7 +16,7 @@ import type { SurfaceId, SurfaceTouchMode, } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "./YasContext"; +import { useRequiredYasWorkspace } from "./YasContext.js"; export interface YasSurfaceViewProps { connectionId: ConnectionId; @@ -25,7 +25,9 @@ export interface YasSurfaceViewProps { style?: React.CSSProperties; /** Render cached frames without owning a server stream when false. */ live?: boolean; - /** How touchscreen contacts are delivered. Defaults to pointer emulation. */ + /** How touchscreen contacts are delivered. Defaults to `"direct"`, which + * forwards every contact to the Wayland client's `wl_touch`; `"pointer"` + * opts into single-finger click/scroll emulation. */ touchMode?: SurfaceTouchMode; /** * Whether this view owns its surface's size, resizing it to fill the diff --git a/js/react/src/YasTerminal.tsx b/js/react/src/YasTerminal.tsx index 9a3ce7c99..894cf58de 100644 --- a/js/react/src/YasTerminal.tsx +++ b/js/react/src/YasTerminal.tsx @@ -3,10 +3,10 @@ import type { Ref } from "react"; import type { ConnectionStatus } from "@yas-run/core"; import type { Terminal } from "@yas-run/browser"; import { YasTerminalSurface } from "@yas-run/core"; -import type { YasTerminalProps } from "./types"; -import { useYasContext, useRequiredYasWorkspace } from "./YasContext"; -import { useYasConnection } from "./hooks/useYasConnection"; -import { useYasSession } from "./hooks/useYasSession"; +import type { YasTerminalProps } from "./types.js"; +import { useYasContext, useRequiredYasWorkspace } from "./YasContext.js"; +import { useYasConnection } from "./hooks/useYasConnection.js"; +import { useYasSession } from "./hooks/useYasSession.js"; // --------------------------------------------------------------------------- // Public handle exposed via ref diff --git a/js/react/src/hooks/index.ts b/js/react/src/hooks/index.ts index 9f5a0f921..b99e3227d 100644 --- a/js/react/src/hooks/index.ts +++ b/js/react/src/hooks/index.ts @@ -1,5 +1,5 @@ -export { useYasConnection } from "./useYasConnection"; -export { useYasSessions } from "./useYasSessions"; -export { useYasWorkspace, useYasWorkspaceState } from "./useYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./useYasSession"; -export { useYasWorkspaceConnection } from "./useYasWorkspaceConnection"; +export { useYasConnection } from "./useYasConnection.js"; +export { useYasSessions } from "./useYasSessions.js"; +export { useYasWorkspace, useYasWorkspaceState } from "./useYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./useYasSession.js"; +export { useYasWorkspaceConnection } from "./useYasWorkspaceConnection.js"; diff --git a/js/react/src/hooks/useYasConnection.ts b/js/react/src/hooks/useYasConnection.ts index 640384ca5..7d74bd248 100644 --- a/js/react/src/hooks/useYasConnection.ts +++ b/js/react/src/hooks/useYasConnection.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasConnectionSnapshot, ConnectionId } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasConnection( connectionId?: ConnectionId, diff --git a/js/react/src/hooks/useYasSession.ts b/js/react/src/hooks/useYasSession.ts index 602bc13d5..f84e36c3c 100644 --- a/js/react/src/hooks/useYasSession.ts +++ b/js/react/src/hooks/useYasSession.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasSession, SessionId } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasSession(sessionId: SessionId | null): YasSession | null { const workspace = useRequiredYasWorkspace(); diff --git a/js/react/src/hooks/useYasSessions.ts b/js/react/src/hooks/useYasSessions.ts index 07487c24e..884cd2dc5 100644 --- a/js/react/src/hooks/useYasSessions.ts +++ b/js/react/src/hooks/useYasSessions.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasSession } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasSessions(): readonly YasSession[] { const workspace = useRequiredYasWorkspace(); diff --git a/js/react/src/hooks/useYasWorkspace.ts b/js/react/src/hooks/useYasWorkspace.ts index bfc05b25a..bcda499be 100644 --- a/js/react/src/hooks/useYasWorkspace.ts +++ b/js/react/src/hooks/useYasWorkspace.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore } from "react"; import type { YasWorkspaceSnapshot } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function useYasWorkspace() { return useRequiredYasWorkspace(); diff --git a/js/react/src/index.ts b/js/react/src/index.ts index 2a5e79802..c16cca515 100644 --- a/js/react/src/index.ts +++ b/js/react/src/index.ts @@ -1,18 +1,21 @@ -export { YasTerminal } from "./YasTerminal"; -export type { YasTerminalHandle } from "./YasTerminal"; -export { YasSurfaceView } from "./YasSurfaceView"; +export { YasTerminal } from "./YasTerminal.js"; +export type { YasTerminalHandle } from "./YasTerminal.js"; +export { YasSurfaceView } from "./YasSurfaceView.js"; export type { YasSurfaceViewProps, YasSurfaceViewHandle, -} from "./YasSurfaceView"; +} from "./YasSurfaceView.js"; -export type { YasTerminalProps } from "./types"; +export type { YasTerminalProps } from "./types.js"; -export { useYasConnection } from "./hooks/useYasConnection"; -export { useYasSessions } from "./hooks/useYasSessions"; -export { useYasWorkspace, useYasWorkspaceState } from "./hooks/useYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession"; -export { useYasWorkspaceConnection } from "./hooks/useYasWorkspaceConnection"; +export { useYasConnection } from "./hooks/useYasConnection.js"; +export { useYasSessions } from "./hooks/useYasSessions.js"; +export { + useYasWorkspace, + useYasWorkspaceState, +} from "./hooks/useYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession.js"; +export { useYasWorkspaceConnection } from "./hooks/useYasWorkspaceConnection.js"; -export { YasWorkspaceProvider } from "./YasContext"; -export type { YasContextValue, YasProviderProps } from "./YasContext"; +export { YasWorkspaceProvider } from "./YasContext.js"; +export type { YasContextValue, YasProviderProps } from "./YasContext.js"; diff --git a/js/react/tsconfig.json b/js/react/tsconfig.json index f2716ac06..7c28e1e40 100644 --- a/js/react/tsconfig.json +++ b/js/react/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "jsx": "react-jsx", "paths": { "@yas-run/browser": ["../../crates/browser/pkg/yas_browser.d.ts"] diff --git a/js/solid/src/YasSurfaceView.tsx b/js/solid/src/YasSurfaceView.tsx index fa4bb451f..52d430efb 100644 --- a/js/solid/src/YasSurfaceView.tsx +++ b/js/solid/src/YasSurfaceView.tsx @@ -15,7 +15,7 @@ import { driveSurfaceResize, } from "@yas-run/core"; import type { ConnectionId, SurfaceId, SurfaceTouchMode } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "./YasContext"; +import { useRequiredYasWorkspace } from "./YasContext.js"; export interface YasSurfaceViewProps { connectionId: ConnectionId; @@ -43,7 +43,9 @@ export interface YasSurfaceViewProps { * do not create a server-side video subscription. Defaults to true. */ live?: boolean; - /** How touchscreen contacts are delivered. Defaults to pointer emulation. */ + /** How touchscreen contacts are delivered. Defaults to `"direct"`, which + * forwards every contact to the Wayland client's `wl_touch`; `"pointer"` + * opts into single-finger click/scroll emulation. */ touchMode?: SurfaceTouchMode; /** * Surface zoom factor, e.g. 1.25 for 125% or an exact 1.25x scale. diff --git a/js/solid/src/YasTerminal.tsx b/js/solid/src/YasTerminal.tsx index 7383a90c9..73886ef17 100644 --- a/js/solid/src/YasTerminal.tsx +++ b/js/solid/src/YasTerminal.tsx @@ -8,8 +8,8 @@ import { } from "solid-js"; import { YasTerminalSurface } from "@yas-run/core"; import type { SessionId, TerminalPalette } from "@yas-run/core"; -import { useYasContext, useRequiredYasWorkspace } from "./YasContext"; -import { createYasWorkspaceState } from "./hooks/createYasWorkspace"; +import { useYasContext, useRequiredYasWorkspace } from "./YasContext.js"; +import { createYasWorkspaceState } from "./hooks/createYasWorkspace.js"; export interface YasTerminalProps { sessionId: SessionId | null; diff --git a/js/solid/src/hooks/createYasSessions.ts b/js/solid/src/hooks/createYasSessions.ts index 5d5aa76b2..c2fecc822 100644 --- a/js/solid/src/hooks/createYasSessions.ts +++ b/js/solid/src/hooks/createYasSessions.ts @@ -1,5 +1,5 @@ import type { YasSession, YasWorkspace } from "@yas-run/core"; -import { createYasWorkspaceState } from "./createYasWorkspace"; +import { createYasWorkspaceState } from "./createYasWorkspace.js"; export function createYasSessions( workspace?: YasWorkspace, diff --git a/js/solid/src/hooks/createYasWorkspace.ts b/js/solid/src/hooks/createYasWorkspace.ts index e797d6ded..774421f13 100644 --- a/js/solid/src/hooks/createYasWorkspace.ts +++ b/js/solid/src/hooks/createYasWorkspace.ts @@ -1,6 +1,6 @@ import { createSignal, onCleanup } from "solid-js"; import type { YasWorkspace, YasWorkspaceSnapshot } from "@yas-run/core"; -import { useRequiredYasWorkspace } from "../YasContext"; +import { useRequiredYasWorkspace } from "../YasContext.js"; export function createYasWorkspace(): YasWorkspace { return useRequiredYasWorkspace(); diff --git a/js/solid/src/hooks/index.ts b/js/solid/src/hooks/index.ts index 755c5e627..2256ecf36 100644 --- a/js/solid/src/hooks/index.ts +++ b/js/solid/src/hooks/index.ts @@ -1,8 +1,8 @@ -export { useYasConnection } from "./useYasConnection"; -export { createYasSessions } from "./createYasSessions"; +export { useYasConnection } from "./useYasConnection.js"; +export { createYasSessions } from "./createYasSessions.js"; export { createYasWorkspace, createYasWorkspaceState, -} from "./createYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./useYasSession"; -export { createYasWorkspaceConnection } from "./createYasWorkspaceConnection"; +} from "./createYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./useYasSession.js"; +export { createYasWorkspaceConnection } from "./createYasWorkspaceConnection.js"; diff --git a/js/solid/src/index.ts b/js/solid/src/index.ts index 3d8745a17..0001b9232 100644 --- a/js/solid/src/index.ts +++ b/js/solid/src/index.ts @@ -1,17 +1,17 @@ -export { YasTerminal } from "./YasTerminal"; -export type { YasTerminalProps } from "./YasTerminal"; +export { YasTerminal } from "./YasTerminal.js"; +export type { YasTerminalProps } from "./YasTerminal.js"; -export { YasSurfaceView } from "./YasSurfaceView"; -export type { YasSurfaceViewProps } from "./YasSurfaceView"; +export { YasSurfaceView } from "./YasSurfaceView.js"; +export type { YasSurfaceViewProps } from "./YasSurfaceView.js"; -export { useYasConnection } from "./hooks/useYasConnection"; -export { createYasSessions } from "./hooks/createYasSessions"; +export { useYasConnection } from "./hooks/useYasConnection.js"; +export { createYasSessions } from "./hooks/createYasSessions.js"; export { createYasWorkspace, createYasWorkspaceState, -} from "./hooks/createYasWorkspace"; -export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession"; -export { createYasWorkspaceConnection } from "./hooks/createYasWorkspaceConnection"; +} from "./hooks/createYasWorkspace.js"; +export { useYasSession, useYasFocusedSession } from "./hooks/useYasSession.js"; +export { createYasWorkspaceConnection } from "./hooks/createYasWorkspaceConnection.js"; -export { YasWorkspaceProvider } from "./YasContext"; -export type { YasContextValue, YasProviderProps } from "./YasContext"; +export { YasWorkspaceProvider } from "./YasContext.js"; +export type { YasContextValue, YasProviderProps } from "./YasContext.js"; diff --git a/js/solid/tsconfig.json b/js/solid/tsconfig.json index 230ac451a..03c1a1db0 100644 --- a/js/solid/tsconfig.json +++ b/js/solid/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "jsx": "preserve", "jsxImportSource": "solid-js", "paths": {