Initial commit

This commit is contained in:
2026-04-24 19:55:33 +03:00
commit f686622bc4
6 changed files with 613 additions and 0 deletions
Generated
+44
View File
@@ -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
}
+63
View File
@@ -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;
}
);
};
}
+216
View File
@@ -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];
};
})
@@ -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();
@@ -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.";
+156
View File
@@ -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];
};
})