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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Each released version links straight to its GitHub Release tag.
`EventRpc`, tried after the declared literal tags and before the unknown carrier, and
`UnknownEvent` refuses an `rpc.*` tag rather than absorbing it. Four operations arrived —
`v2.plugin.awaitActivation`, `v2.plugin.check`, and `v2.plugin.update` as
`PluginsClient.PostAwaitActivationAsync`, `PostCheckAsync`, and `PostUpdateAsync`, and
`v2.rpc.call` as `PostCallAsync` on the new `RpcClient` family. Upstream also removed and
`PluginsClient.AwaitPluginActivationAsync`, `CheckPluginUpdatesAsync`, and `UpdatePluginsAsync`,
and `v2.rpc.call` as `CallAsync` on the new `RpcClient` family. Upstream also removed and
reshaped the plugin types, which nightly consumers will meet as compile breaks: the
`plugin.added` event arm is gone, and `PluginAdded` and `PluginAddedData` with it; `IPluginInfo`
and its `PluginInfoActive`, `PluginInfoFailed`, and `UnknownPluginInfo` variants are replaced by
Expand Down Expand Up @@ -63,6 +63,11 @@ Each released version links straight to its GitHub Release tag.

### 🛠️ General

- **Breaking (nightly API):** `RpcClient.PostCallAsync` is now `CallAsync`;
`PluginsClient.PostAwaitActivationAsync`, `PostCheckAsync`, and `PostUpdateAsync` are now
`AwaitPluginActivationAsync`, `CheckPluginUpdatesAsync`, and `UpdatePluginsAsync`. Their
`OpenCodeRoutes` members use the corresponding names without `Async`; request/response types and
wire behavior are unchanged.
- **Target frameworks:** `netstandard2.0`, `net472`, `net8.0`, `net9.0`, `net10.0` — for both
packages. The suite runs on `net472` on Windows, real-process launcher tests included, and on
`net8.0`/`net9.0`/`net10.0` on all three OSes. `netstandard2.0` is a consumption target rather
Expand Down
38 changes: 19 additions & 19 deletions src/OpenCode.Sdk/OpenCodeRoutes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,17 +1186,17 @@ public static string RemovePersistentPty(string ptyId)
public static class Plugins
{
/// <summary>
/// The &apos;GET /api/plugin&apos; route template.
/// The &apos;POST /api/plugin/await-activation&apos; route template.
/// </summary>
public const string ListPluginsTemplate = "/api/plugin";
public const string AwaitPluginActivationTemplate = "/api/plugin/await-activation";
/// <summary>
/// Builds the &apos;/api/plugin&apos; route.
/// Builds the &apos;/api/plugin/await-activation&apos; route.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <returns>The escaped route.</returns>
public static string ListPlugins(PluginListRequest? request = null)
public static string AwaitPluginActivation(PluginAwaitActivationPostRequest? request = null)
{
var path = "/api/plugin";
var path = "/api/plugin/await-activation";
if (request is null)
{
return path;
Expand All @@ -1208,17 +1208,17 @@ public static string ListPlugins(PluginListRequest? request = null)
}

/// <summary>
/// The &apos;POST /api/plugin/await-activation&apos; route template.
/// The &apos;POST /api/plugin/check&apos; route template.
/// </summary>
public const string PostAwaitActivationTemplate = "/api/plugin/await-activation";
public const string CheckPluginUpdatesTemplate = "/api/plugin/check";
/// <summary>
/// Builds the &apos;/api/plugin/await-activation&apos; route.
/// Builds the &apos;/api/plugin/check&apos; route.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <returns>The escaped route.</returns>
public static string PostAwaitActivation(PluginAwaitActivationPostRequest? request = null)
public static string CheckPluginUpdates(PluginCheckPostRequest? request = null)
{
var path = "/api/plugin/await-activation";
var path = "/api/plugin/check";
if (request is null)
{
return path;
Expand All @@ -1230,17 +1230,17 @@ public static string PostAwaitActivation(PluginAwaitActivationPostRequest? reque
}

/// <summary>
/// The &apos;POST /api/plugin/check&apos; route template.
/// The &apos;GET /api/plugin&apos; route template.
/// </summary>
public const string PostCheckTemplate = "/api/plugin/check";
public const string ListPluginsTemplate = "/api/plugin";
/// <summary>
/// Builds the &apos;/api/plugin/check&apos; route.
/// Builds the &apos;/api/plugin&apos; route.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <returns>The escaped route.</returns>
public static string PostCheck(PluginCheckPostRequest? request = null)
public static string ListPlugins(PluginListRequest? request = null)
{
var path = "/api/plugin/check";
var path = "/api/plugin";
if (request is null)
{
return path;
Expand All @@ -1254,13 +1254,13 @@ public static string PostCheck(PluginCheckPostRequest? request = null)
/// <summary>
/// The &apos;POST /api/plugin/update&apos; route template.
/// </summary>
public const string PostUpdateTemplate = "/api/plugin/update";
public const string UpdatePluginsTemplate = "/api/plugin/update";
/// <summary>
/// Builds the &apos;/api/plugin/update&apos; route.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <returns>The escaped route.</returns>
public static string PostUpdate(PluginUpdatePostRequest? request = null)
public static string UpdatePlugins(PluginUpdatePostRequest? request = null)
{
var path = "/api/plugin/update";
if (request is null)
Expand Down Expand Up @@ -1585,15 +1585,15 @@ public static class Rpc
/// <summary>
/// The &apos;POST /api/rpc/{rpcID}/{method}&apos; route template.
/// </summary>
public const string PostCallTemplate = "/api/rpc/{rpcID}/{method}";
public const string CallTemplate = "/api/rpc/{rpcID}/{method}";
/// <summary>
/// Builds the &apos;/api/rpc/{rpcID}/{method}&apos; route.
/// </summary>
/// <param name = "rpcId">The &apos;rpcID&apos; route value.</param>
/// <param name = "method">The &apos;method&apos; route value.</param>
/// <param name = "request">The request shaping the query.</param>
/// <returns>The escaped route.</returns>
public static string PostCall(string rpcId, string method, RpcCallPostRequest? request = null)
public static string Call(string rpcId, string method, RpcCallPostRequest? request = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(rpcId);
if (rpcId is "." or "..")
Expand Down
32 changes: 16 additions & 16 deletions src/OpenCode.Sdk/Plugins/PluginsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,45 @@ protected PluginsClient()
private Pipeline Pipeline => _pipeline ?? throw MockSeam.CreateError("PluginsClient", "Pipeline");

/// <summary>
/// List plugins. Retrieve enabled server plugins and their current status.
/// Wait for plugin activation. Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <param name = "requestOptions">The per-call options.</param>
/// <param name = "cancellationToken">The cancellation token.</param>
/// <returns>The &apos;PluginListResponse&apos; envelope.</returns>
/// <returns>The &apos;PluginAwaitActivationPostResponse&apos; envelope.</returns>
/// <exception cref = "OpenCodeApiException">The API returned an error status (declared: 400, 401) and NoThrow was not selected.</exception>
/// <exception cref = "OpenCodeTransportException">The server could not be reached or returned a malformed success body.</exception>
public virtual Task<PluginListResponse> ListPluginsAsync(PluginListRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public virtual Task<PluginAwaitActivationPostResponse> AwaitPluginActivationAsync(PluginAwaitActivationPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Plugins.ListPlugins(request), PluginListResponseAdapter.Instance, requestOptions, cancellationToken);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.AwaitPluginActivation(request), PluginAwaitActivationPostResponseAdapter.Instance, requestOptions, cancellationToken);
}

/// <summary>
/// Wait for plugin activation. Wait for configured plugin activation at a Location to settle, including missing-package installs. Completion does not imply every plugin succeeded or background resource discovery finished. Cancelling this wait does not cancel activation.
/// Check plugin updates. Check one or all package plugins for available updates.
/// </summary>
/// <param name = "request">The request shaping the query.</param>
/// <param name = "request">The request body; an empty body is sent when omitted.</param>
/// <param name = "requestOptions">The per-call options.</param>
/// <param name = "cancellationToken">The cancellation token.</param>
/// <returns>The &apos;PluginAwaitActivationPostResponse&apos; envelope.</returns>
/// <returns>The &apos;PluginCheckPostResponse&apos; envelope.</returns>
/// <exception cref = "OpenCodeApiException">The API returned an error status (declared: 400, 401) and NoThrow was not selected.</exception>
/// <exception cref = "OpenCodeTransportException">The server could not be reached or returned a malformed success body.</exception>
public virtual Task<PluginAwaitActivationPostResponse> PostAwaitActivationAsync(PluginAwaitActivationPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public virtual Task<PluginCheckPostResponse> CheckPluginUpdatesAsync(PluginCheckPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.PostAwaitActivation(request), PluginAwaitActivationPostResponseAdapter.Instance, requestOptions, cancellationToken);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.CheckPluginUpdates(request), request ?? EmptyPluginCheckPostRequest, OpenCodeJsonContext.Default.PluginCheckPostRequest, PluginCheckPostResponseAdapter.Instance, requestOptions, cancellationToken);
}

/// <summary>
/// Check plugin updates. Check one or all package plugins for available updates.
/// List plugins. Retrieve enabled server plugins and their current status.
/// </summary>
/// <param name = "request">The request body; an empty body is sent when omitted.</param>
/// <param name = "request">The request shaping the query.</param>
/// <param name = "requestOptions">The per-call options.</param>
/// <param name = "cancellationToken">The cancellation token.</param>
/// <returns>The &apos;PluginCheckPostResponse&apos; envelope.</returns>
/// <returns>The &apos;PluginListResponse&apos; envelope.</returns>
/// <exception cref = "OpenCodeApiException">The API returned an error status (declared: 400, 401) and NoThrow was not selected.</exception>
/// <exception cref = "OpenCodeTransportException">The server could not be reached or returned a malformed success body.</exception>
public virtual Task<PluginCheckPostResponse> PostCheckAsync(PluginCheckPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public virtual Task<PluginListResponse> ListPluginsAsync(PluginListRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.PostCheck(request), request ?? EmptyPluginCheckPostRequest, OpenCodeJsonContext.Default.PluginCheckPostRequest, PluginCheckPostResponseAdapter.Instance, requestOptions, cancellationToken);
return Pipeline.ExecuteAsync(HttpMethod.Get, OpenCodeRoutes.Plugins.ListPlugins(request), PluginListResponseAdapter.Instance, requestOptions, cancellationToken);
}

/// <summary>
Expand All @@ -79,9 +79,9 @@ public virtual Task<PluginCheckPostResponse> PostCheckAsync(PluginCheckPostReque
/// <returns>The &apos;PluginUpdatePostResponse&apos; envelope.</returns>
/// <exception cref = "OpenCodeApiException">The API returned an error status (declared: 400, 401, 503) and NoThrow was not selected.</exception>
/// <exception cref = "OpenCodeTransportException">The server could not be reached or returned a malformed success body.</exception>
public virtual Task<PluginUpdatePostResponse> PostUpdateAsync(PluginUpdatePostRequest request, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public virtual Task<PluginUpdatePostResponse> UpdatePluginsAsync(PluginUpdatePostRequest request, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.PostUpdate(request), request, OpenCodeJsonContext.Default.PluginUpdatePostRequest, PluginUpdatePostResponseAdapter.Instance, requestOptions, cancellationToken);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Plugins.UpdatePlugins(request), request, OpenCodeJsonContext.Default.PluginUpdatePostRequest, PluginUpdatePostResponseAdapter.Instance, requestOptions, cancellationToken);
}
}
4 changes: 2 additions & 2 deletions src/OpenCode.Sdk/Rpc/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ protected RpcClient()
/// <returns>The &apos;RpcCallPostResponse&apos; envelope.</returns>
/// <exception cref = "OpenCodeApiException">The API returned an error status (declared: 400, 401, 500) and NoThrow was not selected.</exception>
/// <exception cref = "OpenCodeTransportException">The server could not be reached or returned a malformed success body.</exception>
public virtual Task<RpcCallPostResponse> PostCallAsync(string rpcId, string method, RpcCallPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
public virtual Task<RpcCallPostResponse> CallAsync(string rpcId, string method, RpcCallPostRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(rpcId);
ArgumentException.ThrowIfNullOrEmpty(method);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Rpc.PostCall(rpcId, method, request), request ?? EmptyRpcCallPostRequest, OpenCodeJsonContext.Default.RpcCallPostRequest, RpcCallPostResponseAdapter.Instance, requestOptions, cancellationToken);
return Pipeline.ExecuteAsync(HttpMethod.Post, OpenCodeRoutes.Rpc.Call(rpcId, method, request), request ?? EmptyRpcCallPostRequest, OpenCodeJsonContext.Default.RpcCallPostRequest, RpcCallPostResponseAdapter.Instance, requestOptions, cancellationToken);
}
}
93 changes: 93 additions & 0 deletions tests/OpenCode.Sdk.Tests/Fixtures/Server/child-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const port = Number(process.env.OPENCODE_SDK_TEST_TREE_PORT);
const nonce = process.env.OPENCODE_SDK_TEST_TREE_NONCE;
const mode = process.env.OPENCODE_SDK_TEST_TREE_MODE;

if (!Number.isInteger(port) || port <= 0 || !nonce || !mode) {
throw new Error("The child-tree fixture requires its port, nonce, and mode environment.");
}

// Each peer owns its own TCP connection to the test. In particular the child does not
// inherit a parent-owned pipe or a timer: killing only the root leaves this child alive.
async function connectLease() {
const net = await import("node:net");
const socket = net.createConnection({
host: "127.0.0.1",
port: Number(process.env.OPENCODE_SDK_TEST_TREE_PORT),
});
let finishAcknowledgement;
const acknowledged = new Promise((resolve) => {
finishAcknowledgement = resolve;
let response = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
response += chunk;
const newline = response.indexOf("\n");
if (newline < 0) return;
resolve(response.slice(0, newline).trim() === "ACK");
});
});
const released = new Promise((resolve) => {
const release = () => {
finishAcknowledgement(false);
resolve();
};
socket.once("end", release);
socket.once("close", release);
socket.once("error", release);
});
await new Promise((resolve, reject) => {
socket.once("connect", resolve);
socket.once("error", reject);
});
return { socket, acknowledged, released };
}

async function runChild() {
const lease = await connectLease();
try {
lease.socket.write(`${JSON.stringify({
nonce: process.env.OPENCODE_SDK_TEST_TREE_NONCE,
role: "child",
pid: process.pid,
})}\n`);
if (!await lease.acknowledged) {
throw new Error("The child-tree lease ended without a valid acknowledgement.");
}
await lease.released;
} finally {
lease.socket.destroy();
}
}

// Establish the root connection first so the test can assign both roles without a
// connection-routing framework. The child cannot connect until after this succeeds.
const lease = await connectLease();
let child;
try {
child = Bun.spawn(
[process.execPath, "-e", `${connectLease.toString()}; await (${runChild.toString()})();`],
{ stdin: "ignore", stdout: "ignore", stderr: "ignore" },
);
lease.socket.write(`${JSON.stringify({
nonce, role: "root", pid: process.pid, childPid: child.pid,
})}\n`);
if (!await lease.acknowledged) {
throw new Error("The child-tree lease ended without a valid acknowledgement.");
}

if (mode === "invalid-line") {
console.log("hello");
} else if (mode !== "silent") {
throw new Error(`Unknown child-tree fixture mode '${mode}'.`);
}

await lease.released;
} finally {
lease.socket.destroy();
// This is the root's direct, owned Bun child. Pre-ACK rejection must reap it even
// while the independently held child lease is still open on the test side.
if (child) {
if (child.exitCode === null) child.kill();
await child.exited;
}
}
1 change: 1 addition & 0 deletions tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

<ItemGroup>
<EmbeddedResource Include="Fixtures/**/*.json" />
<EmbeddedResource Include="Fixtures/**/*.js" />
</ItemGroup>

</Project>
Loading
Loading