From f686622bc47993b540d667ce3bd6ce02f71cc422 Mon Sep 17 00:00:00 2001 From: unexplrd Date: Fri, 24 Apr 2026 19:55:33 +0300 Subject: [PATCH] Initial commit --- flake.lock | 44 ++++ flake.nix | 63 +++++ packages/desktop-package.nix | 216 ++++++++++++++++++ .../patches/build-nix-desktop-package.mjs | 97 ++++++++ packages/patches/desktop-nix-autoupdate.patch | 37 +++ packages/server-package.nix | 156 +++++++++++++ 6 files changed, 613 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 packages/desktop-package.nix create mode 100644 packages/patches/build-nix-desktop-package.mjs create mode 100644 packages/patches/desktop-nix-autoupdate.patch create mode 100644 packages/server-package.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5763c53 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776734388, + "narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "t3code": "t3code" + } + }, + "t3code": { + "flake": false, + "locked": { + "lastModified": 1776967515, + "narHash": "sha256-E5r9Ui7SKl2V6COTsNf2gSHnsae1ACrmHUutxO54Bak=", + "owner": "pingdotgg", + "repo": "t3code", + "rev": "ada410bccff144ce4cfed0e2c6e18974b045f968", + "type": "github" + }, + "original": { + "owner": "pingdotgg", + "repo": "t3code", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7823bb5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + description = "T3 Code packages for Nix"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + t3code = { + url = "github:pingdotgg/t3code"; + flake = false; + }; + }; + + outputs = { + self, + nixpkgs, + t3code, + ... + }: let + lib = nixpkgs.lib; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = lib.genAttrs systems; + in { + packages = forAllSystems ( + system: let + pkgs = import nixpkgs {inherit system;}; + t3 = pkgs.callPackage ./packages/server-package.nix {src = t3code;}; + in + { + inherit t3; + t3code = t3; + default = t3; + } + // lib.optionalAttrs (system == "x86_64-linux") { + t3code-desktop = pkgs.callPackage ./packages/desktop-package.nix {src = t3code;}; + } + ); + + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${self.packages.${system}.t3}/bin/t3"; + }; + t3 = { + type = "app"; + program = "${self.packages.${system}.t3}/bin/t3"; + }; + }); + + checks = forAllSystems ( + system: + { + t3 = self.packages.${system}.t3; + } + // lib.optionalAttrs (system == "x86_64-linux") { + t3code-desktop = self.packages.${system}.t3code-desktop; + } + ); + }; +} diff --git a/packages/desktop-package.nix b/packages/desktop-package.nix new file mode 100644 index 0000000..3753c8f --- /dev/null +++ b/packages/desktop-package.nix @@ -0,0 +1,216 @@ +{ + lib, + src, + asar, + stdenv, + bun, + copyDesktopItems, + electron_40, + gcc, + git, + gnumake, + makeDesktopItem, + makeWrapper, + node-gyp, + nodejs, + pkg-config, + python3, + writableTmpDirAsHomeHook, + xdg-utils, +}: let + desktopPackageJson = lib.importJSON "${src}/apps/desktop/package.json"; + + pname = "t3code-desktop"; + version = desktopPackageJson.version; + + workspacePreparePatched = [ + "apps/server/package.json" + "apps/web/package.json" + "packages/client-runtime/package.json" + "packages/contracts/package.json" + "packages/effect-acp/package.json" + "packages/effect-codex-app-server/package.json" + "packages/shared/package.json" + ]; + + desktopItem = makeDesktopItem { + name = "t3code"; + desktopName = "T3 Code"; + exec = "t3code %U"; + icon = "t3code"; + categories = ["Development"]; + startupWMClass = "t3code"; + }; +in + stdenv.mkDerivation (finalAttrs: { + inherit pname version; + inherit src; + + strictDeps = true; + patches = [./patches/desktop-nix-autoupdate.patch]; + + nodeModules = stdenv.mkDerivation { + pname = "${pname}-node-modules"; + inherit (finalAttrs) version src; + + impureEnvVars = + lib.fetchers.proxyImpureEnvVars + ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ + bun + gcc + gnumake + nodejs + pkg-config + python3 + writableTmpDirAsHomeHook + ]; + + dontConfigure = true; + dontFixup = true; + + postPatch = '' + for packageJson in ${lib.concatStringsSep " " workspacePreparePatched}; do + substituteInPlace "$packageJson" \ + --replace-fail '"prepare": "effect-language-service patch"' '"prepare": "true"' + done + ''; + + buildPhase = '' + runHook preBuild + + export HOME="$TMPDIR" + export BUN_INSTALL_CACHE_DIR="$(mktemp -d)" + export ELECTRON_SKIP_BINARY_DOWNLOAD=1 + export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + export npm_config_build_from_source=true + + bun install \ + --frozen-lockfile \ + --ignore-scripts \ + --linker=hoisted \ + --no-progress \ + --filter ./apps/desktop \ + --filter ./apps/server \ + --filter ./apps/web \ + --filter ./packages/client-runtime \ + --filter ./packages/contracts \ + --filter ./packages/effect-acp \ + --filter ./packages/effect-codex-app-server \ + --filter ./packages/shared + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + cp -R ./node_modules $out + + runHook postInstall + ''; + + outputHash = "sha256-mzcaRKIymMQb934wrto/mBuKt0KzncbzUQ0rzkCLlC4="; + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + }; + + nativeBuildInputs = [ + asar + bun + copyDesktopItems + gcc + gnumake + makeWrapper + node-gyp + nodejs + pkg-config + python3 + writableTmpDirAsHomeHook + ]; + + desktopItems = [desktopItem]; + + env = { + ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + npm_config_build_from_source = "true"; + npm_config_nodedir = "${nodejs}"; + }; + + configurePhase = '' + runHook preConfigure + + mkdir -p ./node_modules + cp -R ${finalAttrs.nodeModules}/. ./node_modules/ + chmod -R u+rw node_modules + if [ -d node_modules/.bin ]; then + chmod -R u+x node_modules/.bin + fi + patchShebangs node_modules + + cd node_modules/node-pty + node-gyp rebuild + node scripts/post-install.js + cd "$NIX_BUILD_TOP/$sourceRoot" + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + export HOME="$TMPDIR" + export BUN_INSTALL_CACHE_DIR="$(mktemp -d)" + + bun run build:desktop + node ${./patches/build-nix-desktop-package.mjs} --output-dir packaged-app + cp -R node_modules packaged-app/node_modules + chmod -R u+rwX packaged-app/node_modules + patchShebangs packaged-app/node_modules + find packaged-app/node_modules -xtype l -delete + rm -rf packaged-app/node_modules/electron + rm -f packaged-app/node_modules/.bin/electron + asar pack --unpack='{*.node}' packaged-app packaged-app.asar + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + install -d "$out/share/${pname}/resources" + install -Dm644 packaged-app.asar "$out/share/${pname}/resources/app.asar" + if [ -d packaged-app.asar.unpacked ]; then + cp -R packaged-app.asar.unpacked "$out/share/${pname}/resources/" + fi + + install -Dm644 apps/desktop/resources/icon.png \ + "$out/share/icons/hicolor/512x512/apps/t3code.png" + + makeWrapper ${lib.getExe electron_40} "$out/bin/t3code" \ + --set T3CODE_DISABLE_AUTO_UPDATE 1 \ + --set-default ELECTRON_FORCE_IS_PACKAGED 1 \ + --set-default ELECTRON_IS_DEV 0 \ + --prefix PATH : ${lib.makeBinPath [ + git + xdg-utils + ]} \ + --add-flags "$out/share/${pname}/resources/app.asar" + + runHook postInstall + ''; + + meta = { + description = "T3 Code desktop app"; + homepage = "https://github.com/pingdotgg/t3code"; + license = lib.licenses.mit; + mainProgram = "t3code"; + platforms = ["x86_64-linux"]; + sourceProvenance = with lib.sourceTypes; [fromSource]; + }; + }) diff --git a/packages/patches/build-nix-desktop-package.mjs b/packages/patches/build-nix-desktop-package.mjs new file mode 100644 index 0000000..b3a2be8 --- /dev/null +++ b/packages/patches/build-nix-desktop-package.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node + +import * as FS from "node:fs"; +import * as Path from "node:path"; + +class BuildNixDesktopPackageError extends Error { + constructor(message) { + super(message); + this.name = "BuildNixDesktopPackageError"; + } +} + +function parseArgs(argv) { + let outputDir; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--output-dir") { + outputDir = argv[index + 1]; + index += 1; + } + } + + if (!outputDir) { + throw new BuildNixDesktopPackageError("Missing required --output-dir argument."); + } + + return { outputDir }; +} + +function assertPathExists(path, description) { + if (!FS.existsSync(path)) { + throw new BuildNixDesktopPackageError( + `Missing ${description} at ${path}. Run 'bun run build:desktop' first.`, + ); + } +} + +function readJson(path) { + return JSON.parse(FS.readFileSync(path, "utf8")); +} + +function main() { + const { outputDir } = parseArgs(process.argv.slice(2)); + const repoRoot = process.cwd(); + const packagedAppDir = Path.resolve(repoRoot, outputDir); + + const rootPackageJson = readJson(Path.join(repoRoot, "package.json")); + const desktopPackageJson = readJson(Path.join(repoRoot, "apps/desktop/package.json")); + const serverPackageJson = readJson(Path.join(repoRoot, "apps/server/package.json")); + + const desktopDistDir = Path.join(repoRoot, "apps/desktop/dist-electron"); + const desktopResourcesDir = Path.join(repoRoot, "apps/desktop/resources"); + const serverDistDir = Path.join(repoRoot, "apps/server/dist"); + const bundledClientIndex = Path.join(serverDistDir, "client/index.html"); + + assertPathExists(desktopDistDir, "desktop bundle"); + assertPathExists(Path.join(desktopDistDir, "main.cjs"), "desktop main entry"); + assertPathExists(Path.join(desktopDistDir, "preload.cjs"), "desktop preload entry"); + assertPathExists(desktopResourcesDir, "desktop resources"); + assertPathExists(serverDistDir, "server bundle"); + assertPathExists(Path.join(serverDistDir, "bin.mjs"), "server entry"); + assertPathExists(bundledClientIndex, "bundled desktop web client"); + + FS.rmSync(packagedAppDir, { recursive: true, force: true }); + FS.mkdirSync(Path.join(packagedAppDir, "apps/desktop"), { recursive: true }); + FS.mkdirSync(Path.join(packagedAppDir, "apps/server"), { recursive: true }); + + FS.cpSync(desktopDistDir, Path.join(packagedAppDir, "apps/desktop/dist-electron"), { + recursive: true, + }); + FS.cpSync(desktopResourcesDir, Path.join(packagedAppDir, "apps/desktop/resources"), { + recursive: true, + }); + FS.cpSync(serverDistDir, Path.join(packagedAppDir, "apps/server/dist"), { + recursive: true, + }); + + const packageJson = { + name: "t3code", + version: serverPackageJson.version ?? desktopPackageJson.version, + private: true, + description: "T3 Code desktop build", + author: "T3 Tools", + main: "apps/desktop/dist-electron/main.cjs", + t3codeCommitHash: "unknown", + packageManager: rootPackageJson.packageManager, + }; + + FS.writeFileSync( + Path.join(packagedAppDir, "package.json"), + `${JSON.stringify(packageJson, null, 2)}\n`, + "utf8", + ); +} + +main(); diff --git a/packages/patches/desktop-nix-autoupdate.patch b/packages/patches/desktop-nix-autoupdate.patch new file mode 100644 index 0000000..9145379 --- /dev/null +++ b/packages/patches/desktop-nix-autoupdate.patch @@ -0,0 +1,37 @@ +diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts +index c2bb4ba1..181c4d55 100644 +--- a/apps/desktop/src/updateState.test.ts ++++ b/apps/desktop/src/updateState.test.ts +@@ -116,6 +116,19 @@ describe("getAutoUpdateDisabledReason", () => { + ).toContain("T3CODE_DISABLE_AUTO_UPDATE"); + }); + ++ it("explains that env-disabled updates can come from package-managed installs", () => { ++ expect( ++ getAutoUpdateDisabledReason({ ++ isDevelopment: false, ++ isPackaged: true, ++ platform: "linux", ++ appImage: undefined, ++ disabledByEnv: true, ++ hasUpdateFeedConfig: true, ++ }), ++ ).toContain("Nix"); ++ }); ++ + it("reports linux non-AppImage builds as disabled", () => { + expect( + getAutoUpdateDisabledReason({ +diff --git a/apps/desktop/src/updateState.ts b/apps/desktop/src/updateState.ts +index 928bb408..9ebdf331 100644 +--- a/apps/desktop/src/updateState.ts ++++ b/apps/desktop/src/updateState.ts +@@ -43,7 +43,7 @@ export function getAutoUpdateDisabledReason(args: { + return "Automatic updates are only available in packaged production builds."; + } + if (args.disabledByEnv) { +- return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; ++ return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting, which is used for package-managed installs such as Nix."; + } + if (args.platform === "linux" && !args.appImage) { + return "Automatic updates on Linux require running the AppImage build."; diff --git a/packages/server-package.nix b/packages/server-package.nix new file mode 100644 index 0000000..505126d --- /dev/null +++ b/packages/server-package.nix @@ -0,0 +1,156 @@ +{ + lib, + src, + stdenv, + bun, + makeBinaryWrapper, + node-gyp, + nodejs, + python3, + writableTmpDirAsHomeHook, +}: let + serverPackageJson = lib.importJSON "${src}/apps/server/package.json"; + + pname = "t3code-server"; + version = serverPackageJson.version; + + workspacePreparePatched = [ + "apps/server/package.json" + "apps/web/package.json" + "packages/client-runtime/package.json" + "packages/contracts/package.json" + "packages/effect-acp/package.json" + "packages/effect-codex-app-server/package.json" + "packages/shared/package.json" + ]; +in + stdenv.mkDerivation (finalAttrs: { + inherit pname version; + inherit src; + + strictDeps = true; + + nodeModules = stdenv.mkDerivation { + pname = "${pname}-node-modules"; + inherit (finalAttrs) version src; + + nativeBuildInputs = [ + bun + nodejs + writableTmpDirAsHomeHook + ]; + + dontConfigure = true; + dontFixup = true; + + postPatch = '' + for packageJson in ${lib.concatStringsSep " " workspacePreparePatched}; do + substituteInPlace "$packageJson" \ + --replace-fail '"prepare": "effect-language-service patch"' '"prepare": "true"' + done + ''; + + buildPhase = '' + runHook preBuild + + export HOME="$TMPDIR" + export BUN_INSTALL_CACHE_DIR="$(mktemp -d)" + + bun install \ + --frozen-lockfile \ + --ignore-scripts \ + --linker=hoisted \ + --no-progress \ + --filter ./apps/server \ + --filter ./apps/web \ + --filter ./packages/client-runtime \ + --filter ./packages/contracts \ + --filter ./packages/effect-acp \ + --filter ./packages/effect-codex-app-server \ + --filter ./packages/shared + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + cp -R ./node_modules $out + + runHook postInstall + ''; + + outputHash = "sha256-l0BXsHRRFPyWjdxWedAdS8K7VdXSzAfw5c+0caqzT6M="; + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + }; + + nativeBuildInputs = [ + bun + makeBinaryWrapper + node-gyp + nodejs + python3 + writableTmpDirAsHomeHook + ]; + + configurePhase = '' + runHook preConfigure + + mkdir -p ./node_modules + cp -R ${finalAttrs.nodeModules}/. ./node_modules/ + + chmod -R u+rwX node_modules + patchShebangs node_modules + + cd node_modules/node-pty + node-gyp rebuild + node scripts/post-install.js + cd "$NIX_BUILD_TOP/$sourceRoot" + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + export HOME="$TMPDIR" + export BUN_INSTALL_CACHE_DIR="$(mktemp -d)" + + bun run --cwd apps/web build + bun run --cwd apps/server build + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/libexec/t3code/apps/server" + mkdir -p "$out/libexec/t3code/apps" + mkdir -p "$out/libexec/t3code/packages" + + cp -R --no-preserve=mode node_modules "$out/libexec/t3code/" + cp -R --no-preserve=mode apps/server/dist "$out/libexec/t3code/apps/server/" + cp -R --no-preserve=mode apps/web "$out/libexec/t3code/apps/" + cp -R --no-preserve=mode packages/client-runtime "$out/libexec/t3code/packages/" + cp -R --no-preserve=mode packages/contracts "$out/libexec/t3code/packages/" + cp -R --no-preserve=mode packages/effect-acp "$out/libexec/t3code/packages/" + cp -R --no-preserve=mode packages/effect-codex-app-server "$out/libexec/t3code/packages/" + cp -R --no-preserve=mode packages/shared "$out/libexec/t3code/packages/" + + makeWrapper ${lib.getExe nodejs} "$out/bin/t3" \ + --add-flags "$out/libexec/t3code/apps/server/dist/bin.mjs" + + runHook postInstall + ''; + + meta = { + description = "t3 code web/server app"; + homepage = "https://github.com/pingdotgg/t3code"; + license = lib.licenses.mit; + mainprogram = "t3"; + platforms = lib.platforms.unix; + sourceprovenance = with lib.sourcetypes; [fromsource]; + }; + })