Skip to content
Open
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
87 changes: 87 additions & 0 deletions .github/workflows/scriptc-native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Native ScriptC binaries for t3code-server fast paths (acp-mcp-bridge, acp-mcp-call).
# Cut a prerelease: `git tag scriptc-v<version> && git push origin scriptc-v<version>`
# Or Actions → ScriptC native → Run workflow (uploads artifacts only; no empty release).
# Publishes a GitHub Release only when at least one ELF is produced with a recorded
# libnode pack. Missing scriptc or an unobtainable libnode pack skips publish.
# A present compiler that fails the compile fails the job.
name: ScriptC native

on:
workflow_dispatch:
push:
tags:
- "scriptc-v*"

permissions:
contents: write

concurrency:
group: scriptc-native-${{ github.ref }}
cancel-in-progress: true

jobs:
native:
name: Build ScriptC ACP entries
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
install-args: --filter t3...

- name: Cache assembled libnode pack
uses: actions/cache@v4
with:
path: apps/server/.scriptc-libnode
key: scriptc-libnode-0.4.7-${{ runner.os }}-${{ runner.arch }}

- name: Build ScriptC entries
id: scriptc
run: node --experimental-strip-types apps/server/scripts/scriptc-build.ts --exec --outdir dist-scriptc

- name: Skip publish when the compiler or libnode pack is missing
if: steps.scriptc.outputs.skipped == 'true'
run: |
echo "scriptc produced no binaries; not creating a GitHub Release." >> "$GITHUB_STEP_SUMMARY"
if [[ -f dist-scriptc/.skipped ]]; then
cat dist-scriptc/.skipped >> "$GITHUB_STEP_SUMMARY"
fi

- name: Smoke native ACP binaries
if: steps.scriptc.outputs.skipped != 'true'
run: node --experimental-strip-types apps/server/scripts/scriptc-smoke.ts --outdir dist-scriptc

- name: Upload native artifacts
if: steps.scriptc.outputs.skipped != 'true'
uses: actions/upload-artifact@v4
with:
name: t3code-scriptc-native
path: |
dist-scriptc/acp-mcp-bridge
dist-scriptc/acp-mcp-bridge.libnode
dist-scriptc/acp-mcp-call
dist-scriptc/acp-mcp-call.libnode
dist-scriptc/manifest.json
if-no-files-found: error

- name: Archive ELFs with beside libnode packs
if: steps.scriptc.outputs.skipped != 'true' && startsWith(github.ref, 'refs/tags/')
run: tar -C dist-scriptc -czf t3code-scriptc-native.tar.gz acp-mcp-bridge acp-mcp-bridge.libnode acp-mcp-call acp-mcp-call.libnode manifest.json

- name: Publish GitHub Release
if: steps.scriptc.outputs.skipped != 'true' && startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: Native t3code-server ${{ github.ref_name }}
prerelease: true
generate_release_notes: true
files: |
dist-scriptc/acp-mcp-bridge
dist-scriptc/acp-mcp-call
dist-scriptc/manifest.json
t3code-scriptc-native.tar.gz
3 changes: 2 additions & 1 deletion apps/desktop/src/app/DesktopManagedPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
readManagedPolicy,
WindowsRegistry,
} from "@t3tools/shared/managedPolicyFile";
import { loadFfiRs } from "@t3tools/shared/nativeFfi";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
Expand Down Expand Up @@ -47,7 +48,7 @@ export const layer = Layer.effect(
);
return DesktopManagedPolicy.of({ read });
}),
).pipe(Layer.provide(layerWindowsRegistry(() => import("ffi-rs"))));
).pipe(Layer.provide(layerWindowsRegistry(loadFfiRs)));

/** A machine with `policy` deployed, one whose policy cannot be used, or none. */
export const layerTest = (policy?: ManagedPolicyDocument | ManagedPolicyFileError) =>
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/electron/WindowsForeground.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { loadFfiRs } from "@t3tools/shared/nativeFfi";

export interface WindowsForegroundApi {
readonly getCurrentThreadId: () => number;
readonly getForegroundWindow: () => bigint;
Expand Down Expand Up @@ -67,7 +69,7 @@ let windowsForegroundApiPromise: Promise<WindowsForegroundApi> | undefined;
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;

export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> {
windowsForegroundApiPromise ??= import("ffi-rs").then(({ DataType, load, open }) => {
windowsForegroundApiPromise ??= loadFfiRs().then(({ DataType, load, open }) => {
const kernel32 = "t3-kernel32";
const user32 = "t3-user32";
open({ library: kernel32, path: "kernel32.dll" });
Expand Down
3 changes: 3 additions & 0 deletions apps/server/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
tmp/
dist-scriptc/
.scriptc-libnode/
**/.scriptc/
3 changes: 3 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"dev:built": "vp run dev",
"build:bundle": "vp pack",
"build:exe": "node scripts/cli.ts build-exe",
"build:scriptc": "node --experimental-strip-types scripts/scriptc-build.ts",
"obtain:scriptc-libnode": "node --experimental-strip-types scripts/scriptc-libnode.ts",
"start": "node dist/bin.mjs",
"typecheck": "tsc --noEmit",
"test": "vp test run",
Expand Down Expand Up @@ -95,6 +97,7 @@
"effect-acp": "workspace:*",
"effect-codex-app-server": "workspace:*",
"graphql": "16.14.1",
"scriptc": "0.4.7",
"vite-plus": "catalog:"
},
"engines": {
Expand Down
233 changes: 233 additions & 0 deletions apps/server/scripts/scriptc-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";
import { expect, it } from "vite-plus/test";
import { libnodePackPackage, hostLibnodeTarget } from "./scriptc-libnode.ts";

const scriptPath = NodeURL.fileURLToPath(new URL("./scriptc-build.ts", import.meta.url));

function run(
args: ReadonlyArray<string>,
env: Record<string, string | undefined> = {},
): ReturnType<typeof NodeChildProcess.spawnSync> & { stdout: string } {
return NodeChildProcess.spawnSync(
process.execPath,
["--experimental-strip-types", scriptPath, ...args],
{
encoding: "utf8",
env: { ...process.env, SCRIPTC_LIBNODE_OBTAIN: "0", ...env },
},
);
}

function writeStubPack(directory: string): string {
const pack = NodePath.join(directory, "libnode-pack");
NodeFS.mkdirSync(pack, { recursive: true });
NodeFS.writeFileSync(NodePath.join(pack, "libnode.so.137"), "libnode\n");
NodeFS.writeFileSync(NodePath.join(pack, "libscr_node_bridge.so"), "bridge\n");
NodeFS.writeFileSync(
NodePath.join(pack, "manifest.json"),
`${JSON.stringify(
{
schema: "scriptc.libnode-pack.v1",
nodeVersion: "24.22.1",
moduleVersion: 137,
bridgeAbi: 10,
platform: "linux",
arch: "x64",
libnode: "libnode.so.137",
bridge: "libscr_node_bridge.so",
files: {
"libnode.so.137": { size: 8, sha256: "00".repeat(32) },
"libscr_node_bridge.so": { size: 7, sha256: "00".repeat(32) },
},
},
null,
2,
)}\n`,
);
return pack;
}

it("lists ACP ScriptC entries and their sources", () => {
const result = run([]);
expect(result.status).toBe(0);
const listed = JSON.parse(result.stdout) as {
entries: ReadonlyArray<{ name: string; sourcePath: string }>;
};
expect(listed.entries.map((entry) => entry.name)).toEqual(["acp-mcp-bridge", "acp-mcp-call"]);
for (const entry of listed.entries) {
expect(NodeFS.existsSync(entry.sourcePath)).toBe(true);
}
});

it("skips --exec when scriptc is missing instead of writing binaries", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const result = run(["--exec", "--outdir", outdir], {
SCRIPTC_BINARY: NodePath.join(outdir, "does-not-exist"),
GITHUB_OUTPUT: githubOutput,
});
expect(result.status).toBe(0);
expect(NodeFS.existsSync(NodePath.join(outdir, ".skipped"))).toBe(true);
expect(NodeFS.existsSync(NodePath.join(outdir, "acp-mcp-bridge"))).toBe(false);
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=true");
});

it("fails --exec when the installed scriptc does not match the pin", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const fakeBin = NodePath.join(outdir, "scriptc");
NodeFS.writeFileSync(
fakeBin,
'#!/bin/sh\nif [ "$1" = "--version" ]; then echo 0.0.0; exit 0; fi\nexit 1\n',
);
NodeFS.chmodSync(fakeBin, 0o755);
const result = run(["--exec", "--outdir", outdir], {
SCRIPTC_BINARY: fakeBin,
GITHUB_OUTPUT: githubOutput,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("0.0.0");
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=true");
expect(NodeFS.existsSync(NodePath.join(outdir, "acp-mcp-bridge"))).toBe(false);
});

it("treats a bare -- as an argument separator, not an entry name", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const result = run(["--", "--exec", "--outdir", outdir], {
SCRIPTC_BINARY: NodePath.join(outdir, "does-not-exist"),
});
expect(result.status).toBe(0);
expect(NodeFS.existsSync(NodePath.join(outdir, ".skipped"))).toBe(true);
});

it("skips --exec when no libnode pack can be recorded", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const fakeBin = NodePath.join(outdir, "scriptc");
NodeFS.writeFileSync(
fakeBin,
'#!/bin/sh\nif [ "$1" = "--version" ]; then echo 0.4.7; exit 0; fi\nexit 1\n',
);
NodeFS.chmodSync(fakeBin, 0o755);
const result = run(["--exec", "--outdir", outdir, "acp-mcp-call"], {
SCRIPTC_BINARY: fakeBin,
GITHUB_OUTPUT: githubOutput,
SCRIPTC_LIBNODE: "",
SCRIPTC_LIBNODE_CACHE: NodePath.join(outdir, "missing-pack"),
SCRIPTC_LIBNODE_OBTAIN: "0",
});
expect(result.status).toBe(0);
expect(NodeFS.existsSync(NodePath.join(outdir, ".skipped"))).toBe(true);
expect(NodeFS.existsSync(NodePath.join(outdir, "acp-mcp-call"))).toBe(false);
expect(result.stderr).toContain("libnode");
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=true");
});

it("skips --exec when the compiler wrote an ELF that did not record a libnode pack", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const pack = writeStubPack(outdir);
const fakeBin = NodePath.join(outdir, "scriptc");
NodeFS.writeFileSync(
fakeBin,
`#!/bin/sh
if [ "$1" = "--version" ]; then echo 0.4.7; exit 0; fi
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "--out" ]; then out="$2"; shift 2; continue; fi
shift
done
printf '%s\\n' '#!/bin/sh' > "$out"
printf '%s\\n' 'echo "scriptc: this program calls into Node.js (@scriptc/node), but no libnode pack was recorded at build time; set SCRIPTC_LIBNODE to a pack directory" >&2' >> "$out"
printf '%s\\n' 'exit 1' >> "$out"
chmod +x "$out"
exit 0
`,
);
NodeFS.chmodSync(fakeBin, 0o755);
const result = run(["--exec", "--outdir", outdir, "acp-mcp-call"], {
SCRIPTC_BINARY: fakeBin,
GITHUB_OUTPUT: githubOutput,
SCRIPTC_LIBNODE: pack,
});
expect(result.status).toBe(0);
expect(NodeFS.existsSync(NodePath.join(outdir, ".skipped"))).toBe(true);
expect(result.stderr).toContain("did not record a libnode pack");
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=true");
});

it("invokes the pinned compiler with --out and records a beside libnode pack", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const argsLog = NodePath.join(outdir, "args.log");
const envLog = NodePath.join(outdir, "env.log");
const pack = writeStubPack(outdir);
const fakeBin = NodePath.join(outdir, "scriptc");
NodeFS.writeFileSync(
fakeBin,
`#!/bin/sh
if [ "$1" = "--version" ]; then echo 0.4.7; exit 0; fi
printf '%s\\n' "$@" > ${JSON.stringify(argsLog)}
printf '%s\\n' "$SCRIPTC_LIBNODE" > ${JSON.stringify(envLog)}
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "--out" ]; then out="$2"; shift 2; continue; fi
shift
done
if [ -z "$out" ]; then echo "missing --out" >&2; exit 1; fi
: > "$out"
exit 0
`,
);
NodeFS.chmodSync(fakeBin, 0o755);
const result = run(["--exec", "--outdir", outdir, "acp-mcp-call"], {
SCRIPTC_BINARY: fakeBin,
GITHUB_OUTPUT: githubOutput,
SCRIPTC_LIBNODE: pack,
});
expect(result.status).toBe(0);
const args = NodeFS.readFileSync(argsLog, "utf8");
expect(args).toContain("--out");
expect(args).not.toContain("--outfile");
expect(args).toContain("--libnode");
expect(args).toContain("beside");
expect(NodeFS.readFileSync(envLog, "utf8").trim()).toBe(pack);
expect(NodeFS.existsSync(NodePath.join(outdir, "acp-mcp-call"))).toBe(true);
expect(JSON.parse(NodeFS.readFileSync(NodePath.join(outdir, "manifest.json"), "utf8"))).toEqual({
skipped: false,
scriptcVersion: "0.4.7",
files: [NodePath.join(outdir, "acp-mcp-call")],
libnode: {
recorded: true,
placement: "path",
directory: pack,
source: "env",
packageName: libnodePackPackage(hostLibnodeTarget()),
nodeVersion: "24.22.1",
platform: "linux",
arch: "x64",
schema: "scriptc.libnode-pack.v1",
},
});
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=false");
});

it("skips --exec when scriptc cannot report its version", () => {
const outdir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-scriptc-build-"));
const githubOutput = NodePath.join(outdir, "github-output");
const fakeBin = NodePath.join(outdir, "scriptc");
NodeFS.writeFileSync(fakeBin, "#!/bin/sh\nexit 3\n");
NodeFS.chmodSync(fakeBin, 0o755);
const result = run(["--exec", "--outdir", outdir], {
SCRIPTC_BINARY: fakeBin,
GITHUB_OUTPUT: githubOutput,
});
expect(result.status).toBe(0);
expect(NodeFS.existsSync(NodePath.join(outdir, ".skipped"))).toBe(true);
expect(NodeFS.readFileSync(githubOutput, "utf8")).toContain("skipped=true");
});
Loading
Loading