authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-16 22:14:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-16 22:14:35-07:00
log8dba2d6484e283a4211b53fe6d3273e6e4c962d0
tree4b034bc8cee601bddad403105d45a96abf06e061
parented6b7fbf27973c8a5180e7074ba448d912c3cce4
signature Signed by PGP key B5690EEEBB952194

Adopt Node-native TypeScript for desktop and server (#2098)


140 files changed, 2071 insertions(+), 1526 deletions(-)

.github/workflows/ci.yml+2-2
...@@ -71,8 +71,8 @@ jobs:...@@ -71,8 +71,8 @@ jobs:
7171
72 - name: Verify preload bundle output72 - name: Verify preload bundle output
73 run: |73 run: |
74 test -f apps/desktop/dist-electron/preload.js74 test -f apps/desktop/dist-electron/preload.cjs
75 grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js75 grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.cjs
7676
77 release_smoke:77 release_smoke:
78 name: Release Smoke78 name: Release Smoke
apps/desktop/package.json+4-3
...@@ -2,13 +2,14 @@...@@ -2,13 +2,14 @@
2 "name": "@t3tools/desktop",2 "name": "@t3tools/desktop",
3 "version": "0.0.20",3 "version": "0.0.20",
4 "private": true,4 "private": true,
5 "main": "dist-electron/main.js",5 "type": "module",
6 "main": "dist-electron/main.cjs",
6 "scripts": {7 "scripts": {
7 "dev": "bun run --parallel dev:bundle dev:electron",8 "dev": "bun run --parallel dev:bundle dev:electron",
8 "dev:bundle": "tsdown --watch",9 "dev:bundle": "tsdown --watch",
9 "dev:electron": "bun run scripts/dev-electron.mjs",10 "dev:electron": "node scripts/dev-electron.mjs",
10 "build": "tsdown",11 "build": "tsdown",
11 "start": "bun run scripts/start-electron.mjs",12 "start": "node scripts/start-electron.mjs",
12 "typecheck": "tsc --noEmit",13 "typecheck": "tsc --noEmit",
13 "test": "vitest run --passWithNoTests",14 "test": "vitest run --passWithNoTests",
14 "smoke-test": "node scripts/smoke-test.mjs"15 "smoke-test": "node scripts/smoke-test.mjs"
apps/desktop/scripts/dev-electron.mjs+4-4
...@@ -17,12 +17,12 @@ if (!Number.isInteger(port) || port <= 0) {...@@ -17,12 +17,12 @@ if (!Number.isInteger(port) || port <= 0) {
17}17}
1818
19const requiredFiles = [19const requiredFiles = [
20 "dist-electron/main.js",20 "dist-electron/main.cjs",
21 "dist-electron/preload.js",21 "dist-electron/preload.cjs",
22 "../server/dist/bin.mjs",22 "../server/dist/bin.mjs",
23];23];
24const watchedDirectories = [24const watchedDirectories = [
25 { directory: "dist-electron", files: new Set(["main.js", "preload.js"]) },25 { directory: "dist-electron", files: new Set(["main.cjs", "preload.cjs"]) },
26 { directory: "../server/dist", files: new Set(["bin.mjs"]) },26 { directory: "../server/dist", files: new Set(["bin.mjs"]) },
27];27];
28const forcedShutdownTimeoutMs = 1_500;28const forcedShutdownTimeoutMs = 1_500;
...@@ -69,7 +69,7 @@ function startApp() {...@@ -69,7 +69,7 @@ function startApp() {
6969
70 const app = spawn(70 const app = spawn(
71 resolveElectronPath(),71 resolveElectronPath(),
72 [`--t3code-dev-root=${desktopDir}`, "dist-electron/main.js"],72 [`--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"],
73 {73 {
74 cwd: desktopDir,74 cwd: desktopDir,
75 env: childEnv,75 env: childEnv,
apps/desktop/scripts/electron-launcher.mjs+7-37
...@@ -8,7 +8,6 @@ import {...@@ -8,7 +8,6 @@ import {
8 mkdirSync,8 mkdirSync,
9 mkdtempSync,9 mkdtempSync,
10 readFileSync,10 readFileSync,
11 readdirSync,
12 rmSync,11 rmSync,
13 statSync,12 statSync,
14 writeFileSync,13 writeFileSync,
...@@ -20,7 +19,7 @@ import { fileURLToPath } from "node:url";...@@ -20,7 +19,7 @@ import { fileURLToPath } from "node:url";
20const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);19const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
21const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";20const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
22const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";21const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";
23const LAUNCHER_VERSION = 1;22const LAUNCHER_VERSION = 2;
2423
25const __dirname = dirname(fileURLToPath(import.meta.url));24const __dirname = dirname(fileURLToPath(import.meta.url));
26export const desktopDir = resolve(__dirname, "..");25export const desktopDir = resolve(__dirname, "..");
...@@ -121,40 +120,6 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) {...@@ -121,40 +120,6 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) {
121 copyFileSync(iconPath, join(resourcesDir, "electron.icns"));120 copyFileSync(iconPath, join(resourcesDir, "electron.icns"));
122}121}
123122
124function patchHelperBundleInfoPlists(appBundlePath) {
125 const frameworksDir = join(appBundlePath, "Contents", "Frameworks");
126 if (!existsSync(frameworksDir)) {
127 return;
128 }
129
130 for (const entry of readdirSync(frameworksDir, { withFileTypes: true })) {
131 if (!entry.isDirectory() || !entry.name.endsWith(".app")) {
132 continue;
133 }
134 if (!entry.name.startsWith("Electron Helper")) {
135 continue;
136 }
137
138 const helperPlistPath = join(frameworksDir, entry.name, "Contents", "Info.plist");
139 if (!existsSync(helperPlistPath)) {
140 continue;
141 }
142
143 const suffix = entry.name.replace("Electron Helper", "").replace(".app", "").trim();
144 const helperName = suffix
145 ? `${APP_DISPLAY_NAME} Helper ${suffix}`
146 : `${APP_DISPLAY_NAME} Helper`;
147 const helperIdSuffix = suffix.replace(/[()]/g, "").trim().toLowerCase().replace(/\s+/g, "-");
148 const helperBundleId = helperIdSuffix
149 ? `${APP_BUNDLE_ID}.helper.${helperIdSuffix}`
150 : `${APP_BUNDLE_ID}.helper`;
151
152 setPlistString(helperPlistPath, "CFBundleDisplayName", helperName);
153 setPlistString(helperPlistPath, "CFBundleName", helperName);
154 setPlistString(helperPlistPath, "CFBundleIdentifier", helperBundleId);
155 }
156}
157
158function readJson(path) {123function readJson(path) {
159 try {124 try {
160 return JSON.parse(readFileSync(path, "utf8"));125 return JSON.parse(readFileSync(path, "utf8"));
...@@ -192,7 +157,6 @@ function buildMacLauncher(electronBinaryPath) {...@@ -192,7 +157,6 @@ function buildMacLauncher(electronBinaryPath) {
192 rmSync(targetAppBundlePath, { recursive: true, force: true });157 rmSync(targetAppBundlePath, { recursive: true, force: true });
193 cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });158 cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
194 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);159 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
195 patchHelperBundleInfoPlists(targetAppBundlePath);
196 writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);160 writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
197161
198 return targetBinaryPath;162 return targetBinaryPath;
...@@ -206,5 +170,11 @@ export function resolveElectronPath() {...@@ -206,5 +170,11 @@ export function resolveElectronPath() {
206 return electronBinaryPath;170 return electronBinaryPath;
207 }171 }
208172
173 // Dev launches do not need a renamed app bundle badly enough to risk breaking
174 // Electron helper resource lookup on macOS.
175 if (isDevelopment) {
176 return electronBinaryPath;
177 }
178
209 return buildMacLauncher(electronBinaryPath);179 return buildMacLauncher(electronBinaryPath);
210}180}
apps/desktop/scripts/smoke-test.mjs+1-1
...@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";...@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5const __dirname = dirname(fileURLToPath(import.meta.url));5const __dirname = dirname(fileURLToPath(import.meta.url));
6const desktopDir = resolve(__dirname, "..");6const desktopDir = resolve(__dirname, "..");
7const electronBin = resolve(desktopDir, "node_modules/.bin/electron");7const electronBin = resolve(desktopDir, "node_modules/.bin/electron");
8const mainJs = resolve(desktopDir, "dist-electron/main.js");8const mainJs = resolve(desktopDir, "dist-electron/main.cjs");
99
10console.log("\nLaunching Electron smoke test...");10console.log("\nLaunching Electron smoke test...");
1111
apps/desktop/scripts/start-electron.mjs+1-1
...@@ -5,7 +5,7 @@ import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";...@@ -5,7 +5,7 @@ import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
5const childEnv = { ...process.env };5const childEnv = { ...process.env };
6delete childEnv.ELECTRON_RUN_AS_NODE;6delete childEnv.ELECTRON_RUN_AS_NODE;
77
8const child = spawn(resolveElectronPath(), ["dist-electron/main.js"], {8const child = spawn(resolveElectronPath(), ["dist-electron/main.cjs"], {
9 stdio: "inherit",9 stdio: "inherit",
10 cwd: desktopDir,10 cwd: desktopDir,
11 env: childEnv,11 env: childEnv,
apps/desktop/src/appBranding.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding";3import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding.ts";
44
5describe("resolveDesktopAppStageLabel", () => {5describe("resolveDesktopAppStageLabel", () => {
6 it("uses Dev in desktop development", () => {6 it("uses Dev in desktop development", () => {
apps/desktop/src/appBranding.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";1import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";
22
3import { isNightlyDesktopVersion } from "./updateChannels";3import { isNightlyDesktopVersion } from "./updateChannels.ts";
44
5const APP_BASE_NAME = "T3 Code";5const APP_BASE_NAME = "T3 Code";
66
apps/desktop/src/backendPort.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it, vi } from "vitest";1import { describe, expect, it, vi } from "vitest";
22
3import { resolveDesktopBackendPort } from "./backendPort";3import { resolveDesktopBackendPort } from "./backendPort.ts";
44
5describe("resolveDesktopBackendPort", () => {5describe("resolveDesktopBackendPort", () => {
6 it("returns the starting port when it is available", async () => {6 it("returns the starting port when it is available", async () => {
apps/desktop/src/backendReadiness.test.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 BackendReadinessAbortedError,4 BackendReadinessAbortedError,
5 isBackendReadinessAborted,5 isBackendReadinessAborted,
6 waitForHttpReady,6 waitForHttpReady,
7} from "./backendReadiness";7} from "./backendReadiness.ts";
88
9describe("waitForHttpReady", () => {9describe("waitForHttpReady", () => {
10 it("returns once the backend serves the requested readiness path", async () => {10 it("returns once the backend serves the requested readiness path", async () => {
apps/desktop/src/clientPersistence.test.ts+1-1
...@@ -18,7 +18,7 @@ import {...@@ -18,7 +18,7 @@ import {
18 writeSavedEnvironmentRegistry,18 writeSavedEnvironmentRegistry,
19 writeSavedEnvironmentSecret,19 writeSavedEnvironmentSecret,
20 type DesktopSecretStorage,20 type DesktopSecretStorage,
21} from "./clientPersistence";21} from "./clientPersistence.ts";
2222
23const tempDirectories: string[] = [];23const tempDirectories: string[] = [];
2424
apps/desktop/src/confirmDialog.test.ts+1-1
...@@ -11,7 +11,7 @@ vi.mock("electron", () => ({...@@ -11,7 +11,7 @@ vi.mock("electron", () => ({
11 },11 },
12}));12}));
1313
14import { showDesktopConfirmDialog } from "./confirmDialog";14import { showDesktopConfirmDialog } from "./confirmDialog.ts";
1515
16describe("showDesktopConfirmDialog", () => {16describe("showDesktopConfirmDialog", () => {
17 beforeEach(() => {17 beforeEach(() => {
apps/desktop/src/desktopSettings.test.ts+1-1
...@@ -11,7 +11,7 @@ import {...@@ -11,7 +11,7 @@ import {
11 setDesktopServerExposurePreference,11 setDesktopServerExposurePreference,
12 setDesktopUpdateChannelPreference,12 setDesktopUpdateChannelPreference,
13 writeDesktopSettings,13 writeDesktopSettings,
14} from "./desktopSettings";14} from "./desktopSettings.ts";
1515
16const tempDirectories: string[] = [];16const tempDirectories: string[] = [];
1717
apps/desktop/src/desktopSettings.ts+1-1
...@@ -2,7 +2,7 @@ import * as FS from "node:fs";...@@ -2,7 +2,7 @@ import * as FS from "node:fs";
2import * as Path from "node:path";2import * as Path from "node:path";
3import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";3import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";
44
5import { resolveDefaultDesktopUpdateChannel } from "./updateChannels";5import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts";
66
7export interface DesktopSettings {7export interface DesktopSettings {
8 readonly serverExposureMode: DesktopServerExposureMode;8 readonly serverExposureMode: DesktopServerExposureMode;
apps/desktop/src/main.ts+14-14
...@@ -36,14 +36,14 @@ import { autoUpdater } from "electron-updater";...@@ -36,14 +36,14 @@ import { autoUpdater } from "electron-updater";
36import type { ContextMenuItem } from "@t3tools/contracts";36import type { ContextMenuItem } from "@t3tools/contracts";
37import { RotatingFileSink } from "@t3tools/shared/logging";37import { RotatingFileSink } from "@t3tools/shared/logging";
38import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";38import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
39import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort";39import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort.ts";
40import {40import {
41 DEFAULT_DESKTOP_SETTINGS,41 DEFAULT_DESKTOP_SETTINGS,
42 readDesktopSettings,42 readDesktopSettings,
43 setDesktopServerExposurePreference,43 setDesktopServerExposurePreference,
44 setDesktopUpdateChannelPreference,44 setDesktopUpdateChannelPreference,
45 writeDesktopSettings,45 writeDesktopSettings,
46} from "./desktopSettings";46} from "./desktopSettings.ts";
47import {47import {
48 readClientSettings,48 readClientSettings,
49 readSavedEnvironmentRegistry,49 readSavedEnvironmentRegistry,
...@@ -52,14 +52,14 @@ import {...@@ -52,14 +52,14 @@ import {
52 writeClientSettings,52 writeClientSettings,
53 writeSavedEnvironmentRegistry,53 writeSavedEnvironmentRegistry,
54 writeSavedEnvironmentSecret,54 writeSavedEnvironmentSecret,
55} from "./clientPersistence";55} from "./clientPersistence.ts";
56import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness";56import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness.ts";
57import { showDesktopConfirmDialog } from "./confirmDialog";57import { showDesktopConfirmDialog } from "./confirmDialog.ts";
58import { resolveDesktopServerExposure } from "./serverExposure";58import { resolveDesktopServerExposure } from "./serverExposure.ts";
59import { syncShellEnvironment } from "./syncShellEnvironment";59import { syncShellEnvironment } from "./syncShellEnvironment.ts";
60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState.ts";
61import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels";61import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels.ts";
62import { ServerListeningDetector } from "./serverListeningDetector";62import { ServerListeningDetector } from "./serverListeningDetector.ts";
63import {63import {
64 createInitialDesktopUpdateState,64 createInitialDesktopUpdateState,
65 reduceDesktopUpdateStateOnCheckFailure,65 reduceDesktopUpdateStateOnCheckFailure,
...@@ -71,9 +71,9 @@ import {...@@ -71,9 +71,9 @@ import {
71 reduceDesktopUpdateStateOnInstallFailure,71 reduceDesktopUpdateStateOnInstallFailure,
72 reduceDesktopUpdateStateOnNoUpdate,72 reduceDesktopUpdateStateOnNoUpdate,
73 reduceDesktopUpdateStateOnUpdateAvailable,73 reduceDesktopUpdateStateOnUpdateAvailable,
74} from "./updateMachine";74} from "./updateMachine.ts";
75import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";75import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch.ts";
76import { resolveDesktopAppBranding } from "./appBranding";76import { resolveDesktopAppBranding } from "./appBranding.ts";
7777
78syncShellEnvironment();78syncShellEnvironment();
7979
...@@ -1967,7 +1967,7 @@ function createWindow(): BrowserWindow {...@@ -1967,7 +1967,7 @@ function createWindow(): BrowserWindow {
1967 title: APP_DISPLAY_NAME,1967 title: APP_DISPLAY_NAME,
1968 ...getWindowTitleBarOptions(),1968 ...getWindowTitleBarOptions(),
1969 webPreferences: {1969 webPreferences: {
1970 preload: Path.join(__dirname, "preload.js"),1970 preload: Path.join(__dirname, "preload.cjs"),
1971 contextIsolation: true,1971 contextIsolation: true,
1972 nodeIntegration: false,1972 nodeIntegration: false,
1973 sandbox: true,1973 sandbox: true,
apps/desktop/src/runtimeArch.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";3import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch.ts";
44
5describe("resolveDesktopRuntimeInfo", () => {5describe("resolveDesktopRuntimeInfo", () => {
6 it("detects Rosetta-translated Intel builds on Apple Silicon", () => {6 it("detects Rosetta-translated Intel builds on Apple Silicon", () => {
apps/desktop/src/serverExposure.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { resolveDesktopServerExposure, resolveLanAdvertisedHost } from "./serverExposure";3import { resolveDesktopServerExposure, resolveLanAdvertisedHost } from "./serverExposure.ts";
44
5describe("resolveLanAdvertisedHost", () => {5describe("resolveLanAdvertisedHost", () => {
6 it("prefers an explicit host override", () => {6 it("prefers an explicit host override", () => {
apps/desktop/src/serverListeningDetector.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { ServerListeningDetector } from "./serverListeningDetector";3import { ServerListeningDetector } from "./serverListeningDetector.ts";
44
5describe("ServerListeningDetector", () => {5describe("ServerListeningDetector", () => {
6 it("resolves when the server logs the listening line", async () => {6 it("resolves when the server logs the listening line", async () => {
apps/desktop/src/syncShellEnvironment.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it, vi } from "vitest";1import { describe, expect, it, vi } from "vitest";
22
3import { syncShellEnvironment } from "./syncShellEnvironment";3import { syncShellEnvironment } from "./syncShellEnvironment.ts";
44
5describe("syncShellEnvironment", () => {5describe("syncShellEnvironment", () => {
6 it("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on macOS", () => {6 it("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on macOS", () => {
apps/desktop/src/syncShellEnvironment.ts+1-1
...@@ -3,8 +3,8 @@ import {...@@ -3,8 +3,8 @@ import {
3 mergePathEntries,3 mergePathEntries,
4 readPathFromLaunchctl,4 readPathFromLaunchctl,
5 readEnvironmentFromLoginShell,5 readEnvironmentFromLoginShell,
6 ShellEnvironmentReader,
7} from "@t3tools/shared/shell";6} from "@t3tools/shared/shell";
7import type { ShellEnvironmentReader } from "@t3tools/shared/shell";
88
9const LOGIN_SHELL_ENV_NAMES = [9const LOGIN_SHELL_ENV_NAMES = [
10 "PATH",10 "PATH",
apps/desktop/src/updateChannels.test.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 doesVersionMatchDesktopUpdateChannel,4 doesVersionMatchDesktopUpdateChannel,
5 isNightlyDesktopVersion,5 isNightlyDesktopVersion,
6 resolveDefaultDesktopUpdateChannel,6 resolveDefaultDesktopUpdateChannel,
7} from "./updateChannels";7} from "./updateChannels.ts";
88
9describe("isNightlyDesktopVersion", () => {9describe("isNightlyDesktopVersion", () => {
10 it("detects packaged nightly versions", () => {10 it("detects packaged nightly versions", () => {
apps/desktop/src/updateMachine.test.ts+1-1
...@@ -11,7 +11,7 @@ import {...@@ -11,7 +11,7 @@ import {
11 reduceDesktopUpdateStateOnInstallFailure,11 reduceDesktopUpdateStateOnInstallFailure,
12 reduceDesktopUpdateStateOnNoUpdate,12 reduceDesktopUpdateStateOnNoUpdate,
13 reduceDesktopUpdateStateOnUpdateAvailable,13 reduceDesktopUpdateStateOnUpdateAvailable,
14} from "./updateMachine";14} from "./updateMachine.ts";
1515
16const runtimeInfo = {16const runtimeInfo = {
17 hostArch: "x64",17 hostArch: "x64",
apps/desktop/src/updateMachine.ts+1-1
...@@ -4,7 +4,7 @@ import type {...@@ -4,7 +4,7 @@ import type {
4 DesktopUpdateState,4 DesktopUpdateState,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
66
7import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState";7import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState.ts";
88
9export function createInitialDesktopUpdateState(9export function createInitialDesktopUpdateState(
10 currentVersion: string,10 currentVersion: string,
apps/desktop/src/updateState.test.ts+1-1
...@@ -6,7 +6,7 @@ import {...@@ -6,7 +6,7 @@ import {
6 getAutoUpdateDisabledReason,6 getAutoUpdateDisabledReason,
7 nextStatusAfterDownloadFailure,7 nextStatusAfterDownloadFailure,
8 shouldBroadcastDownloadProgress,8 shouldBroadcastDownloadProgress,
9} from "./updateState";9} from "./updateState.ts";
1010
11const baseState: DesktopUpdateState = {11const baseState: DesktopUpdateState = {
12 enabled: true,12 enabled: true,
apps/desktop/tsconfig.json+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "electron"],5 "types": ["node", "electron"],
6 "lib": ["ES2023", "DOM", "esnext.disposable"]6 "lib": ["ESNext", "DOM", "esnext.disposable"]
7 },7 },
8 "include": ["src", "tsdown.config.ts"]8 "include": ["src", "tsdown.config.ts"]
9}9}
apps/desktop/tsdown.config.ts+1-1
...@@ -4,7 +4,7 @@ const shared = {...@@ -4,7 +4,7 @@ const shared = {
4 format: "cjs" as const,4 format: "cjs" as const,
5 outDir: "dist-electron",5 outDir: "dist-electron",
6 sourcemap: true,6 sourcemap: true,
7 outExtensions: () => ({ js: ".js" }),7 outExtensions: () => ({ js: ".cjs" }),
8};8};
99
10export default defineConfig([10export default defineConfig([
apps/server/package.json+2-1
...@@ -15,8 +15,9 @@...@@ -15,8 +15,9 @@
15 ],15 ],
16 "type": "module",16 "type": "module",
17 "scripts": {17 "scripts": {
18 "dev": "bun run src/bin.ts",18 "dev": "node --watch src/bin.ts",
19 "build": "node scripts/cli.ts build",19 "build": "node scripts/cli.ts build",
20 "build:bundle": "tsdown",
20 "start": "node dist/bin.mjs",21 "start": "node dist/bin.mjs",
21 "prepare": "effect-language-service patch",22 "prepare": "effect-language-service patch",
22 "typecheck": "tsc --noEmit",23 "typecheck": "tsc --noEmit",
apps/server/scripts/cli.ts+15-20
...@@ -147,13 +147,13 @@ const buildCmd = Command.make(...@@ -147,13 +147,13 @@ const buildCmd = Command.make(
147147
148 yield* Effect.log("[cli] Running tsdown...");148 yield* Effect.log("[cli] Running tsdown...");
149 yield* runCommand(149 yield* runCommand(
150 ChildProcess.make({150 ChildProcess.make(process.execPath, ["--run", "build:bundle"], {
151 cwd: serverDir,151 cwd: serverDir,
152 stdout: config.verbose ? "inherit" : "ignore",152 stdout: config.verbose ? "inherit" : "ignore",
153 stderr: "inherit",153 stderr: "inherit",
154 // Windows needs shell mode to resolve .cmd shims (e.g. bun.cmd).154 // Windows needs shell mode to resolve `.cmd` shims on PATH.
155 shell: process.platform === "win32",155 shell: process.platform === "win32",
156 })`bun tsdown`,156 }),
157 );157 );
158158
159 const webDist = path.join(repoRoot, "apps/web/dist");159 const webDist = path.join(repoRoot, "apps/web/dist");
...@@ -203,10 +203,8 @@ const publishCmd = Command.make(...@@ -203,10 +203,8 @@ const publishCmd = Command.make(
203 }203 }
204204
205 yield* Effect.acquireUseRelease(205 yield* Effect.acquireUseRelease(
206 // Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts206 // Acquire: backup package.json, resolve catalog dependencies, and strip devDependencies/scripts
207 Effect.gen(function* () {207 Effect.gen(function* () {
208 // Resolve catalog dependencies before any file mutations. If this throws,
209 // acquire fails and no release hook runs, so filesystem must still be untouched.
210 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);208 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
211 const pkg: PackageJson = {209 const pkg: PackageJson = {
212 name: serverPackageJson.name,210 name: serverPackageJson.name,
...@@ -216,25 +214,22 @@ const publishCmd = Command.make(...@@ -216,25 +214,22 @@ const publishCmd = Command.make(
216 version,214 version,
217 engines: serverPackageJson.engines,215 engines: serverPackageJson.engines,
218 files: serverPackageJson.files,216 files: serverPackageJson.files,
219 dependencies: serverPackageJson.dependencies,217 dependencies: resolveCatalogDependencies(
220 overrides: rootPackageJson.overrides,218 serverPackageJson.dependencies,
219 rootPackageJson.workspaces.catalog,
220 "apps/server",
221 ),
222 overrides: resolveCatalogDependencies(
223 rootPackageJson.overrides,
224 rootPackageJson.workspaces.catalog,
225 "apps/server",
226 ),
221 };227 };
222228
223 pkg.dependencies = resolveCatalogDependencies(
224 pkg.dependencies,
225 rootPackageJson.workspaces.catalog,
226 "apps/server dependencies",
227 );
228 pkg.overrides = resolveCatalogDependencies(
229 pkg.overrides,
230 rootPackageJson.workspaces.catalog,
231 "root overrides",
232 );
233
234 const original = yield* fs.readFileString(packageJsonPath);229 const original = yield* fs.readFileString(packageJsonPath);
235 yield* fs.writeFileString(backupPath, original);230 yield* fs.writeFileString(backupPath, original);
236 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);231 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
237 yield* Effect.log("[cli] Resolved package.json for publish");232 yield* Effect.log("[cli] Prepared package.json for publish");
238233
239 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);234 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
240 return { iconBackups };235 return { iconBackups };
apps/server/src/auth/Layers/AuthControlPlane.test.ts+1-1
...@@ -2,7 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -2,7 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";3import { Effect, Layer } from "effect";
44
5import { ServerConfigShape } from "../../config.ts";5import type { ServerConfigShape } from "../../config.ts";
6import { ServerConfig } from "../../config.ts";6import { ServerConfig } from "../../config.ts";
7import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";7import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";
8import { ServerSecretStoreLive } from "./ServerSecretStore.ts";8import { ServerSecretStoreLive } from "./ServerSecretStore.ts";
apps/server/src/auth/Layers/AuthControlPlane.ts+3-1
...@@ -10,8 +10,10 @@ import { layerConfig as SqlitePersistenceLayerLive } from "../../persistence/Lay...@@ -10,8 +10,10 @@ import { layerConfig as SqlitePersistenceLayerLive } from "../../persistence/Lay
10import {10import {
11 AuthControlPlane,11 AuthControlPlane,
12 AuthControlPlaneError,12 AuthControlPlaneError,
13 AuthControlPlaneShape,
14 DEFAULT_SESSION_SUBJECT,13 DEFAULT_SESSION_SUBJECT,
14} from "../Services/AuthControlPlane.ts";
15import type {
16 AuthControlPlaneShape,
15 IssuedBearerSession,17 IssuedBearerSession,
16 IssuedPairingLink,18 IssuedPairingLink,
17} from "../Services/AuthControlPlane.ts";19} from "../Services/AuthControlPlane.ts";
apps/server/src/auth/Services/AuthControlPlane.ts+1-1
...@@ -5,7 +5,7 @@ import type {...@@ -5,7 +5,7 @@ import type {
5 AuthSessionId,5 AuthSessionId,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Data, DateTime, Duration, Effect, Context } from "effect";7import { Data, DateTime, Duration, Effect, Context } from "effect";
8import { SessionRole } from "./SessionCredentialService";8import type { SessionRole } from "./SessionCredentialService.ts";
99
10export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";10export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";
1111
apps/server/src/auth/utils.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { deriveAuthClientMetadata } from "./utils";3import { deriveAuthClientMetadata } from "./utils.ts";
44
5describe("deriveAuthClientMetadata", () => {5describe("deriveAuthClientMetadata", () => {
6 it("labels Electron user agents as Electron instead of Chrome", () => {6 it("labels Electron user agents as Electron instead of Chrome", () => {
apps/server/src/bin.ts+3-3
...@@ -5,12 +5,12 @@ import * as Layer from "effect/Layer";...@@ -5,12 +5,12 @@ import * as Layer from "effect/Layer";
5import { Command } from "effect/unstable/cli";5import { Command } from "effect/unstable/cli";
66
7import { NetService } from "@t3tools/shared/Net";7import { NetService } from "@t3tools/shared/Net";
8import { cli } from "./cli";8import { cli } from "./cli.ts";
9import { version } from "../package.json" with { type: "json" };9import packageJson from "../package.json" with { type: "json" };
1010
11const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);11const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
1212
13Command.run(cli, { version }).pipe(13Command.run(cli, { version: packageJson.version }).pipe(
14 Effect.scoped,14 Effect.scoped,
15 Effect.provide(CliRuntimeLayer),15 Effect.provide(CliRuntimeLayer),
16 NodeRuntime.runMain,16 NodeRuntime.runMain,
apps/server/src/bootstrap.test.ts+1-1
...@@ -10,7 +10,7 @@ import * as Fiber from "effect/Fiber";...@@ -10,7 +10,7 @@ import * as Fiber from "effect/Fiber";
10import { TestClock } from "effect/testing";10import { TestClock } from "effect/testing";
11import { vi } from "vitest";11import { vi } from "vitest";
1212
13import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";13import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap.ts";
14import { assertNone, assertSome } from "@effect/vitest/utils";14import { assertNone, assertSome } from "@effect/vitest/utils";
1515
16const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));16const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));
apps/server/src/cli-config.test.ts+2-2
...@@ -5,8 +5,8 @@ import { ConfigProvider, Effect, FileSystem, Layer, Option, Path } from "effect"...@@ -5,8 +5,8 @@ import { ConfigProvider, Effect, FileSystem, Layer, Option, Path } from "effect"
55
6import { NetService } from "@t3tools/shared/Net";6import { NetService } from "@t3tools/shared/Net";
7import * as NodeServices from "@effect/platform-node/NodeServices";7import * as NodeServices from "@effect/platform-node/NodeServices";
8import { deriveServerPaths } from "./config";8import { deriveServerPaths } from "./config.ts";
9import { resolveServerConfig } from "./cli";9import { resolveServerConfig } from "./cli.ts";
1010
11it.layer(NodeServices.layer)("cli config resolution", (it) => {11it.layer(NodeServices.layer)("cli config resolution", (it) => {
12 const defaultObservabilityConfig = {12 const defaultObservabilityConfig = {
apps/server/src/cli.ts+12-11
...@@ -40,30 +40,31 @@ import {...@@ -40,30 +40,31 @@ import {
40 RuntimeMode,40 RuntimeMode,
41 type ServerConfigShape,41 type ServerConfigShape,
42 type StartupPresentation,42 type StartupPresentation,
43} from "./config";43} from "./config.ts";
44import { readBootstrapEnvelope } from "./bootstrap";44import { readBootstrapEnvelope } from "./bootstrap.ts";
45import { expandHomePath, resolveBaseDir } from "./os-jank";45import { expandHomePath, resolveBaseDir } from "./os-jank.ts";
46import { runServer } from "./server";46import { runServer } from "./server.ts";
47import { AuthControlPlaneRuntimeLive } from "./auth/Layers/AuthControlPlane.ts";47import { AuthControlPlaneRuntimeLive } from "./auth/Layers/AuthControlPlane.ts";
48import {48import {
49 formatIssuedPairingCredential,49 formatIssuedPairingCredential,
50 formatIssuedSession,50 formatIssuedSession,
51 formatPairingCredentialList,51 formatPairingCredentialList,
52 formatSessionList,52 formatSessionList,
53} from "./cliAuthFormat";53} from "./cliAuthFormat.ts";
54import { AuthControlPlane, AuthControlPlaneShape } from "./auth/Services/AuthControlPlane.ts";54import { AuthControlPlane } from "./auth/Services/AuthControlPlane.ts";
55import type { AuthControlPlaneShape } from "./auth/Services/AuthControlPlane.ts";
55import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";56import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
56import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";57import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
57import { OrchestrationLayerLive } from "./orchestration/runtimeLayer";58import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
58import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";59import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
59import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";60import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
60import { getAutoBootstrapDefaultModelSelection } from "./serverRuntimeStartup";61import { getAutoBootstrapDefaultModelSelection } from "./serverRuntimeStartup.ts";
61import {62import {
62 clearPersistedServerRuntimeState,63 clearPersistedServerRuntimeState,
63 readPersistedServerRuntimeState,64 readPersistedServerRuntimeState,
64} from "./serverRuntimeState";65} from "./serverRuntimeState.ts";
65import { WorkspacePaths } from "./workspace/Services/WorkspacePaths";66import { WorkspacePaths } from "./workspace/Services/WorkspacePaths.ts";
66import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths";67import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
6768
68const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));69const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
6970
apps/server/src/codexAppServerManager.test.ts+1-1
...@@ -15,7 +15,7 @@ import {...@@ -15,7 +15,7 @@ import {
15 normalizeCodexModelSlug,15 normalizeCodexModelSlug,
16 readCodexAccountSnapshot,16 readCodexAccountSnapshot,
17 resolveCodexModelForAccount,17 resolveCodexModelForAccount,
18} from "./codexAppServerManager";18} from "./codexAppServerManager.ts";
1919
20const asThreadId = (value: string): ThreadId => ThreadId.make(value);20const asThreadId = (value: string): ThreadId => ThreadId.make(value);
2121
apps/server/src/codexAppServerManager.ts+5-5
...@@ -25,16 +25,16 @@ import {...@@ -25,16 +25,16 @@ import {
25 formatCodexCliUpgradeMessage,25 formatCodexCliUpgradeMessage,
26 isCodexCliVersionSupported,26 isCodexCliVersionSupported,
27 parseCodexCliVersion,27 parseCodexCliVersion,
28} from "./provider/codexCliVersion";28} from "./provider/codexCliVersion.ts";
29import {29import {
30 readCodexAccountSnapshot,30 readCodexAccountSnapshot,
31 resolveCodexModelForAccount,31 resolveCodexModelForAccount,
32 type CodexAccountSnapshot,32 type CodexAccountSnapshot,
33} from "./provider/codexAccount";33} from "./provider/codexAccount.ts";
34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer";34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer.ts";
3535
36export { buildCodexInitializeParams } from "./provider/codexAppServer";36export { buildCodexInitializeParams } from "./provider/codexAppServer.ts";
37export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount";37export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount.ts";
3838
39type PendingRequestKey = string;39type PendingRequestKey = string;
4040
apps/server/src/environment/Layers/ServerEnvironment.ts+2-2
...@@ -3,7 +3,7 @@ import { Effect, FileSystem, Layer, Path, Random } from "effect";...@@ -3,7 +3,7 @@ import { Effect, FileSystem, Layer, Path, Random } from "effect";
33
4import { ServerConfig } from "../../config.ts";4import { ServerConfig } from "../../config.ts";
5import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";5import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";
6import { version } from "../../../package.json" with { type: "json" };6import packageJson from "../../../package.json" with { type: "json" };
7import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";7import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
88
9function platformOs(): ExecutionEnvironmentDescriptor["platform"]["os"] {9function platformOs(): ExecutionEnvironmentDescriptor["platform"]["os"] {
...@@ -77,7 +77,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function...@@ -77,7 +77,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function
77 os: platformOs(),77 os: platformOs(),
78 arch: platformArch(),78 arch: platformArch(),
79 },79 },
80 serverVersion: version,80 serverVersion: packageJson.version,
81 capabilities: {81 capabilities: {
82 repositoryIdentity: true,82 repositoryIdentity: true,
83 },83 },
apps/server/src/git/Layers/GitHubCli.test.ts+1-1
...@@ -6,7 +6,7 @@ vi.mock("../../processRunner", () => ({...@@ -6,7 +6,7 @@ vi.mock("../../processRunner", () => ({
6 runProcess: vi.fn(),6 runProcess: vi.fn(),
7}));7}));
88
9import { runProcess } from "../../processRunner";9import { runProcess } from "../../processRunner.ts";
10import { GitHubCli } from "../Services/GitHubCli.ts";10import { GitHubCli } from "../Services/GitHubCli.ts";
11import { GitHubCliLive } from "./GitHubCli.ts";11import { GitHubCliLive } from "./GitHubCli.ts";
1212
apps/server/src/git/Layers/GitHubCli.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Effect, Layer, Result, Schema, SchemaIssue } from "effect";1import { Effect, Layer, Result, Schema, SchemaIssue } from "effect";
2import { TrimmedNonEmptyString } from "@t3tools/contracts";2import { TrimmedNonEmptyString } from "@t3tools/contracts";
33
4import { runProcess } from "../../processRunner";4import { runProcess } from "../../processRunner.ts";
5import { GitHubCliError } from "@t3tools/contracts";5import { GitHubCliError } from "@t3tools/contracts";
6import {6import {
7 GitHubCli,7 GitHubCli,
apps/server/src/git/Layers/GitManager.ts+2-1
...@@ -38,7 +38,8 @@ import {...@@ -38,7 +38,8 @@ import {
38 type GitManagerShape,38 type GitManagerShape,
39 type GitRunStackedActionOptions,39 type GitRunStackedActionOptions,
40} from "../Services/GitManager.ts";40} from "../Services/GitManager.ts";
41import { GitCore, GitStatusDetails } from "../Services/GitCore.ts";41import { GitCore } from "../Services/GitCore.ts";
42import type { GitStatusDetails } from "../Services/GitCore.ts";
42import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";43import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";
43import { TextGeneration } from "../Services/TextGeneration.ts";44import { TextGeneration } from "../Services/TextGeneration.ts";
44import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";45import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";
apps/server/src/git/Services/GitHubCli.ts+1-1
...@@ -8,7 +8,7 @@...@@ -8,7 +8,7 @@
8import { Context } from "effect";8import { Context } from "effect";
9import type { Effect } from "effect";9import type { Effect } from "effect";
1010
11import type { ProcessRunResult } from "../../processRunner";11import type { ProcessRunResult } from "../../processRunner.ts";
12import type { GitHubCliError } from "@t3tools/contracts";12import type { GitHubCliError } from "@t3tools/contracts";
1313
14export interface GitHubPullRequestSummary {14export interface GitHubPullRequestSummary {
apps/server/src/http.ts+4-4
...@@ -15,12 +15,12 @@ import {...@@ -15,12 +15,12 @@ import {
15 ATTACHMENTS_ROUTE_PREFIX,15 ATTACHMENTS_ROUTE_PREFIX,
16 normalizeAttachmentRelativePath,16 normalizeAttachmentRelativePath,
17 resolveAttachmentRelativePath,17 resolveAttachmentRelativePath,
18} from "./attachmentPaths";18} from "./attachmentPaths.ts";
19import { resolveAttachmentPathById } from "./attachmentStore";19import { resolveAttachmentPathById } from "./attachmentStore.ts";
20import { resolveStaticDir, ServerConfig } from "./config";20import { resolveStaticDir, ServerConfig } from "./config.ts";
21import { decodeOtlpTraceRecords } from "./observability/TraceRecord.ts";21import { decodeOtlpTraceRecords } from "./observability/TraceRecord.ts";
22import { BrowserTraceCollector } from "./observability/Services/BrowserTraceCollector.ts";22import { BrowserTraceCollector } from "./observability/Services/BrowserTraceCollector.ts";
23import { ProjectFaviconResolver } from "./project/Services/ProjectFaviconResolver";23import { ProjectFaviconResolver } from "./project/Services/ProjectFaviconResolver.ts";
24import { ServerAuth } from "./auth/Services/ServerAuth.ts";24import { ServerAuth } from "./auth/Services/ServerAuth.ts";
25import { respondToAuthError } from "./auth/http.ts";25import { respondToAuthError } from "./auth/http.ts";
26import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";26import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
apps/server/src/keybindings.test.ts+2-2
...@@ -3,7 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -3,7 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
4import { assertFailure } from "@effect/vitest/utils";4import { assertFailure } from "@effect/vitest/utils";
5import { Cause, Effect, FileSystem, Layer, Logger, Path, Schema } from "effect";5import { Cause, Effect, FileSystem, Layer, Logger, Path, Schema } from "effect";
6import { ServerConfig } from "./config";6import { ServerConfig } from "./config.ts";
77
8import {8import {
9 DEFAULT_KEYBINDINGS,9 DEFAULT_KEYBINDINGS,
...@@ -13,7 +13,7 @@ import {...@@ -13,7 +13,7 @@ import {
13 compileResolvedKeybindingRule,13 compileResolvedKeybindingRule,
14 compileResolvedKeybindingsConfig,14 compileResolvedKeybindingsConfig,
15 parseKeybindingShortcut,15 parseKeybindingShortcut,
16} from "./keybindings";16} from "./keybindings.ts";
17import { KeybindingsConfigError } from "@t3tools/contracts";17import { KeybindingsConfigError } from "@t3tools/contracts";
1818
19const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);19const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
apps/server/src/keybindings.ts+2-2
...@@ -19,7 +19,7 @@ import {...@@ -19,7 +19,7 @@ import {
19 THREAD_JUMP_KEYBINDING_COMMANDS,19 THREAD_JUMP_KEYBINDING_COMMANDS,
20 type ServerConfigIssue,20 type ServerConfigIssue,
21} from "@t3tools/contracts";21} from "@t3tools/contracts";
22import { Mutable } from "effect/Types";22import type { Mutable } from "effect/Types";
23import {23import {
24 Array,24 Array,
25 Cache,25 Cache,
...@@ -44,7 +44,7 @@ import {...@@ -44,7 +44,7 @@ import {
44 Stream,44 Stream,
45} from "effect";45} from "effect";
46import * as Semaphore from "effect/Semaphore";46import * as Semaphore from "effect/Semaphore";
47import { ServerConfig } from "./config";47import { ServerConfig } from "./config.ts";
48import { fromLenientJson } from "@t3tools/shared/schemaJson";48import { fromLenientJson } from "@t3tools/shared/schemaJson";
4949
50type WhenToken =50type WhenToken =
apps/server/src/observability/LocalFileTracer.ts+8-3
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import type * as Exit from "effect/Exit";1import type * as Exit from "effect/Exit";
2import { Effect, Option, Tracer } from "effect";2import { Effect, Option, Tracer } from "effect";
33
4import { EffectTraceRecord, spanToTraceRecord } from "./TraceRecord.ts";4import { spanToTraceRecord } from "./TraceRecord.ts";
5import type { EffectTraceRecord } from "./TraceRecord.ts";
5import { makeTraceSink, type TraceSink } from "./TraceSink.ts";6import { makeTraceSink, type TraceSink } from "./TraceSink.ts";
67
7export interface LocalFileTracerOptions {8export interface LocalFileTracerOptions {
...@@ -27,12 +28,16 @@ class LocalFileSpan implements Tracer.Span {...@@ -27,12 +28,16 @@ class LocalFileSpan implements Tracer.Span {
27 status: Tracer.SpanStatus;28 status: Tracer.SpanStatus;
28 attributes: Map<string, unknown>;29 attributes: Map<string, unknown>;
29 events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]>;30 events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]>;
31 private readonly delegate: Tracer.Span;
32 private readonly push: (record: EffectTraceRecord) => void;
3033
31 constructor(34 constructor(
32 options: Parameters<Tracer.Tracer["span"]>[0],35 options: Parameters<Tracer.Tracer["span"]>[0],
33 private readonly delegate: Tracer.Span,36 delegate: Tracer.Span,
34 private readonly push: (record: EffectTraceRecord) => void,37 push: (record: EffectTraceRecord) => void,
35 ) {38 ) {
39 this.delegate = delegate;
40 this.push = push;
36 this.name = delegate.name;41 this.name = delegate.name;
37 this.spanId = delegate.spanId;42 this.spanId = delegate.spanId;
38 this.traceId = delegate.traceId;43 this.traceId = delegate.traceId;
apps/server/src/open.test.ts+1-1
...@@ -8,7 +8,7 @@ import {...@@ -8,7 +8,7 @@ import {
8 launchDetached,8 launchDetached,
9 resolveAvailableEditors,9 resolveAvailableEditors,
10 resolveEditorLaunch,10 resolveEditorLaunch,
11} from "./open";11} from "./open.ts";
1212
13it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {13it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
14 it.effect("returns commands for command-based editors", () =>14 it.effect("returns commands for command-based editors", () =>
apps/server/src/orchestration/Layers/CheckpointReactor.ts+2-2
...@@ -21,8 +21,8 @@ import { ProviderService } from "../../provider/Services/ProviderService.ts";...@@ -21,8 +21,8 @@ import { ProviderService } from "../../provider/Services/ProviderService.ts";
21import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";21import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";
22import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";22import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
23import { RuntimeReceiptBus } from "../Services/RuntimeReceiptBus.ts";23import { RuntimeReceiptBus } from "../Services/RuntimeReceiptBus.ts";
24import { CheckpointStoreError } from "../../checkpointing/Errors.ts";24import type { CheckpointStoreError } from "../../checkpointing/Errors.ts";
25import { OrchestrationDispatchError } from "../Errors.ts";25import type { OrchestrationDispatchError } from "../Errors.ts";
26import { isGitRepository } from "../../git/Utils.ts";26import { isGitRepository } from "../../git/Utils.ts";
27import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";27import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";
28import { WorkspaceEntries } from "../../workspace/Services/WorkspaceEntries.ts";28import { WorkspaceEntries } from "../../workspace/Services/WorkspaceEntries.ts";
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2-1
...@@ -19,7 +19,8 @@ import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";...@@ -19,7 +19,8 @@ import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
19import { GitCore } from "../../git/Services/GitCore.ts";19import { GitCore } from "../../git/Services/GitCore.ts";
20import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";20import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";
21import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";21import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";
22import { ProviderAdapterRequestError, ProviderServiceError } from "../../provider/Errors.ts";22import { ProviderAdapterRequestError } from "../../provider/Errors.ts";
23import type { ProviderServiceError } from "../../provider/Errors.ts";
23import { TextGeneration } from "../../git/Services/TextGeneration.ts";24import { TextGeneration } from "../../git/Services/TextGeneration.ts";
24import { ProviderService } from "../../provider/Services/ProviderService.ts";25import { ProviderService } from "../../provider/Services/ProviderService.ts";
25import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";26import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
apps/server/src/orchestration/Normalizer.ts+4-4
...@@ -6,10 +6,10 @@ import {...@@ -6,10 +6,10 @@ import {
6 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,6 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
88
9import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore";9import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore.ts";
10import { ServerConfig } from "../config";10import { ServerConfig } from "../config.ts";
11import { parseBase64DataUrl } from "../imageMime";11import { parseBase64DataUrl } from "../imageMime.ts";
12import { WorkspacePaths } from "../workspace/Services/WorkspacePaths";12import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts";
1313
14export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>14export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
15 Effect.gen(function* () {15 Effect.gen(function* () {
apps/server/src/os-jank.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it, vi } from "vitest";1import { describe, expect, it, vi } from "vitest";
22
3import { fixPath } from "./os-jank";3import { fixPath } from "./os-jank.ts";
44
5describe("fixPath", () => {5describe("fixPath", () => {
6 it("hydrates PATH on linux using the resolved login shell", () => {6 it("hydrates PATH on linux using the resolved login shell", () => {
apps/server/src/processRunner.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { runProcess } from "./processRunner";3import { runProcess } from "./processRunner.ts";
44
5describe("runProcess", () => {5describe("runProcess", () => {
6 it("fails when output exceeds max buffer in default mode", async () => {6 it("fails when output exceeds max buffer in default mode", async () => {
apps/server/src/provider/Layers/ClaudeAdapter.ts+1-1
...@@ -17,7 +17,7 @@ import {...@@ -17,7 +17,7 @@ import {
17 type SDKResultMessage,17 type SDKResultMessage,
18 type SettingSource,18 type SettingSource,
19 type SDKUserMessage,19 type SDKUserMessage,
20 ModelUsage,20 type ModelUsage,
21} from "@anthropic-ai/claude-agent-sdk";21} from "@anthropic-ai/claude-agent-sdk";
22import { parseCliArgs } from "@t3tools/shared/cliArgs";22import { parseCliArgs } from "@t3tools/shared/cliArgs";
23import {23import {
apps/server/src/provider/Layers/ClaudeProvider.ts+5-5
...@@ -25,11 +25,11 @@ import {...@@ -25,11 +25,11 @@ import {
25 providerModelsFromSettings,25 providerModelsFromSettings,
26 spawnAndCollect,26 spawnAndCollect,
27 type CommandResult,27 type CommandResult,
28} from "../providerSnapshot";28} from "../providerSnapshot.ts";
29import { compareCliVersions } from "../cliVersion";29import { compareCliVersions } from "../cliVersion.ts";
30import { makeManagedServerProvider } from "../makeManagedServerProvider";30import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
31import { ClaudeProvider } from "../Services/ClaudeProvider";31import { ClaudeProvider } from "../Services/ClaudeProvider.ts";
32import { ServerSettingsService } from "../../serverSettings";32import { ServerSettingsService } from "../../serverSettings.ts";
33import { ServerSettingsError } from "@t3tools/contracts";33import { ServerSettingsError } from "@t3tools/contracts";
3434
35const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = {35const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = {
apps/server/src/provider/Layers/CodexProvider.ts+7-7
...@@ -32,22 +32,22 @@ import {...@@ -32,22 +32,22 @@ import {
32 providerModelsFromSettings,32 providerModelsFromSettings,
33 spawnAndCollect,33 spawnAndCollect,
34 type CommandResult,34 type CommandResult,
35} from "../providerSnapshot";35} from "../providerSnapshot.ts";
36import { makeManagedServerProvider } from "../makeManagedServerProvider";36import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
37import {37import {
38 formatCodexCliUpgradeMessage,38 formatCodexCliUpgradeMessage,
39 isCodexCliVersionSupported,39 isCodexCliVersionSupported,
40 parseCodexCliVersion,40 parseCodexCliVersion,
41} from "../codexCliVersion";41} from "../codexCliVersion.ts";
42import {42import {
43 adjustCodexModelsForAccount,43 adjustCodexModelsForAccount,
44 codexAuthSubLabel,44 codexAuthSubLabel,
45 codexAuthSubType,45 codexAuthSubType,
46 type CodexAccountSnapshot,46 type CodexAccountSnapshot,
47} from "../codexAccount";47} from "../codexAccount.ts";
48import { probeCodexDiscovery } from "../codexAppServer";48import { probeCodexDiscovery } from "../codexAppServer.ts";
49import { CodexProvider } from "../Services/CodexProvider";49import { CodexProvider } from "../Services/CodexProvider.ts";
50import { ServerSettingsService } from "../../serverSettings";50import { ServerSettingsService } from "../../serverSettings.ts";
51import { ServerSettingsError } from "@t3tools/contracts";51import { ServerSettingsError } from "@t3tools/contracts";
5252
53const DEFAULT_CODEX_MODEL_CAPABILITIES: ModelCapabilities = {53const DEFAULT_CODEX_MODEL_CAPABILITIES: ModelCapabilities = {
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+4-2
...@@ -4,8 +4,10 @@ import { assertFailure } from "@effect/vitest/utils";...@@ -4,8 +4,10 @@ import { assertFailure } from "@effect/vitest/utils";
44
5import { Effect, Layer, Stream } from "effect";5import { Effect, Layer, Stream } from "effect";
66
7import { ClaudeAdapter, ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";7import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
8import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";8import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
9import { CodexAdapter } from "../Services/CodexAdapter.ts";
10import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
9import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";11import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
10import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";12import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
11import { ProviderUnsupportedError } from "../Errors.ts";13import { ProviderUnsupportedError } from "../Errors.ts";
apps/server/src/provider/Layers/ProviderRegistry.test.ts+6-6
...@@ -28,12 +28,12 @@ import {...@@ -28,12 +28,12 @@ import {
28 hasCustomModelProvider,28 hasCustomModelProvider,
29 parseAuthStatusFromOutput,29 parseAuthStatusFromOutput,
30 readCodexConfigModelProvider,30 readCodexConfigModelProvider,
31} from "./CodexProvider";31} from "./CodexProvider.ts";
32import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./ClaudeProvider";32import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./ClaudeProvider.ts";
33import { haveProvidersChanged, ProviderRegistryLive } from "./ProviderRegistry";33import { haveProvidersChanged, ProviderRegistryLive } from "./ProviderRegistry.ts";
34import { ServerConfig } from "../../config";34import { ServerConfig } from "../../config.ts";
35import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings";35import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";
36import { ProviderRegistry } from "../Services/ProviderRegistry";36import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
3737
38// ── Test helpers ────────────────────────────────────────────────────38// ── Test helpers ────────────────────────────────────────────────────
3939
apps/server/src/provider/Layers/ProviderRegistry.ts+9-9
...@@ -6,14 +6,14 @@...@@ -6,14 +6,14 @@
6import type { ProviderKind, ServerProvider } from "@t3tools/contracts";6import type { ProviderKind, ServerProvider } from "@t3tools/contracts";
7import { Effect, Equal, FileSystem, Layer, Path, PubSub, Ref, Stream } from "effect";7import { Effect, Equal, FileSystem, Layer, Path, PubSub, Ref, Stream } from "effect";
88
9import { ServerConfig } from "../../config";9import { ServerConfig } from "../../config.ts";
10import { ClaudeProviderLive } from "./ClaudeProvider";10import { ClaudeProviderLive } from "./ClaudeProvider.ts";
11import { CodexProviderLive } from "./CodexProvider";11import { CodexProviderLive } from "./CodexProvider.ts";
12import type { ClaudeProviderShape } from "../Services/ClaudeProvider";12import type { ClaudeProviderShape } from "../Services/ClaudeProvider.ts";
13import { ClaudeProvider } from "../Services/ClaudeProvider";13import { ClaudeProvider } from "../Services/ClaudeProvider.ts";
14import type { CodexProviderShape } from "../Services/CodexProvider";14import type { CodexProviderShape } from "../Services/CodexProvider.ts";
15import { CodexProvider } from "../Services/CodexProvider";15import { CodexProvider } from "../Services/CodexProvider.ts";
16import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry";16import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry.ts";
17import {17import {
18 hydrateCachedProvider,18 hydrateCachedProvider,
19 PROVIDER_CACHE_IDS,19 PROVIDER_CACHE_IDS,
...@@ -21,7 +21,7 @@ import {...@@ -21,7 +21,7 @@ import {
21 readProviderStatusCache,21 readProviderStatusCache,
22 resolveProviderStatusCachePath,22 resolveProviderStatusCachePath,
23 writeProviderStatusCache,23 writeProviderStatusCache,
24} from "../providerStatusCache";24} from "../providerStatusCache.ts";
2525
26const loadProviders = (26const loadProviders = (
27 codexProvider: CodexProviderShape,27 codexProvider: CodexProviderShape,
apps/server/src/provider/Services/ClaudeProvider.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Context } from "effect";1import { Context } from "effect";
22
3import type { ServerProviderShape } from "./ServerProvider";3import type { ServerProviderShape } from "./ServerProvider.ts";
44
5export interface ClaudeProviderShape extends ServerProviderShape {}5export interface ClaudeProviderShape extends ServerProviderShape {}
66
apps/server/src/provider/Services/CodexProvider.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Context } from "effect";1import { Context } from "effect";
22
3import type { ServerProviderShape } from "./ServerProvider";3import type { ServerProviderShape } from "./ServerProvider.ts";
44
5export interface CodexProviderShape extends ServerProviderShape {}5export interface CodexProviderShape extends ServerProviderShape {}
66
apps/server/src/provider/cliVersion.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
22
3import { compareCliVersions, normalizeCliVersion } from "./cliVersion";3import { compareCliVersions, normalizeCliVersion } from "./cliVersion.ts";
44
5describe("cliVersion", () => {5describe("cliVersion", () => {
6 it("normalizes versions with a missing patch segment", () => {6 it("normalizes versions with a missing patch segment", () => {
apps/server/src/provider/codexAppServer.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";1import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
2import readline from "node:readline";2import readline from "node:readline";
3import type { ServerProviderSkill } from "@t3tools/contracts";3import type { ServerProviderSkill } from "@t3tools/contracts";
4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount";4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount.ts";
55
6interface JsonRpcProbeResponse {6interface JsonRpcProbeResponse {
7 readonly id?: unknown;7 readonly id?: unknown;
apps/server/src/provider/codexCliVersion.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { compareCliVersions, normalizeCliVersion } from "./cliVersion";1import { compareCliVersions, normalizeCliVersion } from "./cliVersion.ts";
22
3const CODEX_VERSION_PATTERN = /\bv?(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)\b/;3const CODEX_VERSION_PATTERN = /\bv?(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)\b/;
44
apps/server/src/provider/makeManagedServerProvider.ts+1-1
...@@ -2,7 +2,7 @@ import type { ServerProvider } from "@t3tools/contracts";...@@ -2,7 +2,7 @@ import type { ServerProvider } from "@t3tools/contracts";
2import { Duration, Effect, PubSub, Ref, Scope, Stream } from "effect";2import { Duration, Effect, PubSub, Ref, Scope, Stream } from "effect";
3import * as Semaphore from "effect/Semaphore";3import * as Semaphore from "effect/Semaphore";
44
5import type { ServerProviderShape } from "./Services/ServerProvider";5import type { ServerProviderShape } from "./Services/ServerProvider.ts";
6import { ServerSettingsError } from "@t3tools/contracts";6import { ServerSettingsError } from "@t3tools/contracts";
77
8export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(function* <8export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(function* <
apps/server/src/provider/providerSnapshot.ts+1-1
...@@ -10,7 +10,7 @@ import type {...@@ -10,7 +10,7 @@ import type {
10import { Effect, Stream } from "effect";10import { Effect, Stream } from "effect";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
12import { normalizeModelSlug } from "@t3tools/shared/model";12import { normalizeModelSlug } from "@t3tools/shared/model";
13import { isWindowsCommandNotFound } from "../processRunner";13import { isWindowsCommandNotFound } from "../processRunner.ts";
1414
15export const DEFAULT_TIMEOUT_MS = 4_000;15export const DEFAULT_TIMEOUT_MS = 4_000;
1616
apps/server/src/provider/providerStatusCache.test.ts+1-1
...@@ -8,7 +8,7 @@ import {...@@ -8,7 +8,7 @@ import {
8 readProviderStatusCache,8 readProviderStatusCache,
9 resolveProviderStatusCachePath,9 resolveProviderStatusCachePath,
10 writeProviderStatusCache,10 writeProviderStatusCache,
11} from "./providerStatusCache";11} from "./providerStatusCache.ts";
1212
13const makeProvider = (13const makeProvider = (
14 provider: ServerProvider["provider"],14 provider: ServerProvider["provider"],
apps/server/src/server.ts+49-49
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Effect, Layer } from "effect";1import { Effect, Layer } from "effect";
2import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";2import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
33
4import { ServerConfig } from "./config";4import { ServerConfig } from "./config.ts";
5import {5import {
6 attachmentsRouteLayer,6 attachmentsRouteLayer,
7 otlpTracesProxyRouteLayer,7 otlpTracesProxyRouteLayer,
...@@ -9,46 +9,46 @@ import {...@@ -9,46 +9,46 @@ import {
9 serverEnvironmentRouteLayer,9 serverEnvironmentRouteLayer,
10 staticAndDevRouteLayer,10 staticAndDevRouteLayer,
11 browserApiCorsLayer,11 browserApiCorsLayer,
12} from "./http";12} from "./http.ts";
13import { fixPath } from "./os-jank";13import { fixPath } from "./os-jank.ts";
14import { websocketRpcRouteLayer } from "./ws";14import { websocketRpcRouteLayer } from "./ws.ts";
15import { OpenLive } from "./open";15import { OpenLive } from "./open.ts";
16import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite";16import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
17import { ServerLifecycleEventsLive } from "./serverLifecycleEvents";17import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
18import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService";18import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
19import { makeEventNdjsonLogger } from "./provider/Layers/EventNdjsonLogger";19import { makeEventNdjsonLogger } from "./provider/Layers/EventNdjsonLogger.ts";
20import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";20import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
21import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime";21import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";
22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter.ts";
23import { makeClaudeAdapterLive } from "./provider/Layers/ClaudeAdapter";23import { makeClaudeAdapterLive } from "./provider/Layers/ClaudeAdapter.ts";
24import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";24import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
25import { makeProviderServiceLive } from "./provider/Layers/ProviderService";25import { makeProviderServiceLive } from "./provider/Layers/ProviderService.ts";
26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper";26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
27import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery";27import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
28import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore";28import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
29import { GitCoreLive } from "./git/Layers/GitCore";29import { GitCoreLive } from "./git/Layers/GitCore.ts";
30import { GitHubCliLive } from "./git/Layers/GitHubCli";30import { GitHubCliLive } from "./git/Layers/GitHubCli.ts";
31import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster";31import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.ts";
32import { RoutingTextGenerationLive } from "./git/Layers/RoutingTextGeneration";32import { RoutingTextGenerationLive } from "./git/Layers/RoutingTextGeneration.ts";
33import { TerminalManagerLive } from "./terminal/Layers/Manager";33import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
34import { GitManagerLive } from "./git/Layers/GitManager";34import { GitManagerLive } from "./git/Layers/GitManager.ts";
35import { KeybindingsLive } from "./keybindings";35import { KeybindingsLive } from "./keybindings.ts";
36import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup";36import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
37import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor";37import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
38import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus";38import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
39import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion";39import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
40import { ProviderCommandReactorLive } from "./orchestration/Layers/ProviderCommandReactor";40import { ProviderCommandReactorLive } from "./orchestration/Layers/ProviderCommandReactor.ts";
41import { CheckpointReactorLive } from "./orchestration/Layers/CheckpointReactor";41import { CheckpointReactorLive } from "./orchestration/Layers/CheckpointReactor.ts";
42import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry";42import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";
43import { ServerSettingsLive } from "./serverSettings";43import { ServerSettingsLive } from "./serverSettings.ts";
44import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver";44import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
45import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver";45import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
46import { WorkspaceEntriesLive } from "./workspace/Layers/WorkspaceEntries";46import { WorkspaceEntriesLive } from "./workspace/Layers/WorkspaceEntries.ts";
47import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem";47import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";
48import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths";48import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
49import { ProjectSetupScriptRunnerLive } from "./project/Layers/ProjectSetupScriptRunner";49import { ProjectSetupScriptRunnerLive } from "./project/Layers/ProjectSetupScriptRunner.ts";
50import { ObservabilityLive } from "./observability/Layers/Observability";50import { ObservabilityLive } from "./observability/Layers/Observability.ts";
51import { ServerEnvironmentLive } from "./environment/Layers/ServerEnvironment";51import { ServerEnvironmentLive } from "./environment/Layers/ServerEnvironment.ts";
52import {52import {
53 authBearerBootstrapRouteLayer,53 authBearerBootstrapRouteLayer,
54 authBootstrapRouteLayer,54 authBootstrapRouteLayer,
...@@ -60,27 +60,27 @@ import {...@@ -60,27 +60,27 @@ import {
60 authPairingCredentialRouteLayer,60 authPairingCredentialRouteLayer,
61 authSessionRouteLayer,61 authSessionRouteLayer,
62 authWebSocketTokenRouteLayer,62 authWebSocketTokenRouteLayer,
63} from "./auth/http";63} from "./auth/http.ts";
64import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore";64import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
65import { ServerAuthLive } from "./auth/Layers/ServerAuth";65import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";
66import { OrchestrationLayerLive } from "./orchestration/runtimeLayer";66import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
67import {67import {
68 clearPersistedServerRuntimeState,68 clearPersistedServerRuntimeState,
69 makePersistedServerRuntimeState,69 makePersistedServerRuntimeState,
70 persistServerRuntimeState,70 persistServerRuntimeState,
71} from "./serverRuntimeState";71} from "./serverRuntimeState.ts";
72import {72import {
73 orchestrationDispatchRouteLayer,73 orchestrationDispatchRouteLayer,
74 orchestrationSnapshotRouteLayer,74 orchestrationSnapshotRouteLayer,
75} from "./orchestration/http";75} from "./orchestration/http.ts";
7676
77const PtyAdapterLive = Layer.unwrap(77const PtyAdapterLive = Layer.unwrap(
78 Effect.gen(function* () {78 Effect.gen(function* () {
79 if (typeof Bun !== "undefined") {79 if (typeof Bun !== "undefined") {
80 const BunPTY = yield* Effect.promise(() => import("./terminal/Layers/BunPTY"));80 const BunPTY = yield* Effect.promise(() => import("./terminal/Layers/BunPTY.ts"));
81 return BunPTY.layer;81 return BunPTY.layer;
82 } else {82 } else {
83 const NodePTY = yield* Effect.promise(() => import("./terminal/Layers/NodePTY"));83 const NodePTY = yield* Effect.promise(() => import("./terminal/Layers/NodePTY.ts"));
84 return NodePTY.layer;84 return NodePTY.layer;
85 }85 }
86 }),86 }),
apps/server/src/serverLogger.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Effect, Logger, References, Layer } from "effect";1import { Effect, Logger, References, Layer } from "effect";
22
3import { ServerConfig } from "./config";3import { ServerConfig } from "./config.ts";
44
5export const ServerLoggerLive = Effect.gen(function* () {5export const ServerLoggerLive = Effect.gen(function* () {
6 const config = yield* ServerConfig;6 const config = yield* ServerConfig;
apps/server/src/serverRuntimeStartup.ts+13-13
...@@ -21,24 +21,24 @@ import {...@@ -21,24 +21,24 @@ import {
21 Console,21 Console,
22} from "effect";22} from "effect";
2323
24import { ServerConfig } from "./config";24import { ServerConfig } from "./config.ts";
25import { Keybindings } from "./keybindings";25import { Keybindings } from "./keybindings.ts";
26import { Open } from "./open";26import { Open } from "./open.ts";
27import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine";27import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
28import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";28import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
29import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor";29import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts";
30import { ServerLifecycleEvents } from "./serverLifecycleEvents";30import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";
31import { ServerSettingsService } from "./serverSettings";31import { ServerSettingsService } from "./serverSettings.ts";
32import { ServerEnvironment } from "./environment/Services/ServerEnvironment";32import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
33import { AnalyticsService } from "./telemetry/Services/AnalyticsService";33import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
34import { ServerAuth } from "./auth/Services/ServerAuth";34import { ServerAuth } from "./auth/Services/ServerAuth.ts";
35import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper";35import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts";
36import {36import {
37 formatHeadlessServeOutput,37 formatHeadlessServeOutput,
38 formatHostForUrl,38 formatHostForUrl,
39 isWildcardHost,39 isWildcardHost,
40 issueHeadlessServeAccessInfo,40 issueHeadlessServeAccessInfo,
41} from "./startupAccess";41} from "./startupAccess.ts";
4242
43export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{43export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{
44 readonly message: string;44 readonly message: string;
apps/server/src/serverRuntimeState.ts+2-2
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Effect, FileSystem, Option, Path, Schema } from "effect";1import { Effect, FileSystem, Option, Path, Schema } from "effect";
22
3import { type ServerConfigShape } from "./config";3import { type ServerConfigShape } from "./config.ts";
4import { formatHostForUrl, isWildcardHost } from "./startupAccess";4import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
55
6export const PersistedServerRuntimeState = Schema.Struct({6export const PersistedServerRuntimeState = Schema.Struct({
7 version: Schema.Literal(1),7 version: Schema.Literal(1),
apps/server/src/serverSettings.test.ts+2-2
...@@ -2,8 +2,8 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -2,8 +2,8 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
2import { DEFAULT_SERVER_SETTINGS, ServerSettingsPatch } from "@t3tools/contracts";2import { DEFAULT_SERVER_SETTINGS, ServerSettingsPatch } from "@t3tools/contracts";
3import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
4import { Effect, FileSystem, Layer, Schema } from "effect";4import { Effect, FileSystem, Layer, Schema } from "effect";
5import { ServerConfig } from "./config";5import { ServerConfig } from "./config.ts";
6import { ServerSettingsLive, ServerSettingsService } from "./serverSettings";6import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
77
8const makeServerSettingsLayer = () =>8const makeServerSettingsLayer = () =>
9 ServerSettingsLive.pipe(9 ServerSettingsLive.pipe(
apps/server/src/serverSettings.ts+1-1
...@@ -39,7 +39,7 @@ import {...@@ -39,7 +39,7 @@ import {
39 Cause,39 Cause,
40} from "effect";40} from "effect";
41import * as Semaphore from "effect/Semaphore";41import * as Semaphore from "effect/Semaphore";
42import { ServerConfig } from "./config";42import { ServerConfig } from "./config.ts";
43import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";43import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
44import { fromLenientJson } from "@t3tools/shared/schemaJson";44import { fromLenientJson } from "@t3tools/shared/schemaJson";
45import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";45import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
apps/server/src/startupAccess.test.ts+1-1
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 resolveHeadlessConnectionHost,7 resolveHeadlessConnectionHost,
8 resolveHeadlessConnectionString,8 resolveHeadlessConnectionString,
9 resolveListeningPort,9 resolveListeningPort,
10} from "./startupAccess";10} from "./startupAccess.ts";
1111
12it("prefers localhost when no explicit host is configured", () => {12it("prefers localhost when no explicit host is configured", () => {
13 expect(resolveHeadlessConnectionHost(undefined)).toBe("localhost");13 expect(resolveHeadlessConnectionHost(undefined)).toBe("localhost");
apps/server/src/startupAccess.ts+2-2
...@@ -4,8 +4,8 @@ import { QrCode } from "@t3tools/shared/qrCode";...@@ -4,8 +4,8 @@ import { QrCode } from "@t3tools/shared/qrCode";
4import { Effect } from "effect";4import { Effect } from "effect";
5import { HttpServer } from "effect/unstable/http";5import { HttpServer } from "effect/unstable/http";
66
7import { ServerConfig } from "./config";7import { ServerConfig } from "./config.ts";
8import { ServerAuth } from "./auth/Services/ServerAuth";8import { ServerAuth } from "./auth/Services/ServerAuth.ts";
99
10export interface HeadlessServeAccessInfo {10export interface HeadlessServeAccessInfo {
11 readonly connectionString: string;11 readonly connectionString: string;
apps/server/src/telemetry/Identify.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Effect, FileSystem, Path, Random, Schema } from "effect";1import { Effect, FileSystem, Path, Random, Schema } from "effect";
2import * as Crypto from "node:crypto";2import * as Crypto from "node:crypto";
3import { homedir } from "node:os";3import { homedir } from "node:os";
4import { ServerConfig } from "../config";4import { ServerConfig } from "../config.ts";
55
6const CodexAuthJsonSchema = Schema.Struct({6const CodexAuthJsonSchema = Schema.Struct({
7 tokens: Schema.Struct({7 tokens: Schema.Struct({
apps/server/src/telemetry/Layers/AnalyticsService.ts+2-2
...@@ -13,7 +13,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab...@@ -13,7 +13,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab
13import { ServerConfig } from "../../config.ts";13import { ServerConfig } from "../../config.ts";
14import { AnalyticsService, type AnalyticsServiceShape } from "../Services/AnalyticsService.ts";14import { AnalyticsService, type AnalyticsServiceShape } from "../Services/AnalyticsService.ts";
15import { getTelemetryIdentifier } from "../Identify.ts";15import { getTelemetryIdentifier } from "../Identify.ts";
16import { version } from "../../../package.json" with { type: "json" };16import packageJson from "../../../package.json" with { type: "json" };
1717
18interface BufferedAnalyticsEvent {18interface BufferedAnalyticsEvent {
19 readonly event: string;19 readonly event: string;
...@@ -86,7 +86,7 @@ const makeAnalyticsService = Effect.gen(function* () {...@@ -86,7 +86,7 @@ const makeAnalyticsService = Effect.gen(function* () {
86 platform: process.platform,86 platform: process.platform,
87 wsl: process.env.WSL_DISTRO_NAME,87 wsl: process.env.WSL_DISTRO_NAME,
88 arch: process.arch,88 arch: process.arch,
89 t3CodeVersion: version,89 t3CodeVersion: packageJson.version,
90 clientType,90 clientType,
91 },91 },
92 timestamp: event.capturedAt,92 timestamp: event.capturedAt,
apps/server/src/terminal/Layers/BunPTY.ts+5-2
...@@ -1,13 +1,16 @@...@@ -1,13 +1,16 @@
1import { Effect, Layer } from "effect";1import { Effect, Layer } from "effect";
2import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";2import { PtyAdapter } from "../Services/PTY.ts";
3import type { PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY.ts";
34
4class BunPtyProcess implements PtyProcess {5class BunPtyProcess implements PtyProcess {
5 private readonly dataListeners = new Set<(data: string) => void>();6 private readonly dataListeners = new Set<(data: string) => void>();
6 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();7 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();
7 private readonly decoder = new TextDecoder();8 private readonly decoder = new TextDecoder();
9 private readonly process: Bun.Subprocess;
8 private didExit = false;10 private didExit = false;
911
10 constructor(private readonly process: Bun.Subprocess) {12 constructor(process: Bun.Subprocess) {
13 this.process = process;
11 void this.process.exited14 void this.process.exited
12 .then((exitCode) => {15 .then((exitCode) => {
13 this.emitExit({16 this.emitExit({
apps/server/src/terminal/Layers/Manager.test.ts+11-5
...@@ -24,25 +24,28 @@ import {...@@ -24,25 +24,28 @@ import {
24import { TestClock } from "effect/testing";24import { TestClock } from "effect/testing";
25import { expect } from "vitest";25import { expect } from "vitest";
2626
27import type { TerminalManagerShape } from "../Services/Manager";27import type { TerminalManagerShape } from "../Services/Manager.ts";
28import {28import {
29 type PtyAdapterShape,29 type PtyAdapterShape,
30 type PtyExitEvent,30 type PtyExitEvent,
31 type PtyProcess,31 type PtyProcess,
32 type PtySpawnInput,32 type PtySpawnInput,
33 PtySpawnError,33 PtySpawnError,
34} from "../Services/PTY";34} from "../Services/PTY.ts";
35import { makeTerminalManagerWithOptions } from "./Manager";35import { makeTerminalManagerWithOptions } from "./Manager.ts";
3636
37class FakePtyProcess implements PtyProcess {37class FakePtyProcess implements PtyProcess {
38 readonly writes: string[] = [];38 readonly writes: string[] = [];
39 readonly resizeCalls: Array<{ cols: number; rows: number }> = [];39 readonly resizeCalls: Array<{ cols: number; rows: number }> = [];
40 readonly killSignals: Array<string | undefined> = [];40 readonly killSignals: Array<string | undefined> = [];
41 readonly pid: number;
41 private readonly dataListeners = new Set<(data: string) => void>();42 private readonly dataListeners = new Set<(data: string) => void>();
42 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();43 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();
43 killed = false;44 killed = false;
4445
45 constructor(readonly pid: number) {}46 constructor(pid: number) {
47 this.pid = pid;
48 }
4649
47 write(data: string): void {50 write(data: string): void {
48 this.writes.push(data);51 this.writes.push(data);
...@@ -88,9 +91,12 @@ class FakePtyAdapter implements PtyAdapterShape {...@@ -88,9 +91,12 @@ class FakePtyAdapter implements PtyAdapterShape {
88 readonly spawnInputs: PtySpawnInput[] = [];91 readonly spawnInputs: PtySpawnInput[] = [];
89 readonly processes: FakePtyProcess[] = [];92 readonly processes: FakePtyProcess[] = [];
90 readonly spawnFailures: Error[] = [];93 readonly spawnFailures: Error[] = [];
94 private readonly mode: "sync" | "async";
91 private nextPid = 9000;95 private nextPid = 9000;
9296
93 constructor(private readonly mode: "sync" | "async" = "sync") {}97 constructor(mode: "sync" | "async" = "sync") {
98 this.mode = mode;
99 }
94100
95 spawn(input: PtySpawnInput): Effect.Effect<PtyProcess, PtySpawnError> {101 spawn(input: PtySpawnInput): Effect.Effect<PtyProcess, PtySpawnError> {
96 this.spawnInputs.push(input);102 this.spawnInputs.push(input);
apps/server/src/terminal/Layers/Manager.ts+5-5
...@@ -22,13 +22,13 @@ import {...@@ -22,13 +22,13 @@ import {
22 SynchronizedRef,22 SynchronizedRef,
23} from "effect";23} from "effect";
2424
25import { ServerConfig } from "../../config";25import { ServerConfig } from "../../config.ts";
26import {26import {
27 increment,27 increment,
28 terminalRestartsTotal,28 terminalRestartsTotal,
29 terminalSessionsTotal,29 terminalSessionsTotal,
30} from "../../observability/Metrics";30} from "../../observability/Metrics.ts";
31import { runProcess } from "../../processRunner";31import { runProcess } from "../../processRunner.ts";
32import {32import {
33 TerminalCwdError,33 TerminalCwdError,
34 TerminalHistoryError,34 TerminalHistoryError,
...@@ -36,14 +36,14 @@ import {...@@ -36,14 +36,14 @@ import {
36 TerminalNotRunningError,36 TerminalNotRunningError,
37 TerminalSessionLookupError,37 TerminalSessionLookupError,
38 type TerminalManagerShape,38 type TerminalManagerShape,
39} from "../Services/Manager";39} from "../Services/Manager.ts";
40import {40import {
41 PtyAdapter,41 PtyAdapter,
42 PtySpawnError,42 PtySpawnError,
43 type PtyAdapterShape,43 type PtyAdapterShape,
44 type PtyExitEvent,44 type PtyExitEvent,
45 type PtyProcess,45 type PtyProcess,
46} from "../Services/PTY";46} from "../Services/PTY.ts";
4747
48const DEFAULT_HISTORY_LINE_LIMIT = 5_000;48const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
49const DEFAULT_PERSIST_DEBOUNCE_MS = 40;49const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
apps/server/src/terminal/Layers/NodePTY.test.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { FileSystem, Path, Effect } from "effect";1import { FileSystem, Path, Effect } from "effect";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
33
4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY";4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY.ts";
5import * as NodeServices from "@effect/platform-node/NodeServices";5import * as NodeServices from "@effect/platform-node/NodeServices";
66
7it.layer(NodeServices.layer)("ensureNodePtySpawnHelperExecutable", (it) => {7it.layer(NodeServices.layer)("ensureNodePtySpawnHelperExecutable", (it) => {
apps/server/src/terminal/Layers/NodePTY.ts+7-2
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { createRequire } from "node:module";1import { createRequire } from "node:module";
22
3import { Effect, FileSystem, Layer, Path } from "effect";3import { Effect, FileSystem, Layer, Path } from "effect";
4import { PtyAdapter, PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY";4import { PtyAdapter } from "../Services/PTY.ts";
5import type { PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY.ts";
56
6let didEnsureSpawnHelperExecutable = false;7let didEnsureSpawnHelperExecutable = false;
78
...@@ -46,7 +47,11 @@ export const ensureNodePtySpawnHelperExecutable = Effect.fn(function* (explicitP...@@ -46,7 +47,11 @@ export const ensureNodePtySpawnHelperExecutable = Effect.fn(function* (explicitP
46});47});
4748
48class NodePtyProcess implements PtyProcess {49class NodePtyProcess implements PtyProcess {
49 constructor(private readonly process: import("node-pty").IPty) {}50 private readonly process: import("node-pty").IPty;
51
52 constructor(process: import("node-pty").IPty) {
53 this.process = process;
54 }
5055
51 get pid(): number {56 get pid(): number {
52 return this.process.pid;57 return this.process.pid;
apps/server/src/terminal/Services/Manager.ts+1-1
...@@ -22,7 +22,7 @@ import {...@@ -22,7 +22,7 @@ import {
22 TerminalSessionStatus,22 TerminalSessionStatus,
23 TerminalWriteInput,23 TerminalWriteInput,
24} from "@t3tools/contracts";24} from "@t3tools/contracts";
25import { PtyProcess } from "./PTY";25import type { PtyProcess } from "./PTY.ts";
26import { Effect, Context } from "effect";26import { Effect, Context } from "effect";
2727
28export {28export {
apps/server/src/ws.ts+26-26
...@@ -27,42 +27,42 @@ import { clamp } from "effect/Number";...@@ -27,42 +27,42 @@ import { clamp } from "effect/Number";
27import { HttpRouter, HttpServerRequest } from "effect/unstable/http";27import { HttpRouter, HttpServerRequest } from "effect/unstable/http";
28import { RpcSerialization, RpcServer } from "effect/unstable/rpc";28import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
2929
30import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery";30import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery.ts";
31import { ServerConfig } from "./config";31import { ServerConfig } from "./config.ts";
32import { GitCore } from "./git/Services/GitCore";32import { GitCore } from "./git/Services/GitCore.ts";
33import { GitManager } from "./git/Services/GitManager";33import { GitManager } from "./git/Services/GitManager.ts";
34import { GitStatusBroadcaster } from "./git/Services/GitStatusBroadcaster";34import { GitStatusBroadcaster } from "./git/Services/GitStatusBroadcaster.ts";
35import { Keybindings } from "./keybindings";35import { Keybindings } from "./keybindings.ts";
36import { Open, resolveAvailableEditors } from "./open";36import { Open, resolveAvailableEditors } from "./open.ts";
37import { normalizeDispatchCommand } from "./orchestration/Normalizer";37import { normalizeDispatchCommand } from "./orchestration/Normalizer.ts";
38import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine";38import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
39import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";39import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
40import {40import {
41 observeRpcEffect,41 observeRpcEffect,
42 observeRpcStream,42 observeRpcStream,
43 observeRpcStreamEffect,43 observeRpcStreamEffect,
44} from "./observability/RpcInstrumentation";44} from "./observability/RpcInstrumentation.ts";
45import { ProviderRegistry } from "./provider/Services/ProviderRegistry";45import { ProviderRegistry } from "./provider/Services/ProviderRegistry.ts";
46import { ServerLifecycleEvents } from "./serverLifecycleEvents";46import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";
47import { ServerRuntimeStartup } from "./serverRuntimeStartup";47import { ServerRuntimeStartup } from "./serverRuntimeStartup.ts";
48import { ServerSettingsService } from "./serverSettings";48import { ServerSettingsService } from "./serverSettings.ts";
49import { TerminalManager } from "./terminal/Services/Manager";49import { TerminalManager } from "./terminal/Services/Manager.ts";
50import { WorkspaceEntries } from "./workspace/Services/WorkspaceEntries";50import { WorkspaceEntries } from "./workspace/Services/WorkspaceEntries.ts";
51import { WorkspaceFileSystem } from "./workspace/Services/WorkspaceFileSystem";51import { WorkspaceFileSystem } from "./workspace/Services/WorkspaceFileSystem.ts";
52import { WorkspacePathOutsideRootError } from "./workspace/Services/WorkspacePaths";52import { WorkspacePathOutsideRootError } from "./workspace/Services/WorkspacePaths.ts";
53import { ProjectSetupScriptRunner } from "./project/Services/ProjectSetupScriptRunner";53import { ProjectSetupScriptRunner } from "./project/Services/ProjectSetupScriptRunner.ts";
54import { RepositoryIdentityResolver } from "./project/Services/RepositoryIdentityResolver";54import { RepositoryIdentityResolver } from "./project/Services/RepositoryIdentityResolver.ts";
55import { ServerEnvironment } from "./environment/Services/ServerEnvironment";55import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
56import { ServerAuth } from "./auth/Services/ServerAuth";56import { ServerAuth } from "./auth/Services/ServerAuth.ts";
57import {57import {
58 BootstrapCredentialService,58 BootstrapCredentialService,
59 type BootstrapCredentialChange,59 type BootstrapCredentialChange,
60} from "./auth/Services/BootstrapCredentialService";60} from "./auth/Services/BootstrapCredentialService.ts";
61import {61import {
62 SessionCredentialService,62 SessionCredentialService,
63 type SessionCredentialChange,63 type SessionCredentialChange,
64} from "./auth/Services/SessionCredentialService";64} from "./auth/Services/SessionCredentialService.ts";
65import { respondToAuthError } from "./auth/http";65import { respondToAuthError } from "./auth/http.ts";
6666
67function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<67function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<
68 OrchestrationEvent,68 OrchestrationEvent,
apps/server/tsconfig.json+1-3
...@@ -3,9 +3,7 @@...@@ -3,9 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "bun"],5 "types": ["node", "bun"],
6 "lib": ["ES2023", "esnext.disposable"],6 "lib": ["ESNext", "esnext.disposable"],
7 "noEmit": true,
8 "allowImportingTsExtensions": true,
9 "plugins": [7 "plugins": [
10 {8 {
11 "name": "@effect/language-service",9 "name": "@effect/language-service",
apps/server/vitest.config.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { defineConfig, mergeConfig } from "vitest/config";1import { defineConfig, mergeConfig } from "vitest/config";
22
3import baseConfig from "../../vitest.config";3import baseConfig from "../../vitest.config.ts";
44
5export default mergeConfig(5export default mergeConfig(
6 baseConfig,6 baseConfig,
apps/web/tsconfig.json+4
...@@ -2,6 +2,10 @@...@@ -2,6 +2,10 @@
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "module": "Preserve",
6 "moduleResolution": "Bundler",
7 "erasableSyntaxOnly": false,
8 "verbatimModuleSyntax": false,
5 "jsx": "react-jsx",9 "jsx": "react-jsx",
6 "lib": ["ES2023", "DOM", "DOM.Iterable"],10 "lib": ["ES2023", "DOM", "DOM.Iterable"],
7 "types": ["vite/client"],11 "types": ["vite/client"],
docs/observability.md+4-4
...@@ -69,11 +69,11 @@ npx t3...@@ -69,11 +69,11 @@ npx t3
69```69```
7070
71```bash71```bash
72bun dev72node --run dev
73```73```
7474
75```bash75```bash
76bun dev:desktop76node --run dev:desktop
77```77```
7878
79### Option 2: Run With A Local LGTM Stack79### Option 2: Run With A Local LGTM Stack
...@@ -122,13 +122,13 @@ npx t3...@@ -122,13 +122,13 @@ npx t3
122Monorepo web/server dev:122Monorepo web/server dev:
123123
124```bash124```bash
125bun dev125node --run dev
126```126```
127127
128Monorepo desktop dev:128Monorepo desktop dev:
129129
130```bash130```bash
131bun dev:desktop131node --run dev:desktop
132```132```
133133
134Packaged desktop app:134Packaged desktop app:
package.json+1-2
...@@ -33,14 +33,13 @@...@@ -33,14 +33,13 @@
33 "start": "turbo run start --filter=t3",33 "start": "turbo run start --filter=t3",
34 "start:desktop": "turbo run start --filter=@t3tools/desktop",34 "start:desktop": "turbo run start --filter=@t3tools/desktop",
35 "start:marketing": "turbo run preview --filter=@t3tools/marketing",35 "start:marketing": "turbo run preview --filter=@t3tools/marketing",
36 "start:mock-update-server": "bun run scripts/mock-update-server.ts",36 "start:mock-update-server": "node scripts/mock-update-server.ts",
37 "build": "turbo run build",37 "build": "turbo run build",
38 "build:marketing": "turbo run build --filter=@t3tools/marketing",38 "build:marketing": "turbo run build --filter=@t3tools/marketing",
39 "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=t3",39 "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=t3",
40 "typecheck": "turbo run typecheck",40 "typecheck": "turbo run typecheck",
41 "lint": "oxlint --report-unused-disable-directives",41 "lint": "oxlint --report-unused-disable-directives",
42 "test": "turbo run test",42 "test": "turbo run test",
43 "test:process-reaper": "bun run --cwd apps/server test:process-reaper",
44 "test:desktop-smoke": "turbo run smoke-test --filter=@t3tools/desktop",43 "test:desktop-smoke": "turbo run smoke-test --filter=@t3tools/desktop",
45 "fmt": "oxfmt",44 "fmt": "oxfmt",
46 "fmt:check": "oxfmt --check",45 "fmt:check": "oxfmt --check",
packages/client-runtime/src/index.ts+2-2
...@@ -1,2 +1,2 @@...@@ -1,2 +1,2 @@
1export * from "./knownEnvironment";1export * from "./knownEnvironment.ts";
2export * from "./scoped";2export * from "./scoped.ts";
packages/client-runtime/src/knownEnvironment.test.ts+2-2
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";1import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vitest";
33
4import { createKnownEnvironment, getKnownEnvironmentHttpBaseUrl } from "./knownEnvironment";4import { createKnownEnvironment, getKnownEnvironmentHttpBaseUrl } from "./knownEnvironment.ts";
5import {5import {
6 parseScopedProjectKey,6 parseScopedProjectKey,
7 parseScopedThreadKey,7 parseScopedThreadKey,
...@@ -10,7 +10,7 @@ import {...@@ -10,7 +10,7 @@ import {
10 scopedThreadKey,10 scopedThreadKey,
11 scopeProjectRef,11 scopeProjectRef,
12 scopeThreadRef,12 scopeThreadRef,
13} from "./scoped";13} from "./scoped.ts";
1414
15describe("known environment bootstrap helpers", () => {15describe("known environment bootstrap helpers", () => {
16 it("creates known environments from explicit server base urls", () => {16 it("creates known environments from explicit server base urls", () => {
packages/contracts/src/auth.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Schema } from "effect";1import { Schema } from "effect";
22
3import { AuthSessionId, TrimmedNonEmptyString } from "./baseSchemas";3import { AuthSessionId, TrimmedNonEmptyString } from "./baseSchemas.ts";
44
5/**5/**
6 * Declares the server's overall authentication posture.6 * Declares the server's overall authentication posture.
packages/contracts/src/editor.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
4export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]);4export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]);
5export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;5export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
packages/contracts/src/environment.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Effect, Schema } from "effect";1import { Effect, Schema } from "effect";
22
3import { EnvironmentId, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas";3import { EnvironmentId, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
44
5export const ExecutionEnvironmentPlatformOs = Schema.Literals([5export const ExecutionEnvironmentPlatformOs = Schema.Literals([
6 "darwin",6 "darwin",
packages/contracts/src/filesystem.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
4const FILESYSTEM_PATH_MAX_LENGTH = 512;4const FILESYSTEM_PATH_MAX_LENGTH = 512;
55
packages/contracts/src/git.test.ts+1-1
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 GitRunStackedActionResult,7 GitRunStackedActionResult,
8 GitRunStackedActionInput,8 GitRunStackedActionInput,
9 GitResolvePullRequestResult,9 GitResolvePullRequestResult,
10} from "./git";10} from "./git.ts";
1111
12const decodeCreateWorktreeInput = Schema.decodeUnknownSync(GitCreateWorktreeInput);12const decodeCreateWorktreeInput = Schema.decodeUnknownSync(GitCreateWorktreeInput);
13const decodePreparePullRequestThreadInput = Schema.decodeUnknownSync(13const decodePreparePullRequestThreadInput = Schema.decodeUnknownSync(
packages/contracts/src/git.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { NonNegativeInt, PositiveInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas";2import { NonNegativeInt, PositiveInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
33
4const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;4const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
5const GIT_LIST_BRANCHES_MAX_LIMIT = 200;5const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
packages/contracts/src/index.ts+17-17
...@@ -1,17 +1,17 @@...@@ -1,17 +1,17 @@
1export * from "./baseSchemas";1export * from "./baseSchemas.ts";
2export * from "./auth";2export * from "./auth.ts";
3export * from "./environment";3export * from "./environment.ts";
4export * from "./ipc";4export * from "./ipc.ts";
5export * from "./terminal";5export * from "./terminal.ts";
6export * from "./provider";6export * from "./provider.ts";
7export * from "./providerRuntime";7export * from "./providerRuntime.ts";
8export * from "./model";8export * from "./model.ts";
9export * from "./keybindings";9export * from "./keybindings.ts";
10export * from "./server";10export * from "./server.ts";
11export * from "./settings";11export * from "./settings.ts";
12export * from "./git";12export * from "./git.ts";
13export * from "./orchestration";13export * from "./orchestration.ts";
14export * from "./editor";14export * from "./editor.ts";
15export * from "./project";15export * from "./project.ts";
16export * from "./filesystem";16export * from "./filesystem.ts";
17export * from "./rpc";17export * from "./rpc.ts";
packages/contracts/src/ipc.ts+10-10
...@@ -17,19 +17,19 @@ import type {...@@ -17,19 +17,19 @@ import type {
17 GitStatusInput,17 GitStatusInput,
18 GitStatusResult,18 GitStatusResult,
19 GitCreateBranchResult,19 GitCreateBranchResult,
20} from "./git";20} from "./git.ts";
21import type { FilesystemBrowseInput, FilesystemBrowseResult } from "./filesystem";21import type { FilesystemBrowseInput, FilesystemBrowseResult } from "./filesystem.ts";
22import type {22import type {
23 ProjectSearchEntriesInput,23 ProjectSearchEntriesInput,
24 ProjectSearchEntriesResult,24 ProjectSearchEntriesResult,
25 ProjectWriteFileInput,25 ProjectWriteFileInput,
26 ProjectWriteFileResult,26 ProjectWriteFileResult,
27} from "./project";27} from "./project.ts";
28import type {28import type {
29 ServerConfig,29 ServerConfig,
30 ServerProviderUpdatedPayload,30 ServerProviderUpdatedPayload,
31 ServerUpsertKeybindingResult,31 ServerUpsertKeybindingResult,
32} from "./server";32} from "./server.ts";
33import type {33import type {
34 TerminalClearInput,34 TerminalClearInput,
35 TerminalCloseInput,35 TerminalCloseInput,
...@@ -39,8 +39,8 @@ import type {...@@ -39,8 +39,8 @@ import type {
39 TerminalRestartInput,39 TerminalRestartInput,
40 TerminalSessionSnapshot,40 TerminalSessionSnapshot,
41 TerminalWriteInput,41 TerminalWriteInput,
42} from "./terminal";42} from "./terminal.ts";
43import type { ServerUpsertKeybindingInput } from "./server";43import type { ServerUpsertKeybindingInput } from "./server.ts";
44import type {44import type {
45 ClientOrchestrationCommand,45 ClientOrchestrationCommand,
46 OrchestrationGetFullThreadDiffInput,46 OrchestrationGetFullThreadDiffInput,
...@@ -50,10 +50,10 @@ import type {...@@ -50,10 +50,10 @@ import type {
50 OrchestrationShellStreamItem,50 OrchestrationShellStreamItem,
51 OrchestrationSubscribeThreadInput,51 OrchestrationSubscribeThreadInput,
52 OrchestrationThreadStreamItem,52 OrchestrationThreadStreamItem,
53} from "./orchestration";53} from "./orchestration.ts";
54import type { EnvironmentId } from "./baseSchemas";54import type { EnvironmentId } from "./baseSchemas.ts";
55import { EditorId } from "./editor";55import { EditorId } from "./editor.ts";
56import { ClientSettings, ServerSettings, ServerSettingsPatch } from "./settings";56import { ServerSettings, type ClientSettings, type ServerSettingsPatch } from "./settings.ts";
5757
58export interface ContextMenuItem<T extends string = string> {58export interface ContextMenuItem<T extends string = string> {
59 id: T;59 id: T;
packages/contracts/src/keybindings.test.ts+1-1
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 KeybindingRule,7 KeybindingRule,
8 ResolvedKeybindingRule,8 ResolvedKeybindingRule,
9 ResolvedKeybindingsConfig,9 ResolvedKeybindingsConfig,
10} from "./keybindings";10} from "./keybindings.ts";
1111
12const decode = <S extends Schema.Top>(12const decode = <S extends Schema.Top>(
13 schema: S,13 schema: S,
packages/contracts/src/keybindings.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { TrimmedString } from "./baseSchemas";2import { TrimmedString } from "./baseSchemas.ts";
33
4export const MAX_KEYBINDING_VALUE_LENGTH = 64;4export const MAX_KEYBINDING_VALUE_LENGTH = 64;
5const MAX_KEYBINDING_WHEN_LENGTH = 256;5const MAX_KEYBINDING_WHEN_LENGTH = 256;
packages/contracts/src/model.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import type { ProviderKind } from "./orchestration";3import type { ProviderKind } from "./orchestration.ts";
44
5export const CodexReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low"]);5export const CodexReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low"]);
6export type CodexReasoningEffort = typeof CodexReasoningEffort.Type;6export type CodexReasoningEffort = typeof CodexReasoningEffort.Type;
packages/contracts/src/orchestration.test.ts+1-1
...@@ -19,7 +19,7 @@ import {...@@ -19,7 +19,7 @@ import {
19 ThreadCreatedPayload,19 ThreadCreatedPayload,
20 ThreadTurnDiff,20 ThreadTurnDiff,
21 ThreadTurnStartRequestedPayload,21 ThreadTurnStartRequestedPayload,
22} from "./orchestration";22} from "./orchestration.ts";
2323
24const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput);24const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput);
25const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff);25const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff);
packages/contracts/src/orchestration.ts+3-3
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { Effect, Option, Schema, SchemaIssue, Struct } from "effect";1import { Effect, Option, Schema, SchemaIssue, Struct } from "effect";
2import { ClaudeModelOptions, CodexModelOptions } from "./model";2import { ClaudeModelOptions, CodexModelOptions } from "./model.ts";
3import { RepositoryIdentity } from "./environment";3import { RepositoryIdentity } from "./environment.ts";
4import {4import {
5 ApprovalRequestId,5 ApprovalRequestId,
6 CheckpointRef,6 CheckpointRef,
...@@ -14,7 +14,7 @@ import {...@@ -14,7 +14,7 @@ import {
14 ThreadId,14 ThreadId,
15 TrimmedNonEmptyString,15 TrimmedNonEmptyString,
16 TurnId,16 TurnId,
17} from "./baseSchemas";17} from "./baseSchemas.ts";
1818
19export const ORCHESTRATION_WS_METHODS = {19export const ORCHESTRATION_WS_METHODS = {
20 dispatchCommand: "orchestration.dispatchCommand",20 dispatchCommand: "orchestration.dispatchCommand",
packages/contracts/src/project.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";2import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
33
4const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;4const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
5const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;5const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
packages/contracts/src/provider.test.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
2import { Schema } from "effect";2import { Schema } from "effect";
33
4import { ProviderSendTurnInput, ProviderSessionStartInput } from "./provider";4import { ProviderSendTurnInput, ProviderSessionStartInput } from "./provider.ts";
55
6const decodeProviderSessionStartInput = Schema.decodeUnknownSync(ProviderSessionStartInput);6const decodeProviderSessionStartInput = Schema.decodeUnknownSync(ProviderSessionStartInput);
7const decodeProviderSendTurnInput = Schema.decodeUnknownSync(ProviderSendTurnInput);7const decodeProviderSendTurnInput = Schema.decodeUnknownSync(ProviderSendTurnInput);
packages/contracts/src/provider.ts+3-3
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import {3import {
4 ApprovalRequestId,4 ApprovalRequestId,
5 EventId,5 EventId,
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 ProviderItemId,7 ProviderItemId,
8 ThreadId,8 ThreadId,
9 TurnId,9 TurnId,
10} from "./baseSchemas";10} from "./baseSchemas.ts";
11import {11import {
12 ChatAttachment,12 ChatAttachment,
13 ModelSelection,13 ModelSelection,
...@@ -21,7 +21,7 @@ import {...@@ -21,7 +21,7 @@ import {
21 ProviderSandboxMode,21 ProviderSandboxMode,
22 ProviderUserInputAnswers,22 ProviderUserInputAnswers,
23 RuntimeMode,23 RuntimeMode,
24} from "./orchestration";24} from "./orchestration.ts";
2525
26const ProviderSessionStatus = Schema.Literals([26const ProviderSessionStatus = Schema.Literals([
27 "connecting",27 "connecting",
packages/contracts/src/providerRuntime.test.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
2import { Schema } from "effect";2import { Schema } from "effect";
33
4import { ProviderRuntimeEvent } from "./providerRuntime";4import { ProviderRuntimeEvent } from "./providerRuntime.ts";
55
6const decodeRuntimeEvent = Schema.decodeUnknownSync(ProviderRuntimeEvent);6const decodeRuntimeEvent = Schema.decodeUnknownSync(ProviderRuntimeEvent);
77
packages/contracts/src/providerRuntime.ts+2-2
...@@ -11,8 +11,8 @@ import {...@@ -11,8 +11,8 @@ import {
11 ThreadId,11 ThreadId,
12 TrimmedNonEmptyString,12 TrimmedNonEmptyString,
13 TurnId,13 TurnId,
14} from "./baseSchemas";14} from "./baseSchemas.ts";
15import { ProviderKind } from "./orchestration";15import { ProviderKind } from "./orchestration.ts";
1616
17const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;17const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
18const UnknownRecordSchema = Schema.Record(Schema.String, Schema.Unknown);18const UnknownRecordSchema = Schema.Record(Schema.String, Schema.Unknown);
packages/contracts/src/rpc.ts+14-10
...@@ -2,9 +2,13 @@ import { Schema } from "effect";...@@ -2,9 +2,13 @@ import { Schema } from "effect";
2import * as Rpc from "effect/unstable/rpc/Rpc";2import * as Rpc from "effect/unstable/rpc/Rpc";
3import * as RpcGroup from "effect/unstable/rpc/RpcGroup";3import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
44
5import { OpenError, OpenInEditorInput } from "./editor";5import { OpenError, OpenInEditorInput } from "./editor.ts";
6import { AuthAccessStreamEvent } from "./auth";6import { AuthAccessStreamEvent } from "./auth.ts";
7import { FilesystemBrowseInput, FilesystemBrowseResult, FilesystemBrowseError } from "./filesystem";7import {
8 FilesystemBrowseInput,
9 FilesystemBrowseResult,
10 FilesystemBrowseError,
11} from "./filesystem.ts";
8import {12import {
9 GitActionProgressEvent,13 GitActionProgressEvent,
10 GitCheckoutInput,14 GitCheckoutInput,
...@@ -29,8 +33,8 @@ import {...@@ -29,8 +33,8 @@ import {
29 GitStatusInput,33 GitStatusInput,
30 GitStatusResult,34 GitStatusResult,
31 GitStatusStreamEvent,35 GitStatusStreamEvent,
32} from "./git";36} from "./git.ts";
33import { KeybindingsConfigError } from "./keybindings";37import { KeybindingsConfigError } from "./keybindings.ts";
34import {38import {
35 ClientOrchestrationCommand,39 ClientOrchestrationCommand,
36 ORCHESTRATION_WS_METHODS,40 ORCHESTRATION_WS_METHODS,
...@@ -43,7 +47,7 @@ import {...@@ -43,7 +47,7 @@ import {
43 OrchestrationReplayEventsError,47 OrchestrationReplayEventsError,
44 OrchestrationReplayEventsInput,48 OrchestrationReplayEventsInput,
45 OrchestrationRpcSchemas,49 OrchestrationRpcSchemas,
46} from "./orchestration";50} from "./orchestration.ts";
47import {51import {
48 ProjectSearchEntriesError,52 ProjectSearchEntriesError,
49 ProjectSearchEntriesInput,53 ProjectSearchEntriesInput,
...@@ -51,7 +55,7 @@ import {...@@ -51,7 +55,7 @@ import {
51 ProjectWriteFileError,55 ProjectWriteFileError,
52 ProjectWriteFileInput,56 ProjectWriteFileInput,
53 ProjectWriteFileResult,57 ProjectWriteFileResult,
54} from "./project";58} from "./project.ts";
55import {59import {
56 TerminalClearInput,60 TerminalClearInput,
57 TerminalCloseInput,61 TerminalCloseInput,
...@@ -62,7 +66,7 @@ import {...@@ -62,7 +66,7 @@ import {
62 TerminalRestartInput,66 TerminalRestartInput,
63 TerminalSessionSnapshot,67 TerminalSessionSnapshot,
64 TerminalWriteInput,68 TerminalWriteInput,
65} from "./terminal";69} from "./terminal.ts";
66import {70import {
67 ServerConfigStreamEvent,71 ServerConfigStreamEvent,
68 ServerConfig,72 ServerConfig,
...@@ -70,8 +74,8 @@ import {...@@ -70,8 +74,8 @@ import {
70 ServerProviderUpdatedPayload,74 ServerProviderUpdatedPayload,
71 ServerUpsertKeybindingInput,75 ServerUpsertKeybindingInput,
72 ServerUpsertKeybindingResult,76 ServerUpsertKeybindingResult,
73} from "./server";77} from "./server.ts";
74import { ServerSettings, ServerSettingsError, ServerSettingsPatch } from "./settings";78import { ServerSettings, ServerSettingsError, ServerSettingsPatch } from "./settings.ts";
7579
76export const WS_METHODS = {80export const WS_METHODS = {
77 // Project registry methods81 // Project registry methods
packages/contracts/src/server.test.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vitest";
33
4import { ServerProvider } from "./server";4import { ServerProvider } from "./server.ts";
55
6const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider);6const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider);
77
packages/contracts/src/server.ts+8-8
...@@ -1,18 +1,18 @@...@@ -1,18 +1,18 @@
1import { Effect, Schema } from "effect";1import { Effect, Schema } from "effect";
2import { ExecutionEnvironmentDescriptor } from "./environment";2import { ExecutionEnvironmentDescriptor } from "./environment.ts";
3import { ServerAuthDescriptor } from "./auth";3import { ServerAuthDescriptor } from "./auth.ts";
4import {4import {
5 IsoDateTime,5 IsoDateTime,
6 NonNegativeInt,6 NonNegativeInt,
7 ProjectId,7 ProjectId,
8 ThreadId,8 ThreadId,
9 TrimmedNonEmptyString,9 TrimmedNonEmptyString,
10} from "./baseSchemas";10} from "./baseSchemas.ts";
11import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings";11import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings.ts";
12import { EditorId } from "./editor";12import { EditorId } from "./editor.ts";
13import { ModelCapabilities } from "./model";13import { ModelCapabilities } from "./model.ts";
14import { ProviderKind } from "./orchestration";14import { ProviderKind } from "./orchestration.ts";
15import { ServerSettings } from "./settings";15import { ServerSettings } from "./settings.ts";
1616
17const KeybindingsMalformedConfigIssue = Schema.Struct({17const KeybindingsMalformedConfigIssue = Schema.Struct({
18 kind: Schema.Literal("keybindings.malformed-config"),18 kind: Schema.Literal("keybindings.malformed-config"),
packages/contracts/src/settings.ts+3-3
...@@ -1,13 +1,13 @@...@@ -1,13 +1,13 @@
1import { Effect } from "effect";1import { Effect } from "effect";
2import * as Schema from "effect/Schema";2import * as Schema from "effect/Schema";
3import * as SchemaTransformation from "effect/SchemaTransformation";3import * as SchemaTransformation from "effect/SchemaTransformation";
4import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas";4import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
5import {5import {
6 ClaudeModelOptions,6 ClaudeModelOptions,
7 CodexModelOptions,7 CodexModelOptions,
8 DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,8 DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
9} from "./model";9} from "./model.ts";
10import { ModelSelection } from "./orchestration";10import { ModelSelection } from "./orchestration.ts";
1111
12// ── Client Settings (local-only) ───────────────────────────────12// ── Client Settings (local-only) ───────────────────────────────
1313
packages/contracts/src/terminal.test.ts+1-1
...@@ -11,7 +11,7 @@ import {...@@ -11,7 +11,7 @@ import {
11 TerminalSessionSnapshot,11 TerminalSessionSnapshot,
12 TerminalThreadInput,12 TerminalThreadInput,
13 TerminalWriteInput,13 TerminalWriteInput,
14} from "./terminal";14} from "./terminal.ts";
1515
16function decodeSync<S extends Schema.Top>(schema: S, input: unknown): Schema.Schema.Type<S> {16function decodeSync<S extends Schema.Top>(schema: S, input: unknown): Schema.Schema.Type<S> {
17 return Schema.decodeUnknownSync(schema as never)(input) as Schema.Schema.Type<S>;17 return Schema.decodeUnknownSync(schema as never)(input) as Schema.Schema.Type<S>;
packages/contracts/src/terminal.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Effect, Schema } from "effect";1import { Effect, Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
4export const DEFAULT_TERMINAL_ID = "default";4export const DEFAULT_TERMINAL_ID = "default";
55
packages/shared/src/DrainableWorker.test.ts+1-1
...@@ -2,7 +2,7 @@ import { it } from "@effect/vitest";...@@ -2,7 +2,7 @@ import { it } from "@effect/vitest";
2import { describe, expect } from "vitest";2import { describe, expect } from "vitest";
3import { Deferred, Effect } from "effect";3import { Deferred, Effect } from "effect";
44
5import { makeDrainableWorker } from "./DrainableWorker";5import { makeDrainableWorker } from "./DrainableWorker.ts";
66
7describe("makeDrainableWorker", () => {7describe("makeDrainableWorker", () => {
8 it.live("waits for work enqueued during active processing before draining", () =>8 it.live("waits for work enqueued during active processing before draining", () =>
packages/shared/src/KeyedCoalescingWorker.test.ts+1-1
...@@ -2,7 +2,7 @@ import { it } from "@effect/vitest";...@@ -2,7 +2,7 @@ import { it } from "@effect/vitest";
2import { describe, expect } from "vitest";2import { describe, expect } from "vitest";
3import { Deferred, Effect } from "effect";3import { Deferred, Effect } from "effect";
44
5import { makeKeyedCoalescingWorker } from "./KeyedCoalescingWorker";5import { makeKeyedCoalescingWorker } from "./KeyedCoalescingWorker.ts";
66
7describe("makeKeyedCoalescingWorker", () => {7describe("makeKeyedCoalescingWorker", () => {
8 it.live("waits for latest work enqueued during active processing before draining the key", () =>8 it.live("waits for latest work enqueued during active processing before draining the key", () =>
packages/shared/src/Net.test.ts+1-1
...@@ -3,7 +3,7 @@ import * as Net from "node:net";...@@ -3,7 +3,7 @@ import * as Net from "node:net";
3import { assert, describe, it } from "@effect/vitest";3import { assert, describe, it } from "@effect/vitest";
4import { Effect } from "effect";4import { Effect } from "effect";
55
6import { NetError, NetService } from "./Net";6import { NetError, NetService } from "./Net.ts";
77
8const closeServer = (server: Net.Server) =>8const closeServer = (server: Net.Server) =>
9 Effect.sync(() => {9 Effect.sync(() => {
packages/shared/src/String.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { truncate } from "./String";3import { truncate } from "./String.ts";
44
5describe("truncate", () => {5describe("truncate", () => {
6 it("trims surrounding whitespace", () => {6 it("trims surrounding whitespace", () => {
packages/shared/src/cliArgs.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { parseCliArgs } from "./cliArgs";3import { parseCliArgs } from "./cliArgs.ts";
44
5describe("parseCliArgs", () => {5describe("parseCliArgs", () => {
6 it("returns empty result for empty string", () => {6 it("returns empty result for empty string", () => {
packages/shared/src/git.test.ts+1-1
...@@ -8,7 +8,7 @@ import {...@@ -8,7 +8,7 @@ import {
8 normalizeGitRemoteUrl,8 normalizeGitRemoteUrl,
9 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,9 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,
10 WORKTREE_BRANCH_PREFIX,10 WORKTREE_BRANCH_PREFIX,
11} from "./git";11} from "./git.ts";
1212
13describe("normalizeGitRemoteUrl", () => {13describe("normalizeGitRemoteUrl", () => {
14 it("canonicalizes equivalent GitHub remotes across protocol variants", () => {14 it("canonicalizes equivalent GitHub remotes across protocol variants", () => {
packages/shared/src/model.test.ts+1-1
...@@ -18,7 +18,7 @@ import {...@@ -18,7 +18,7 @@ import {
18 resolveModelSlugForProvider,18 resolveModelSlugForProvider,
19 resolveSelectableModel,19 resolveSelectableModel,
20 trimOrNull,20 trimOrNull,
21} from "./model";21} from "./model.ts";
2222
23const codexCaps: ModelCapabilities = {23const codexCaps: ModelCapabilities = {
24 reasoningEffortLevels: [24 reasoningEffortLevels: [
packages/shared/src/path.test.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 isUncPath,4 isUncPath,
5 isWindowsAbsolutePath,5 isWindowsAbsolutePath,
6 isWindowsDrivePath,6 isWindowsDrivePath,
7} from "./path";7} from "./path.ts";
88
9describe("path helpers", () => {9describe("path helpers", () => {
10 it("detects windows drive paths", () => {10 it("detects windows drive paths", () => {
packages/shared/src/qrCode.ts+856-855
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1// @ts-nocheck
2/* oxlint-disable eslint/no-useless-escape */1/* oxlint-disable eslint/no-useless-escape */
3/*2/*
4 * QR Code generator library (TypeScript)3 * QR Code generator library (TypeScript)
...@@ -25,960 +24,962 @@...@@ -25,960 +24,962 @@
2524
26"use strict";25"use strict";
2726
28namespace qrcodegen {27type bit = number;
29 type bit = number;28type byte = number;
30 type byte = number;29type int = number;
31 type int = number;
32
33 /*---- QR Code symbol class ----*/
34
35 /*
36 * A QR Code symbol, which is a type of two-dimension barcode.
37 * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
38 * Instances of this class represent an immutable square grid of dark and light cells.
39 * The class provides static factory functions to create a QR Code from text or binary data.
40 * The class covers the QR Code Model 2 specification, supporting all versions (sizes)
41 * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
42 *
43 * Ways to create a QR Code object:
44 * - High level: Take the payload data and call QrCode.encodeText() or QrCode.encodeBinary().
45 * - Mid level: Custom-make the list of segments and call QrCode.encodeSegments().
46 * - Low level: Custom-make the array of data codeword bytes (including
47 * segment headers and final padding, excluding error correction codewords),
48 * supply the appropriate version number, and call the QrCode() constructor.
49 * (Note that all ways require supplying the desired error correction level.)
50 */
51 export class QrCode {
52 /*-- Static factory functions (high level) --*/
53
54 // Returns a QR Code representing the given Unicode text string at the given error correction level.
55 // As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
56 // Unicode code points (not UTF-16 code units) if the low error correction level is used. The smallest possible
57 // QR Code version is automatically chosen for the output. The ECC level of the result may be higher than the
58 // ecl argument if it can be done without increasing the version.
59 public static encodeText(text: string, ecl: QrCode.Ecc): QrCode {
60 const segs: Array<QrSegment> = qrcodegen.QrSegment.makeSegments(text);
61 return QrCode.encodeSegments(segs, ecl);
62 }
6330
64 // Returns a QR Code representing the given binary data at the given error correction level.31/*---- QR Code symbol class ----*/
65 // This function always encodes using the binary segment mode, not any text mode. The maximum number of
66 // bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
67 // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
68 public static encodeBinary(data: Readonly<Array<byte>>, ecl: QrCode.Ecc): QrCode {
69 const seg: QrSegment = qrcodegen.QrSegment.makeBytes(data);
70 return QrCode.encodeSegments([seg], ecl);
71 }
7232
73 /*-- Static factory functions (mid level) --*/33/*
7434 * A QR Code symbol, which is a type of two-dimension barcode.
75 // Returns a QR Code representing the given segments with the given encoding parameters.35 * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
76 // The smallest possible QR Code version within the given range is automatically36 * Instances of this class represent an immutable square grid of dark and light cells.
77 // chosen for the output. Iff boostEcl is true, then the ECC level of the result37 * The class provides static factory functions to create a QR Code from text or binary data.
78 // may be higher than the ecl argument if it can be done without increasing the38 * The class covers the QR Code Model 2 specification, supporting all versions (sizes)
79 // version. The mask number is either between 0 to 7 (inclusive) to force that39 * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
80 // mask, or -1 to automatically choose an appropriate mask (which may be slow).40 *
81 // This function allows the user to create a custom sequence of segments that switches41 * Ways to create a QR Code object:
82 // between modes (such as alphanumeric and byte) to encode text in less space.42 * - High level: Take the payload data and call QrCode.encodeText() or QrCode.encodeBinary().
83 // This is a mid-level API; the high-level API is encodeText() and encodeBinary().43 * - Mid level: Custom-make the list of segments and call QrCode.encodeSegments().
84 public static encodeSegments(44 * - Low level: Custom-make the array of data codeword bytes (including
85 segs: Readonly<Array<QrSegment>>,45 * segment headers and final padding, excluding error correction codewords),
86 ecl: QrCode.Ecc,46 * supply the appropriate version number, and call the QrCode() constructor.
87 minVersion: int = 1,47 * (Note that all ways require supplying the desired error correction level.)
88 maxVersion: int = 40,48 */
89 mask: int = -1,49export class QrCode {
90 boostEcl: boolean = true,50 public static Ecc: typeof QrCodeEcc;
91 ): QrCode {51
92 if (52 /*-- Static factory functions (high level) --*/
93 !(53
94 QrCode.MIN_VERSION <= minVersion &&54 // Returns a QR Code representing the given Unicode text string at the given error correction level.
95 minVersion <= maxVersion &&55 // As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
96 maxVersion <= QrCode.MAX_VERSION56 // Unicode code points (not UTF-16 code units) if the low error correction level is used. The smallest possible
97 ) ||57 // QR Code version is automatically chosen for the output. The ECC level of the result may be higher than the
98 mask < -1 ||58 // ecl argument if it can be done without increasing the version.
99 mask > 759 public static encodeText(text: string, ecl: QrCodeEcc): QrCode {
100 )60 const segs: Array<QrSegment> = QrSegment.makeSegments(text);
101 throw new RangeError("Invalid value");61 return QrCode.encodeSegments(segs, ecl);
10262 }
103 // Find the minimal version number to use
104 let version: int;
105 let dataUsedBits: int;
106 for (version = minVersion; ; version++) {
107 const dataCapacityBits: int = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
108 const usedBits: number = QrSegment.getTotalBits(segs, version);
109 if (usedBits <= dataCapacityBits) {
110 dataUsedBits = usedBits;
111 break; // This version number is found to be suitable
112 }
113 if (version >= maxVersion)
114 // All versions in the range could not fit the given data
115 throw new RangeError("Data too long");
116 }
11763
118 // Increase the error correction level while the data still fits in the current version number64 // Returns a QR Code representing the given binary data at the given error correction level.
119 for (const newEcl of [QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH]) {65 // This function always encodes using the binary segment mode, not any text mode. The maximum number of
120 // From low to high66 // bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
121 if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8)67 // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
122 ecl = newEcl;68 public static encodeBinary(data: Readonly<Array<byte>>, ecl: QrCodeEcc): QrCode {
123 }69 const seg: QrSegment = QrSegment.makeBytes(data);
70 return QrCode.encodeSegments([seg], ecl);
71 }
12472
125 // Concatenate all segments to create the data bit string73 /*-- Static factory functions (mid level) --*/
126 let bb: Array<bit> = [];74
127 for (const seg of segs) {75 // Returns a QR Code representing the given segments with the given encoding parameters.
128 appendBits(seg.mode.modeBits, 4, bb);76 // The smallest possible QR Code version within the given range is automatically
129 appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);77 // chosen for the output. Iff boostEcl is true, then the ECC level of the result
130 for (const b of seg.getData()) bb.push(b);78 // may be higher than the ecl argument if it can be done without increasing the
79 // version. The mask number is either between 0 to 7 (inclusive) to force that
80 // mask, or -1 to automatically choose an appropriate mask (which may be slow).
81 // This function allows the user to create a custom sequence of segments that switches
82 // between modes (such as alphanumeric and byte) to encode text in less space.
83 // This is a mid-level API; the high-level API is encodeText() and encodeBinary().
84 public static encodeSegments(
85 segs: Readonly<Array<QrSegment>>,
86 ecl: QrCodeEcc,
87 minVersion: int = 1,
88 maxVersion: int = 40,
89 mask: int = -1,
90 boostEcl: boolean = true,
91 ): QrCode {
92 if (
93 !(
94 QrCode.MIN_VERSION <= minVersion &&
95 minVersion <= maxVersion &&
96 maxVersion <= QrCode.MAX_VERSION
97 ) ||
98 mask < -1 ||
99 mask > 7
100 )
101 throw new RangeError("Invalid value");
102
103 // Find the minimal version number to use
104 let version: int;
105 let dataUsedBits: int;
106 for (version = minVersion; ; version++) {
107 const dataCapacityBits: int = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
108 const usedBits: number = QrSegment.getTotalBits(segs, version);
109 if (usedBits <= dataCapacityBits) {
110 dataUsedBits = usedBits;
111 break; // This version number is found to be suitable
131 }112 }
132 assert(bb.length == dataUsedBits);113 if (version >= maxVersion)
133114 // All versions in the range could not fit the given data
134 // Add terminator and pad up to a byte if applicable115 throw new RangeError("Data too long");
135 const dataCapacityBits: int = QrCode.getNumDataCodewords(version, ecl) * 8;
136 assert(bb.length <= dataCapacityBits);
137 appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
138 appendBits(0, (8 - (bb.length % 8)) % 8, bb);
139 assert(bb.length % 8 == 0);
140
141 // Pad with alternating bytes until data capacity is reached
142 for (let padByte = 0xec; bb.length < dataCapacityBits; padByte ^= 0xec ^ 0x11)
143 appendBits(padByte, 8, bb);
144
145 // Pack bits into bytes in big endian
146 let dataCodewords: Array<byte> = [];
147 while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
148 bb.forEach((b: bit, i: int) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7))));
149
150 // Create the QR Code object
151 return new QrCode(version, ecl, dataCodewords, mask);
152 }116 }
153117
154 /*-- Fields --*/118 // Increase the error correction level while the data still fits in the current version number
155119 for (const newEcl of [QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH]) {
156 // The width and height of this QR Code, measured in modules, between120 // From low to high
157 // 21 and 177 (inclusive). This is equal to version * 4 + 17.121 if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8) ecl = newEcl;
158 public readonly size: int;122 }
159
160 // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
161 // Even if a QR Code is created with automatic masking requested (mask = -1),
162 // the resulting object still has a mask value between 0 and 7.
163 public readonly mask: int;
164
165 // The modules of this QR Code (false = light, true = dark).
166 // Immutable after constructor finishes. Accessed through getModule().
167 private readonly modules: Array<Array<boolean>> = [];
168
169 // Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
170 private readonly isFunction: Array<Array<boolean>> = [];
171
172 /*-- Constructor (low level) and fields --*/
173
174 // Creates a new QR Code with the given version number,
175 // error correction level, data codeword bytes, and mask number.
176 // This is a low-level API that most users should not use directly.
177 // A mid-level API is the encodeSegments() function.
178 public constructor(
179 // The version number of this QR Code, which is between 1 and 40 (inclusive).
180 // This determines the size of this barcode.
181 public readonly version: int,
182
183 // The error correction level used in this QR Code.
184 public readonly errorCorrectionLevel: QrCode.Ecc,
185
186 dataCodewords: Readonly<Array<byte>>,
187
188 msk: int,
189 ) {
190 // Check scalar arguments
191 if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
192 throw new RangeError("Version value out of range");
193 if (msk < -1 || msk > 7) throw new RangeError("Mask value out of range");
194 this.size = version * 4 + 17;
195
196 // Initialize both grids to be size*size arrays of Boolean false
197 let row: Array<boolean> = [];
198 for (let i = 0; i < this.size; i++) row.push(false);
199 for (let i = 0; i < this.size; i++) {
200 this.modules.push(row.slice()); // Initially all light
201 this.isFunction.push(row.slice());
202 }
203
204 // Compute ECC, draw modules
205 this.drawFunctionPatterns();
206 const allCodewords: Array<byte> = this.addEccAndInterleave(dataCodewords);
207 this.drawCodewords(allCodewords);
208
209 // Do masking
210 if (msk == -1) {
211 // Automatically choose best mask
212 let minPenalty: int = 1000000000;
213 for (let i = 0; i < 8; i++) {
214 this.applyMask(i);
215 this.drawFormatBits(i);
216 const penalty: int = this.getPenaltyScore();
217 if (penalty < minPenalty) {
218 msk = i;
219 minPenalty = penalty;
220 }
221 this.applyMask(i); // Undoes the mask due to XOR
222 }
223 }
224 assert(0 <= msk && msk <= 7);
225 this.mask = msk;
226 this.applyMask(msk); // Apply the final choice of mask
227 this.drawFormatBits(msk); // Overwrite old format bits
228123
229 this.isFunction = [];124 // Concatenate all segments to create the data bit string
125 let bb: Array<bit> = [];
126 for (const seg of segs) {
127 appendBits(seg.mode.modeBits, 4, bb);
128 appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
129 for (const b of seg.getData()) bb.push(b);
230 }130 }
131 assert(bb.length == dataUsedBits);
231132
232 /*-- Accessor methods --*/133 // Add terminator and pad up to a byte if applicable
134 const dataCapacityBits: int = QrCode.getNumDataCodewords(version, ecl) * 8;
135 assert(bb.length <= dataCapacityBits);
136 appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
137 appendBits(0, (8 - (bb.length % 8)) % 8, bb);
138 assert(bb.length % 8 == 0);
233139
234 // Returns the color of the module (pixel) at the given coordinates, which is false140 // Pad with alternating bytes until data capacity is reached
235 // for light or true for dark. The top left corner has the coordinates (x=0, y=0).141 for (let padByte = 0xec; bb.length < dataCapacityBits; padByte ^= 0xec ^ 0x11)
236 // If the given coordinates are out of bounds, then false (light) is returned.142 appendBits(padByte, 8, bb);
237 public getModule(x: int, y: int): boolean {
238 return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x];
239 }
240143
241 /*-- Private helper methods for constructor: Drawing function modules --*/144 // Pack bits into bytes in big endian
145 let dataCodewords: Array<byte> = [];
146 while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
147 bb.forEach((b: bit, i: int) => (dataCodewords[i >>> 3]! |= b << (7 - (i & 7))));
242148
243 // Reads this object's version field, and draws and marks all function modules.149 // Create the QR Code object
244 private drawFunctionPatterns(): void {150 return new QrCode(version, ecl, dataCodewords, mask);
245 // Draw horizontal and vertical timing patterns151 }
246 for (let i = 0; i < this.size; i++) {
247 this.setFunctionModule(6, i, i % 2 == 0);
248 this.setFunctionModule(i, 6, i % 2 == 0);
249 }
250152
251 // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)153 /*-- Fields --*/
252 this.drawFinderPattern(3, 3);154
253 this.drawFinderPattern(this.size - 4, 3);155 // The width and height of this QR Code, measured in modules, between
254 this.drawFinderPattern(3, this.size - 4);156 // 21 and 177 (inclusive). This is equal to version * 4 + 17.
255157 public readonly version: int;
256 // Draw numerous alignment patterns158 public readonly errorCorrectionLevel: QrCodeEcc;
257 const alignPatPos: Array<int> = this.getAlignmentPatternPositions();159 public readonly size: int;
258 const numAlign: int = alignPatPos.length;160
259 for (let i = 0; i < numAlign; i++) {161 // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
260 for (let j = 0; j < numAlign; j++) {162 // Even if a QR Code is created with automatic masking requested (mask = -1),
261 // Don't draw on the three finder corners163 // the resulting object still has a mask value between 0 and 7.
262 if (164 public readonly mask: int;
263 !((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))165
264 )166 // The modules of this QR Code (false = light, true = dark).
265 this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);167 // Immutable after constructor finishes. Accessed through getModule().
168 private readonly modules: Array<Array<boolean>> = [];
169
170 // Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
171 private readonly isFunction: Array<Array<boolean>> = [];
172
173 /*-- Constructor (low level) and fields --*/
174
175 // Creates a new QR Code with the given version number,
176 // error correction level, data codeword bytes, and mask number.
177 // This is a low-level API that most users should not use directly.
178 // A mid-level API is the encodeSegments() function.
179 public constructor(
180 // The version number of this QR Code, which is between 1 and 40 (inclusive).
181 // This determines the size of this barcode.
182 version: int,
183
184 // The error correction level used in this QR Code.
185 errorCorrectionLevel: QrCodeEcc,
186
187 dataCodewords: Readonly<Array<byte>>,
188
189 msk: int,
190 ) {
191 this.version = version;
192 this.errorCorrectionLevel = errorCorrectionLevel;
193
194 // Check scalar arguments
195 if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
196 throw new RangeError("Version value out of range");
197 if (msk < -1 || msk > 7) throw new RangeError("Mask value out of range");
198 this.size = version * 4 + 17;
199
200 // Initialize both grids to be size*size arrays of Boolean false
201 let row: Array<boolean> = [];
202 for (let i = 0; i < this.size; i++) row.push(false);
203 for (let i = 0; i < this.size; i++) {
204 this.modules.push(row.slice()); // Initially all light
205 this.isFunction.push(row.slice());
206 }
207
208 // Compute ECC, draw modules
209 this.drawFunctionPatterns();
210 const allCodewords: Array<byte> = this.addEccAndInterleave(dataCodewords);
211 this.drawCodewords(allCodewords);
212
213 // Do masking
214 if (msk == -1) {
215 // Automatically choose best mask
216 let minPenalty: int = 1000000000;
217 for (let i = 0; i < 8; i++) {
218 this.applyMask(i);
219 this.drawFormatBits(i);
220 const penalty: int = this.getPenaltyScore();
221 if (penalty < minPenalty) {
222 msk = i;
223 minPenalty = penalty;
266 }224 }
225 this.applyMask(i); // Undoes the mask due to XOR
267 }226 }
268
269 // Draw configuration data
270 this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
271 this.drawVersion();
272 }227 }
228 assert(0 <= msk && msk <= 7);
229 this.mask = msk;
230 this.applyMask(msk); // Apply the final choice of mask
231 this.drawFormatBits(msk); // Overwrite old format bits
273232
274 // Draws two copies of the format bits (with its own error correction code)233 this.isFunction = [];
275 // based on the given mask and this object's error correction level field.234 }
276 private drawFormatBits(mask: int): void {235
277 // Calculate error correction code and pack bits236 /*-- Accessor methods --*/
278 const data: int = (this.errorCorrectionLevel.formatBits << 3) | mask; // errCorrLvl is uint2, mask is uint3237
279 let rem: int = data;238 // Returns the color of the module (pixel) at the given coordinates, which is false
280 for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);239 // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
281 const bits = ((data << 10) | rem) ^ 0x5412; // uint15240 // If the given coordinates are out of bounds, then false (light) is returned.
282 assert(bits >>> 15 == 0);241 public getModule(x: int, y: int): boolean {
283242 return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y]![x]!;
284 // Draw first copy243 }
285 for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
286 this.setFunctionModule(8, 7, getBit(bits, 6));
287 this.setFunctionModule(8, 8, getBit(bits, 7));
288 this.setFunctionModule(7, 8, getBit(bits, 8));
289 for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, getBit(bits, i));
290
291 // Draw second copy
292 for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
293 for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
294 this.setFunctionModule(8, this.size - 8, true); // Always dark
295 }
296244
297 // Draws two copies of the version bits (with its own error correction code),245 /*-- Private helper methods for constructor: Drawing function modules --*/
298 // based on this object's version field, iff 7 <= version <= 40.246
299 private drawVersion(): void {247 // Reads this object's version field, and draws and marks all function modules.
300 if (this.version < 7) return;248 private drawFunctionPatterns(): void {
301249 // Draw horizontal and vertical timing patterns
302 // Calculate error correction code and pack bits250 for (let i = 0; i < this.size; i++) {
303 let rem: int = this.version; // version is uint6, in the range [7, 40]251 this.setFunctionModule(6, i, i % 2 == 0);
304 for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);252 this.setFunctionModule(i, 6, i % 2 == 0);
305 const bits: int = (this.version << 12) | rem; // uint18253 }
306 assert(bits >>> 18 == 0);254
307255 // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
308 // Draw two copies256 this.drawFinderPattern(3, 3);
309 for (let i = 0; i < 18; i++) {257 this.drawFinderPattern(this.size - 4, 3);
310 const color: boolean = getBit(bits, i);258 this.drawFinderPattern(3, this.size - 4);
311 const a: int = this.size - 11 + (i % 3);259
312 const b: int = Math.floor(i / 3);260 // Draw numerous alignment patterns
313 this.setFunctionModule(a, b, color);261 const alignPatPos: Array<int> = this.getAlignmentPatternPositions();
314 this.setFunctionModule(b, a, color);262 const numAlign: int = alignPatPos.length;
263 for (let i = 0; i < numAlign; i++) {
264 for (let j = 0; j < numAlign; j++) {
265 // Don't draw on the three finder corners
266 if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0)))
267 this.drawAlignmentPattern(alignPatPos[i]!, alignPatPos[j]!);
315 }268 }
316 }269 }
317270
318 // Draws a 9*9 finder pattern including the border separator,271 // Draw configuration data
319 // with the center module at (x, y). Modules can be out of bounds.272 this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
320 private drawFinderPattern(x: int, y: int): void {273 this.drawVersion();
321 for (let dy = -4; dy <= 4; dy++) {274 }
322 for (let dx = -4; dx <= 4; dx++) {275
323 const dist: int = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm276 // Draws two copies of the format bits (with its own error correction code)
324 const xx: int = x + dx;277 // based on the given mask and this object's error correction level field.
325 const yy: int = y + dy;278 private drawFormatBits(mask: int): void {
326 if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size)279 // Calculate error correction code and pack bits
327 this.setFunctionModule(xx, yy, dist != 2 && dist != 4);280 const data: int = (this.errorCorrectionLevel.formatBits << 3) | mask; // errCorrLvl is uint2, mask is uint3
328 }281 let rem: int = data;
329 }282 for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
283 const bits = ((data << 10) | rem) ^ 0x5412; // uint15
284 assert(bits >>> 15 == 0);
285
286 // Draw first copy
287 for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
288 this.setFunctionModule(8, 7, getBit(bits, 6));
289 this.setFunctionModule(8, 8, getBit(bits, 7));
290 this.setFunctionModule(7, 8, getBit(bits, 8));
291 for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, getBit(bits, i));
292
293 // Draw second copy
294 for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
295 for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
296 this.setFunctionModule(8, this.size - 8, true); // Always dark
297 }
298
299 // Draws two copies of the version bits (with its own error correction code),
300 // based on this object's version field, iff 7 <= version <= 40.
301 private drawVersion(): void {
302 if (this.version < 7) return;
303
304 // Calculate error correction code and pack bits
305 let rem: int = this.version; // version is uint6, in the range [7, 40]
306 for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
307 const bits: int = (this.version << 12) | rem; // uint18
308 assert(bits >>> 18 == 0);
309
310 // Draw two copies
311 for (let i = 0; i < 18; i++) {
312 const color: boolean = getBit(bits, i);
313 const a: int = this.size - 11 + (i % 3);
314 const b: int = Math.floor(i / 3);
315 this.setFunctionModule(a, b, color);
316 this.setFunctionModule(b, a, color);
330 }317 }
318 }
331319
332 // Draws a 5*5 alignment pattern, with the center module320 // Draws a 9*9 finder pattern including the border separator,
333 // at (x, y). All modules must be in bounds.321 // with the center module at (x, y). Modules can be out of bounds.
334 private drawAlignmentPattern(x: int, y: int): void {322 private drawFinderPattern(x: int, y: int): void {
335 for (let dy = -2; dy <= 2; dy++) {323 for (let dy = -4; dy <= 4; dy++) {
336 for (let dx = -2; dx <= 2; dx++)324 for (let dx = -4; dx <= 4; dx++) {
337 this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);325 const dist: int = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm
326 const xx: int = x + dx;
327 const yy: int = y + dy;
328 if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size)
329 this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
338 }330 }
339 }331 }
332 }
340333
341 // Sets the color of a module and marks it as a function module.334 // Draws a 5*5 alignment pattern, with the center module
342 // Only used by the constructor. Coordinates must be in bounds.335 // at (x, y). All modules must be in bounds.
343 private setFunctionModule(x: int, y: int, isDark: boolean): void {336 private drawAlignmentPattern(x: int, y: int): void {
344 this.modules[y][x] = isDark;337 for (let dy = -2; dy <= 2; dy++) {
345 this.isFunction[y][x] = true;338 for (let dx = -2; dx <= 2; dx++)
339 this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
346 }340 }
341 }
347342
348 /*-- Private helper methods for constructor: Codewords and masking --*/343 // Sets the color of a module and marks it as a function module.
349344 // Only used by the constructor. Coordinates must be in bounds.
350 // Returns a new byte string representing the given data with the appropriate error correction345 private setFunctionModule(x: int, y: int, isDark: boolean): void {
351 // codewords appended to it, based on this object's version and error correction level.346 this.modules[y]![x] = isDark;
352 private addEccAndInterleave(data: Readonly<Array<byte>>): Array<byte> {347 this.isFunction[y]![x] = true;
353 const ver: int = this.version;348 }
354 const ecl: QrCode.Ecc = this.errorCorrectionLevel;
355 if (data.length != QrCode.getNumDataCodewords(ver, ecl))
356 throw new RangeError("Invalid argument");
357
358 // Calculate parameter numbers
359 const numBlocks: int = QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
360 const blockEccLen: int = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver];
361 const rawCodewords: int = Math.floor(QrCode.getNumRawDataModules(ver) / 8);
362 const numShortBlocks: int = numBlocks - (rawCodewords % numBlocks);
363 const shortBlockLen: int = Math.floor(rawCodewords / numBlocks);
364
365 // Split data into blocks and append ECC to each block
366 let blocks: Array<Array<byte>> = [];
367 const rsDiv: Array<byte> = QrCode.reedSolomonComputeDivisor(blockEccLen);
368 for (let i = 0, k = 0; i < numBlocks; i++) {
369 let dat: Array<byte> = data.slice(
370 k,
371 k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1),
372 );
373 k += dat.length;
374 const ecc: Array<byte> = QrCode.reedSolomonComputeRemainder(dat, rsDiv);
375 if (i < numShortBlocks) dat.push(0);
376 blocks.push(dat.concat(ecc));
377 }
378349
379 // Interleave (not concatenate) the bytes from every block into a single sequence350 /*-- Private helper methods for constructor: Codewords and masking --*/
380 let result: Array<byte> = [];351
381 for (let i = 0; i < blocks[0].length; i++) {352 // Returns a new byte string representing the given data with the appropriate error correction
382 blocks.forEach((block, j) => {353 // codewords appended to it, based on this object's version and error correction level.
383 // Skip the padding byte in short blocks354 private addEccAndInterleave(data: Readonly<Array<byte>>): Array<byte> {
384 if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]);355 const ver: int = this.version;
385 });356 const ecl: QrCodeEcc = this.errorCorrectionLevel;
386 }357 if (data.length != QrCode.getNumDataCodewords(ver, ecl))
387 assert(result.length == rawCodewords);358 throw new RangeError("Invalid argument");
388 return result;359
389 }360 // Calculate parameter numbers
361 const numBlocks: int = QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal]![ver]!;
362 const blockEccLen: int = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal]![ver]!;
363 const rawCodewords: int = Math.floor(QrCode.getNumRawDataModules(ver) / 8);
364 const numShortBlocks: int = numBlocks - (rawCodewords % numBlocks);
365 const shortBlockLen: int = Math.floor(rawCodewords / numBlocks);
366
367 // Split data into blocks and append ECC to each block
368 let blocks: Array<Array<byte>> = [];
369 const rsDiv: Array<byte> = QrCode.reedSolomonComputeDivisor(blockEccLen);
370 for (let i = 0, k = 0; i < numBlocks; i++) {
371 let dat: Array<byte> = data.slice(
372 k,
373 k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1),
374 );
375 k += dat.length;
376 const ecc: Array<byte> = QrCode.reedSolomonComputeRemainder(dat, rsDiv);
377 if (i < numShortBlocks) dat.push(0);
378 blocks.push(dat.concat(ecc));
379 }
380
381 // Interleave (not concatenate) the bytes from every block into a single sequence
382 let result: Array<byte> = [];
383 for (let i = 0; i < blocks[0]!.length; i++) {
384 blocks.forEach((block, j) => {
385 // Skip the padding byte in short blocks
386 if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]!);
387 });
388 }
389 assert(result.length == rawCodewords);
390 return result;
391 }
390392
391 // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire393 // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
392 // data area of this QR Code. Function modules need to be marked off before this is called.394 // data area of this QR Code. Function modules need to be marked off before this is called.
393 private drawCodewords(data: Readonly<Array<byte>>): void {395 private drawCodewords(data: Readonly<Array<byte>>): void {
394 if (data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8))396 if (data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8))
395 throw new RangeError("Invalid argument");397 throw new RangeError("Invalid argument");
396 let i: int = 0; // Bit index into the data398 let i: int = 0; // Bit index into the data
397 // Do the funny zigzag scan399 // Do the funny zigzag scan
398 for (let right = this.size - 1; right >= 1; right -= 2) {400 for (let right = this.size - 1; right >= 1; right -= 2) {
399 // Index of right column in each column pair401 // Index of right column in each column pair
400 if (right == 6) right = 5;402 if (right == 6) right = 5;
401 for (let vert = 0; vert < this.size; vert++) {403 for (let vert = 0; vert < this.size; vert++) {
402 // Vertical counter404 // Vertical counter
403 for (let j = 0; j < 2; j++) {405 for (let j = 0; j < 2; j++) {
404 const x: int = right - j; // Actual x coordinate406 const x: int = right - j; // Actual x coordinate
405 const upward: boolean = ((right + 1) & 2) == 0;407 const upward: boolean = ((right + 1) & 2) == 0;
406 const y: int = upward ? this.size - 1 - vert : vert; // Actual y coordinate408 const y: int = upward ? this.size - 1 - vert : vert; // Actual y coordinate
407 if (!this.isFunction[y][x] && i < data.length * 8) {409 if (!this.isFunction[y]![x]! && i < data.length * 8) {
408 this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));410 this.modules[y]![x] = getBit(data[i >>> 3]!, 7 - (i & 7));
409 i++;411 i++;
410 }
411 // If this QR Code has any remainder bits (0 to 7), they were assigned as
412 // 0/false/light by the constructor and are left unchanged by this method
413 }412 }
413 // If this QR Code has any remainder bits (0 to 7), they were assigned as
414 // 0/false/light by the constructor and are left unchanged by this method
414 }415 }
415 }416 }
416 assert(i == data.length * 8);
417 }417 }
418 assert(i == data.length * 8);
419 }
418420
419 // XORs the codeword modules in this QR Code with the given mask pattern.421 // XORs the codeword modules in this QR Code with the given mask pattern.
420 // The function modules must be marked and the codeword bits must be drawn422 // The function modules must be marked and the codeword bits must be drawn
421 // before masking. Due to the arithmetic of XOR, calling applyMask() with423 // before masking. Due to the arithmetic of XOR, calling applyMask() with
422 // the same mask value a second time will undo the mask. A final well-formed424 // the same mask value a second time will undo the mask. A final well-formed
423 // QR Code needs exactly one (not zero, two, etc.) mask applied.425 // QR Code needs exactly one (not zero, two, etc.) mask applied.
424 private applyMask(mask: int): void {426 private applyMask(mask: int): void {
425 if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");427 if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");
426 for (let y = 0; y < this.size; y++) {428 for (let y = 0; y < this.size; y++) {
427 for (let x = 0; x < this.size; x++) {429 for (let x = 0; x < this.size; x++) {
428 let invert: boolean;430 let invert: boolean;
429 switch (mask) {431 switch (mask) {
430 case 0:432 case 0:
431 invert = (x + y) % 2 == 0;433 invert = (x + y) % 2 == 0;
432 break;434 break;
433 case 1:435 case 1:
434 invert = y % 2 == 0;436 invert = y % 2 == 0;
435 break;437 break;
436 case 2:438 case 2:
437 invert = x % 3 == 0;439 invert = x % 3 == 0;
438 break;440 break;
439 case 3:441 case 3:
440 invert = (x + y) % 3 == 0;442 invert = (x + y) % 3 == 0;
441 break;443 break;
442 case 4:444 case 4:
443 invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;445 invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
444 break;446 break;
445 case 5:447 case 5:
446 invert = ((x * y) % 2) + ((x * y) % 3) == 0;448 invert = ((x * y) % 2) + ((x * y) % 3) == 0;
447 break;449 break;
448 case 6:450 case 6:
449 invert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;451 invert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
450 break;452 break;
451 case 7:453 case 7:
452 invert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;454 invert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
453 break;455 break;
454 default:456 default:
455 throw new Error("Unreachable");457 throw new Error("Unreachable");
456 }
457 if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x];
458 }458 }
459 if (!this.isFunction[y]![x]! && invert) this.modules[y]![x] = !this.modules[y]![x]!;
459 }460 }
460 }461 }
462 }
461463
462 // Calculates and returns the penalty score based on state of this QR Code's current modules.464 // Calculates and returns the penalty score based on state of this QR Code's current modules.
463 // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.465 // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
464 private getPenaltyScore(): int {466 private getPenaltyScore(): int {
465 let result: int = 0;467 let result: int = 0;
466468
467 // Adjacent modules in row having same color, and finder-like patterns469 // Adjacent modules in row having same color, and finder-like patterns
468 for (let y = 0; y < this.size; y++) {470 for (let y = 0; y < this.size; y++) {
469 let runColor = false;471 let runColor = false;
470 let runX = 0;472 let runX = 0;
471 let runHistory = [0, 0, 0, 0, 0, 0, 0];473 let runHistory = [0, 0, 0, 0, 0, 0, 0];
472 for (let x = 0; x < this.size; x++) {
473 if (this.modules[y][x] == runColor) {
474 runX++;
475 if (runX == 5) result += QrCode.PENALTY_N1;
476 else if (runX > 5) result++;
477 } else {
478 this.finderPenaltyAddHistory(runX, runHistory);
479 if (!runColor)
480 result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
481 runColor = this.modules[y][x];
482 runX = 1;
483 }
484 }
485 result +=
486 this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * QrCode.PENALTY_N3;
487 }
488 // Adjacent modules in column having same color, and finder-like patterns
489 for (let x = 0; x < this.size; x++) {474 for (let x = 0; x < this.size; x++) {
490 let runColor = false;475 if (this.modules[y]![x] === runColor) {
491 let runY = 0;476 runX++;
492 let runHistory = [0, 0, 0, 0, 0, 0, 0];477 if (runX == 5) result += QrCode.PENALTY_N1;
493 for (let y = 0; y < this.size; y++) {478 else if (runX > 5) result++;
494 if (this.modules[y][x] == runColor) {479 } else {
495 runY++;480 this.finderPenaltyAddHistory(runX, runHistory);
496 if (runY == 5) result += QrCode.PENALTY_N1;481 if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
497 else if (runY > 5) result++;482 runColor = this.modules[y]![x]!;
498 } else {483 runX = 1;
499 this.finderPenaltyAddHistory(runY, runHistory);
500 if (!runColor)
501 result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
502 runColor = this.modules[y][x];
503 runY = 1;
504 }
505 }484 }
506 result +=
507 this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * QrCode.PENALTY_N3;
508 }485 }
509486 result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * QrCode.PENALTY_N3;
510 // 2*2 blocks of modules having same color487 }
511 for (let y = 0; y < this.size - 1; y++) {488 // Adjacent modules in column having same color, and finder-like patterns
512 for (let x = 0; x < this.size - 1; x++) {489 for (let x = 0; x < this.size; x++) {
513 const color: boolean = this.modules[y][x];490 let runColor = false;
514 if (491 let runY = 0;
515 color == this.modules[y][x + 1] &&492 let runHistory = [0, 0, 0, 0, 0, 0, 0];
516 color == this.modules[y + 1][x] &&493 for (let y = 0; y < this.size; y++) {
517 color == this.modules[y + 1][x + 1]494 if (this.modules[y]![x] === runColor) {
518 )495 runY++;
519 result += QrCode.PENALTY_N2;496 if (runY == 5) result += QrCode.PENALTY_N1;
497 else if (runY > 5) result++;
498 } else {
499 this.finderPenaltyAddHistory(runY, runHistory);
500 if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
501 runColor = this.modules[y]![x]!;
502 runY = 1;
520 }503 }
521 }504 }
522505 result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * QrCode.PENALTY_N3;
523 // Balance of dark and light modules506 }
524 let dark: int = 0;507
525 for (const row of this.modules)508 // 2*2 blocks of modules having same color
526 dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);509 for (let y = 0; y < this.size - 1; y++) {
527 const total: int = this.size * this.size; // Note that size is odd, so dark/total != 1/2510 for (let x = 0; x < this.size - 1; x++) {
528 // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%511 const color: boolean = this.modules[y]![x]!;
529 const k: int = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;512 if (
530 assert(0 <= k && k <= 9);513 color == this.modules[y]![x + 1]! &&
531 result += k * QrCode.PENALTY_N4;514 color == this.modules[y + 1]![x]! &&
532 assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4515 color == this.modules[y + 1]![x + 1]!
533 return result;516 )
534 }517 result += QrCode.PENALTY_N2;
535
536 /*-- Private helper functions --*/
537
538 // Returns an ascending list of positions of alignment patterns for this version number.
539 // Each position is in the range [0,177), and are used on both the x and y axes.
540 // This could be implemented as lookup table of 40 variable-length lists of integers.
541 private getAlignmentPatternPositions(): Array<int> {
542 if (this.version == 1) return [];
543 else {
544 const numAlign: int = Math.floor(this.version / 7) + 2;
545 const step: int =
546 this.version == 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
547 let result: Array<int> = [6];
548 for (let pos = this.size - 7; result.length < numAlign; pos -= step)
549 result.splice(1, 0, pos);
550 return result;
551 }518 }
552 }519 }
553520
554 // Returns the number of data bits that can be stored in a QR Code of the given version number, after521 // Balance of dark and light modules
555 // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.522 let dark: int = 0;
556 // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.523 for (const row of this.modules) dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
557 private static getNumRawDataModules(ver: int): int {524 const total: int = this.size * this.size; // Note that size is odd, so dark/total != 1/2
558 if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)525 // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
559 throw new RangeError("Version number out of range");526 const k: int = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
560 let result: int = (16 * ver + 128) * ver + 64;527 assert(0 <= k && k <= 9);
561 if (ver >= 2) {528 result += k * QrCode.PENALTY_N4;
562 const numAlign: int = Math.floor(ver / 7) + 2;529 assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
563 result -= (25 * numAlign - 10) * numAlign - 55;530 return result;
564 if (ver >= 7) result -= 36;531 }
565 }532
566 assert(208 <= result && result <= 29648);533 /*-- Private helper functions --*/
534
535 // Returns an ascending list of positions of alignment patterns for this version number.
536 // Each position is in the range [0,177), and are used on both the x and y axes.
537 // This could be implemented as lookup table of 40 variable-length lists of integers.
538 private getAlignmentPatternPositions(): Array<int> {
539 if (this.version == 1) return [];
540 else {
541 const numAlign: int = Math.floor(this.version / 7) + 2;
542 const step: int =
543 this.version == 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
544 let result: Array<int> = [6];
545 for (let pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
567 return result;546 return result;
568 }547 }
548 }
569549
570 // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any550 // Returns the number of data bits that can be stored in a QR Code of the given version number, after
571 // QR Code of the given version number and error correction level, with remainder bits discarded.551 // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
572 // This stateless pure function could be implemented as a (40*4)-cell lookup table.552 // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
573 private static getNumDataCodewords(ver: int, ecl: QrCode.Ecc): int {553 private static getNumRawDataModules(ver: int): int {
574 return (554 if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)
575 Math.floor(QrCode.getNumRawDataModules(ver) / 8) -555 throw new RangeError("Version number out of range");
576 QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] *556 let result: int = (16 * ver + 128) * ver + 64;
577 QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]557 if (ver >= 2) {
578 );558 const numAlign: int = Math.floor(ver / 7) + 2;
579 }559 result -= (25 * numAlign - 10) * numAlign - 55;
560 if (ver >= 7) result -= 36;
561 }
562 assert(208 <= result && result <= 29648);
563 return result;
564 }
580565
581 // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be566 // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
582 // implemented as a lookup table over all possible parameter values, instead of as an algorithm.567 // QR Code of the given version number and error correction level, with remainder bits discarded.
583 private static reedSolomonComputeDivisor(degree: int): Array<byte> {568 // This stateless pure function could be implemented as a (40*4)-cell lookup table.
584 if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");569 private static getNumDataCodewords(ver: int, ecl: QrCodeEcc): int {
585 // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.570 return (
586 // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].571 Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
587 let result: Array<byte> = [];572 QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal]![ver]! *
588 for (let i = 0; i < degree - 1; i++) result.push(0);573 QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal]![ver]!
589 result.push(1); // Start off with the monomial x^0574 );
590575 }
591 // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
592 // and drop the highest monomial term which is always 1x^degree.
593 // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
594 let root = 1;
595 for (let i = 0; i < degree; i++) {
596 // Multiply the current product by (x - r^i)
597 for (let j = 0; j < result.length; j++) {
598 result[j] = QrCode.reedSolomonMultiply(result[j], root);
599 if (j + 1 < result.length) result[j] ^= result[j + 1];
600 }
601 root = QrCode.reedSolomonMultiply(root, 0x02);
602 }
603 return result;
604 }
605576
606 // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.577 // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be
607 private static reedSolomonComputeRemainder(578 // implemented as a lookup table over all possible parameter values, instead of as an algorithm.
608 data: Readonly<Array<byte>>,579 private static reedSolomonComputeDivisor(degree: int): Array<byte> {
609 divisor: Readonly<Array<byte>>,580 if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");
610 ): Array<byte> {581 // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
611 let result: Array<byte> = divisor.map((_) => 0);582 // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
612 for (const b of data) {583 let result: Array<byte> = [];
613 // Polynomial division584 for (let i = 0; i < degree - 1; i++) result.push(0);
614 const factor: byte = b ^ (result.shift() as byte);585 result.push(1); // Start off with the monomial x^0
615 result.push(0);586
616 divisor.forEach((coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)));587 // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
588 // and drop the highest monomial term which is always 1x^degree.
589 // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
590 let root = 1;
591 for (let i = 0; i < degree; i++) {
592 // Multiply the current product by (x - r^i)
593 for (let j = 0; j < result.length; j++) {
594 result[j] = QrCode.reedSolomonMultiply(result[j]!, root);
595 if (j + 1 < result.length) result[j]! ^= result[j + 1]!;
617 }596 }
618 return result;597 root = QrCode.reedSolomonMultiply(root, 0x02);
619 }598 }
599 return result;
600 }
620601
621 // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result602 // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
622 // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.603 private static reedSolomonComputeRemainder(
623 private static reedSolomonMultiply(x: byte, y: byte): byte {604 data: Readonly<Array<byte>>,
624 if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");605 divisor: Readonly<Array<byte>>,
625 // Russian peasant multiplication606 ): Array<byte> {
626 let z: int = 0;607 let result: Array<byte> = divisor.map((_) => 0);
627 for (let i = 7; i >= 0; i--) {608 for (const b of data) {
628 z = (z << 1) ^ ((z >>> 7) * 0x11d);609 // Polynomial division
629 z ^= ((y >>> i) & 1) * x;610 const factor: byte = b ^ (result.shift() as byte);
630 }611 result.push(0);
631 assert(z >>> 8 == 0);612 divisor.forEach((coef, i) => (result[i]! ^= QrCode.reedSolomonMultiply(coef, factor)));
632 return z as byte;613 }
633 }614 return result;
615 }
634616
635 // Can only be called immediately after a light run is added, and617 // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result
636 // returns either 0, 1, or 2. A helper function for getPenaltyScore().618 // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
637 private finderPenaltyCountPatterns(runHistory: Readonly<Array<int>>): int {619 private static reedSolomonMultiply(x: byte, y: byte): byte {
638 const n: int = runHistory[1];620 if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");
639 assert(n <= this.size * 3);621 // Russian peasant multiplication
640 const core: boolean =622 let z: int = 0;
641 n > 0 &&623 for (let i = 7; i >= 0; i--) {
642 runHistory[2] == n &&624 z = (z << 1) ^ ((z >>> 7) * 0x11d);
643 runHistory[3] == n * 3 &&625 z ^= ((y >>> i) & 1) * x;
644 runHistory[4] == n &&626 }
645 runHistory[5] == n;627 assert(z >>> 8 == 0);
646 return (628 return z as byte;
647 (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) +629 }
648 (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0)
649 );
650 }
651630
652 // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().631 // Can only be called immediately after a light run is added, and
653 private finderPenaltyTerminateAndCount(632 // returns either 0, 1, or 2. A helper function for getPenaltyScore().
654 currentRunColor: boolean,633 private finderPenaltyCountPatterns(runHistory: Readonly<Array<int>>): int {
655 currentRunLength: int,634 const n: int = runHistory[1]!;
656 runHistory: Array<int>,635 assert(n <= this.size * 3);
657 ): int {636 const core: boolean =
658 if (currentRunColor) {637 n > 0 &&
659 // Terminate dark run638 runHistory[2] === n &&
660 this.finderPenaltyAddHistory(currentRunLength, runHistory);639 runHistory[3] === n * 3 &&
661 currentRunLength = 0;640 runHistory[4] === n &&
662 }641 runHistory[5] === n;
663 currentRunLength += this.size; // Add light border to final run642 return (
643 (core && runHistory[0]! >= n * 4 && runHistory[6]! >= n ? 1 : 0) +
644 (core && runHistory[6]! >= n * 4 && runHistory[0]! >= n ? 1 : 0)
645 );
646 }
647
648 // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
649 private finderPenaltyTerminateAndCount(
650 currentRunColor: boolean,
651 currentRunLength: int,
652 runHistory: Array<int>,
653 ): int {
654 if (currentRunColor) {
655 // Terminate dark run
664 this.finderPenaltyAddHistory(currentRunLength, runHistory);656 this.finderPenaltyAddHistory(currentRunLength, runHistory);
665 return this.finderPenaltyCountPatterns(runHistory);657 currentRunLength = 0;
666 }658 }
659 currentRunLength += this.size; // Add light border to final run
660 this.finderPenaltyAddHistory(currentRunLength, runHistory);
661 return this.finderPenaltyCountPatterns(runHistory);
662 }
667663
668 // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().664 // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
669 private finderPenaltyAddHistory(currentRunLength: int, runHistory: Array<int>): void {665 private finderPenaltyAddHistory(currentRunLength: int, runHistory: Array<int>): void {
670 if (runHistory[0] == 0) currentRunLength += this.size; // Add light border to initial run666 if (runHistory[0] === 0) currentRunLength += this.size; // Add light border to initial run
671 runHistory.pop();667 runHistory.pop();
672 runHistory.unshift(currentRunLength);668 runHistory.unshift(currentRunLength);
673 }669 }
674670
675 /*-- Constants and tables --*/671 /*-- Constants and tables --*/
676672
677 // The minimum version number supported in the QR Code Model 2 standard.673 // The minimum version number supported in the QR Code Model 2 standard.
678 public static readonly MIN_VERSION: int = 1;674 public static readonly MIN_VERSION: int = 1;
679 // The maximum version number supported in the QR Code Model 2 standard.675 // The maximum version number supported in the QR Code Model 2 standard.
680 public static readonly MAX_VERSION: int = 40;676 public static readonly MAX_VERSION: int = 40;
681677
682 // For use in getPenaltyScore(), when evaluating which mask is best.678 // For use in getPenaltyScore(), when evaluating which mask is best.
683 private static readonly PENALTY_N1: int = 3;679 private static readonly PENALTY_N1: int = 3;
684 private static readonly PENALTY_N2: int = 3;680 private static readonly PENALTY_N2: int = 3;
685 private static readonly PENALTY_N3: int = 40;681 private static readonly PENALTY_N3: int = 40;
686 private static readonly PENALTY_N4: int = 10;682 private static readonly PENALTY_N4: int = 10;
687683
688 private static readonly ECC_CODEWORDS_PER_BLOCK: Array<Array<int>> = [684 private static readonly ECC_CODEWORDS_PER_BLOCK: Array<Array<int>> = [
689 // Version: (note that index 0 is for padding, and is set to an illegal value)685 // Version: (note that index 0 is for padding, and is set to an illegal value)
690 //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level686 //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
691 [687 [
692 -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28,688 -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30,
693 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,689 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
694 ], // Low690 ], // Low
695 [691 [
696 -1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28,692 -1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28,
697 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,693 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
698 ], // Medium694 ], // Medium
699 [695 [
700 -1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30,696 -1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30,
701 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,697 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
702 ], // Quartile698 ], // Quartile
703 [699 [
704 -1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24,700 -1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24,
705 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,701 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
706 ], // High702 ], // High
707 ];703 ];
708704
709 private static readonly NUM_ERROR_CORRECTION_BLOCKS: Array<Array<int>> = [705 private static readonly NUM_ERROR_CORRECTION_BLOCKS: Array<Array<int>> = [
710 // Version: (note that index 0 is for padding, and is set to an illegal value)706 // Version: (note that index 0 is for padding, and is set to an illegal value)
711 //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level707 //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
712 [708 [
713 -1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13,709 -1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13,
714 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25,710 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25,
715 ], // Low711 ], // Low
716 [712 [
717 -1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21,713 -1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23,
718 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49,714 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49,
719 ], // Medium715 ], // Medium
720 [716 [
721 -1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29,717 -1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29,
722 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68,718 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68,
723 ], // Quartile719 ], // Quartile
724 [720 [
725 -1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32,721 -1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35,
726 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81,722 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81,
727 ], // High723 ], // High
728 ];724 ];
729 }725}
730
731 // Appends the given number of low-order bits of the given value
732 // to the given buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
733 function appendBits(val: int, len: int, bb: Array<bit>): void {
734 if (len < 0 || len > 31 || val >>> len != 0) throw new RangeError("Value out of range");
735 for (
736 let i = len - 1;
737 i >= 0;
738 i-- // Append bit by bit
739 )
740 bb.push((val >>> i) & 1);
741 }
742
743 // Returns true iff the i'th bit of x is set to 1.
744 function getBit(x: int, i: int): boolean {
745 return ((x >>> i) & 1) != 0;
746 }
747
748 // Throws an exception if the given condition is false.
749 function assert(cond: boolean): void {
750 if (!cond) throw new Error("Assertion error");
751 }
752
753 /*---- Data segment class ----*/
754
755 /*
756 * A segment of character/binary/control data in a QR Code symbol.
757 * Instances of this class are immutable.
758 * The mid-level way to create a segment is to take the payload data
759 * and call a static factory function such as QrSegment.makeNumeric().
760 * The low-level way to create a segment is to custom-make the bit buffer
761 * and call the QrSegment() constructor with appropriate values.
762 * This segment class imposes no length restrictions, but QR Codes have restrictions.
763 * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
764 * Any segment longer than this is meaningless for the purpose of generating QR Codes.
765 */
766 export class QrSegment {
767 /*-- Static factory functions (mid level) --*/
768
769 // Returns a segment representing the given binary data encoded in
770 // byte mode. All input byte arrays are acceptable. Any text string
771 // can be converted to UTF-8 bytes and encoded as a byte mode segment.
772 public static makeBytes(data: Readonly<Array<byte>>): QrSegment {
773 let bb: Array<bit> = [];
774 for (const b of data) appendBits(b, 8, bb);
775 return new QrSegment(QrSegment.Mode.BYTE, data.length, bb);
776 }
777726
778 // Returns a segment representing the given string of decimal digits encoded in numeric mode.727// Appends the given number of low-order bits of the given value
779 public static makeNumeric(digits: string): QrSegment {728// to the given buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
780 if (!QrSegment.isNumeric(digits))729function appendBits(val: int, len: int, bb: Array<bit>): void {
781 throw new RangeError("String contains non-numeric characters");730 if (len < 0 || len > 31 || val >>> len != 0) throw new RangeError("Value out of range");
782 let bb: Array<bit> = [];731 for (
783 for (let i = 0; i < digits.length; ) {732 let i = len - 1;
784 // Consume up to 3 digits per iteration733 i >= 0;
785 const n: int = Math.min(digits.length - i, 3);734 i-- // Append bit by bit
786 appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);735 )
787 i += n;736 bb.push((val >>> i) & 1);
788 }737}
789 return new QrSegment(QrSegment.Mode.NUMERIC, digits.length, bb);
790 }
791738
792 // Returns a segment representing the given text string encoded in alphanumeric mode.739// Returns true iff the i'th bit of x is set to 1.
793 // The characters allowed are: 0 to 9, A to Z (uppercase only), space,740function getBit(x: int, i: int): boolean {
794 // dollar, percent, asterisk, plus, hyphen, period, slash, colon.741 return ((x >>> i) & 1) != 0;
795 public static makeAlphanumeric(text: string): QrSegment {742}
796 if (!QrSegment.isAlphanumeric(text))
797 throw new RangeError("String contains unencodable characters in alphanumeric mode");
798 let bb: Array<bit> = [];
799 let i: int;
800 for (i = 0; i + 2 <= text.length; i += 2) {
801 // Process groups of 2
802 let temp: int = QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
803 temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
804 appendBits(temp, 11, bb);
805 }
806 if (i < text.length)
807 // 1 character remaining
808 appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
809 return new QrSegment(QrSegment.Mode.ALPHANUMERIC, text.length, bb);
810 }
811743
812 // Returns a new mutable list of zero or more segments to represent the given Unicode text string.744// Throws an exception if the given condition is false.
813 // The result may use various segment modes and switch modes to optimize the length of the bit stream.745function assert(cond: boolean): void {
814 public static makeSegments(text: string): Array<QrSegment> {746 if (!cond) throw new Error("Assertion error");
815 // Select the most efficient segment encoding automatically747}
816 if (text == "") return [];
817 else if (QrSegment.isNumeric(text)) return [QrSegment.makeNumeric(text)];
818 else if (QrSegment.isAlphanumeric(text)) return [QrSegment.makeAlphanumeric(text)];
819 else return [QrSegment.makeBytes(QrSegment.toUtf8ByteArray(text))];
820 }
821748
822 // Returns a segment representing an Extended Channel Interpretation749/*---- Data segment class ----*/
823 // (ECI) designator with the given assignment value.
824 public static makeEci(assignVal: int): QrSegment {
825 let bb: Array<bit> = [];
826 if (assignVal < 0) throw new RangeError("ECI assignment value out of range");
827 else if (assignVal < 1 << 7) appendBits(assignVal, 8, bb);
828 else if (assignVal < 1 << 14) {
829 appendBits(0b10, 2, bb);
830 appendBits(assignVal, 14, bb);
831 } else if (assignVal < 1000000) {
832 appendBits(0b110, 3, bb);
833 appendBits(assignVal, 21, bb);
834 } else throw new RangeError("ECI assignment value out of range");
835 return new QrSegment(QrSegment.Mode.ECI, 0, bb);
836 }
837750
838 // Tests whether the given string can be encoded as a segment in numeric mode.751/*
839 // A string is encodable iff each character is in the range 0 to 9.752 * A segment of character/binary/control data in a QR Code symbol.
840 public static isNumeric(text: string): boolean {753 * Instances of this class are immutable.
841 return QrSegment.NUMERIC_REGEX.test(text);754 * The mid-level way to create a segment is to take the payload data
842 }755 * and call a static factory function such as QrSegment.makeNumeric().
756 * The low-level way to create a segment is to custom-make the bit buffer
757 * and call the QrSegment() constructor with appropriate values.
758 * This segment class imposes no length restrictions, but QR Codes have restrictions.
759 * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
760 * Any segment longer than this is meaningless for the purpose of generating QR Codes.
761 */
762export class QrSegment {
763 public static Mode: typeof QrSegmentMode;
764
765 /*-- Static factory functions (mid level) --*/
766
767 // Returns a segment representing the given binary data encoded in
768 // byte mode. All input byte arrays are acceptable. Any text string
769 // can be converted to UTF-8 bytes and encoded as a byte mode segment.
770 public static makeBytes(data: Readonly<Array<byte>>): QrSegment {
771 let bb: Array<bit> = [];
772 for (const b of data) appendBits(b, 8, bb);
773 return new QrSegment(QrSegment.Mode.BYTE, data.length, bb);
774 }
843775
844 // Tests whether the given string can be encoded as a segment in alphanumeric mode.776 // Returns a segment representing the given string of decimal digits encoded in numeric mode.
845 // A string is encodable iff each character is in the following set: 0 to 9, A to Z777 public static makeNumeric(digits: string): QrSegment {
846 // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.778 if (!QrSegment.isNumeric(digits))
847 public static isAlphanumeric(text: string): boolean {779 throw new RangeError("String contains non-numeric characters");
848 return QrSegment.ALPHANUMERIC_REGEX.test(text);780 let bb: Array<bit> = [];
849 }781 for (let i = 0; i < digits.length; ) {
782 // Consume up to 3 digits per iteration
783 const n: int = Math.min(digits.length - i, 3);
784 appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
785 i += n;
786 }
787 return new QrSegment(QrSegment.Mode.NUMERIC, digits.length, bb);
788 }
850789
851 /*-- Constructor (low level) and fields --*/790 // Returns a segment representing the given text string encoded in alphanumeric mode.
852791 // The characters allowed are: 0 to 9, A to Z (uppercase only), space,
853 // Creates a new QR Code segment with the given attributes and data.792 // dollar, percent, asterisk, plus, hyphen, period, slash, colon.
854 // The character count (numChars) must agree with the mode and the bit buffer length,793 public static makeAlphanumeric(text: string): QrSegment {
855 // but the constraint isn't checked. The given bit buffer is cloned and stored.794 if (!QrSegment.isAlphanumeric(text))
856 public constructor(795 throw new RangeError("String contains unencodable characters in alphanumeric mode");
857 // The mode indicator of this segment.796 let bb: Array<bit> = [];
858 public readonly mode: QrSegment.Mode,797 let i: int;
859798 for (i = 0; i + 2 <= text.length; i += 2) {
860 // The length of this segment's unencoded data. Measured in characters for799 // Process groups of 2
861 // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.800 let temp: int = QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
862 // Always zero or positive. Not the same as the data's bit length.801 temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
863 public readonly numChars: int,802 appendBits(temp, 11, bb);
864803 }
865 // The data bits of this segment. Accessed through getData().804 if (i < text.length)
866 private readonly bitData: Array<bit>,805 // 1 character remaining
867 ) {806 appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
868 if (numChars < 0) throw new RangeError("Invalid argument");807 return new QrSegment(QrSegment.Mode.ALPHANUMERIC, text.length, bb);
869 this.bitData = bitData.slice(); // Make defensive copy808 }
870 }
871809
872 /*-- Methods --*/810 // Returns a new mutable list of zero or more segments to represent the given Unicode text string.
811 // The result may use various segment modes and switch modes to optimize the length of the bit stream.
812 public static makeSegments(text: string): Array<QrSegment> {
813 // Select the most efficient segment encoding automatically
814 if (text == "") return [];
815 else if (QrSegment.isNumeric(text)) return [QrSegment.makeNumeric(text)];
816 else if (QrSegment.isAlphanumeric(text)) return [QrSegment.makeAlphanumeric(text)];
817 else return [QrSegment.makeBytes(QrSegment.toUtf8ByteArray(text))];
818 }
873819
874 // Returns a new copy of the data bits of this segment.820 // Returns a segment representing an Extended Channel Interpretation
875 public getData(): Array<bit> {821 // (ECI) designator with the given assignment value.
876 return this.bitData.slice(); // Make defensive copy822 public static makeEci(assignVal: int): QrSegment {
877 }823 let bb: Array<bit> = [];
824 if (assignVal < 0) throw new RangeError("ECI assignment value out of range");
825 else if (assignVal < 1 << 7) appendBits(assignVal, 8, bb);
826 else if (assignVal < 1 << 14) {
827 appendBits(0b10, 2, bb);
828 appendBits(assignVal, 14, bb);
829 } else if (assignVal < 1000000) {
830 appendBits(0b110, 3, bb);
831 appendBits(assignVal, 21, bb);
832 } else throw new RangeError("ECI assignment value out of range");
833 return new QrSegment(QrSegment.Mode.ECI, 0, bb);
834 }
878835
879 // (Package-private) Calculates and returns the number of bits needed to encode the given segments at836 // Tests whether the given string can be encoded as a segment in numeric mode.
880 // the given version. The result is infinity if a segment has too many characters to fit its length field.837 // A string is encodable iff each character is in the range 0 to 9.
881 public static getTotalBits(segs: Readonly<Array<QrSegment>>, version: int): number {838 public static isNumeric(text: string): boolean {
882 let result: number = 0;839 return QrSegment.NUMERIC_REGEX.test(text);
883 for (const seg of segs) {840 }
884 const ccbits: int = seg.mode.numCharCountBits(version);841
885 if (seg.numChars >= 1 << ccbits) return Infinity; // The segment's length doesn't fit the field's bit width842 // Tests whether the given string can be encoded as a segment in alphanumeric mode.
886 result += 4 + ccbits + seg.bitData.length;843 // A string is encodable iff each character is in the following set: 0 to 9, A to Z
887 }844 // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
888 return result;845 public static isAlphanumeric(text: string): boolean {
846 return QrSegment.ALPHANUMERIC_REGEX.test(text);
847 }
848
849 /*-- Constructor (low level) and fields --*/
850
851 public readonly mode: QrSegmentMode;
852 public readonly numChars: int;
853 private readonly bitData: Array<bit>;
854
855 // Creates a new QR Code segment with the given attributes and data.
856 // The character count (numChars) must agree with the mode and the bit buffer length,
857 // but the constraint isn't checked. The given bit buffer is cloned and stored.
858 public constructor(
859 // The mode indicator of this segment.
860 mode: QrSegmentMode,
861
862 // The length of this segment's unencoded data. Measured in characters for
863 // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
864 // Always zero or positive. Not the same as the data's bit length.
865 numChars: int,
866
867 // The data bits of this segment. Accessed through getData().
868 bitData: Array<bit>,
869 ) {
870 this.mode = mode;
871 this.numChars = numChars;
872 if (numChars < 0) throw new RangeError("Invalid argument");
873 this.bitData = bitData.slice(); // Make defensive copy
874 }
875
876 /*-- Methods --*/
877
878 // Returns a new copy of the data bits of this segment.
879 public getData(): Array<bit> {
880 return this.bitData.slice(); // Make defensive copy
881 }
882
883 // (Package-private) Calculates and returns the number of bits needed to encode the given segments at
884 // the given version. The result is infinity if a segment has too many characters to fit its length field.
885 public static getTotalBits(segs: Readonly<Array<QrSegment>>, version: int): number {
886 let result: number = 0;
887 for (const seg of segs) {
888 const ccbits: int = seg.mode.numCharCountBits(version);
889 if (seg.numChars >= 1 << ccbits) return Infinity; // The segment's length doesn't fit the field's bit width
890 result += 4 + ccbits + seg.bitData.length;
889 }891 }
892 return result;
893 }
890894
891 // Returns a new array of bytes representing the given string encoded in UTF-8.895 // Returns a new array of bytes representing the given string encoded in UTF-8.
892 private static toUtf8ByteArray(str: string): Array<byte> {896 private static toUtf8ByteArray(str: string): Array<byte> {
893 str = encodeURI(str);897 str = encodeURI(str);
894 let result: Array<byte> = [];898 let result: Array<byte> = [];
895 for (let i = 0; i < str.length; i++) {899 for (let i = 0; i < str.length; i++) {
896 if (str.charAt(i) != "%") result.push(str.charCodeAt(i));900 if (str.charAt(i) != "%") result.push(str.charCodeAt(i));
897 else {901 else {
898 result.push(parseInt(str.substring(i + 1, i + 3), 16));902 result.push(parseInt(str.substring(i + 1, i + 3), 16));
899 i += 2;903 i += 2;
900 }
901 }904 }
902 return result;
903 }905 }
906 return result;
907 }
904908
905 /*-- Constants --*/909 /*-- Constants --*/
906910
907 // Describes precisely all strings that are encodable in numeric mode.911 // Describes precisely all strings that are encodable in numeric mode.
908 private static readonly NUMERIC_REGEX: RegExp = /^[0-9]*$/;912 private static readonly NUMERIC_REGEX: RegExp = /^[0-9]*$/;
909913
910 // Describes precisely all strings that are encodable in alphanumeric mode.914 // Describes precisely all strings that are encodable in alphanumeric mode.
911 private static readonly ALPHANUMERIC_REGEX: RegExp = /^[A-Z0-9 $%*+.\/:-]*$/;915 private static readonly ALPHANUMERIC_REGEX: RegExp = /^[A-Z0-9 $%*+.\/:-]*$/;
912916
913 // The set of all legal characters in alphanumeric mode,917 // The set of all legal characters in alphanumeric mode,
914 // where each character value maps to the index in the string.918 // where each character value maps to the index in the string.
915 private static readonly ALPHANUMERIC_CHARSET: string =919 private static readonly ALPHANUMERIC_CHARSET: string =
916 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";920 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
917 }
918}921}
919922
920/*---- Public helper enumeration ----*/923/*---- Public helper enumeration ----*/
921924
922namespace qrcodegen.QrCode {925class QrCodeEcc {
923 type int = number;926 /*-- Constants --*/
924927
925 /*928 public static readonly LOW = new QrCodeEcc(0, 1); // The QR Code can tolerate about 7% erroneous codewords
926 * The error correction level in a QR Code symbol. Immutable.929 public static readonly MEDIUM = new QrCodeEcc(1, 0); // The QR Code can tolerate about 15% erroneous codewords
927 */930 public static readonly QUARTILE = new QrCodeEcc(2, 3); // The QR Code can tolerate about 25% erroneous codewords
928 export class Ecc {931 public static readonly HIGH = new QrCodeEcc(3, 2); // The QR Code can tolerate about 30% erroneous codewords
929 /*-- Constants --*/
930932
931 public static readonly LOW = new Ecc(0, 1); // The QR Code can tolerate about 7% erroneous codewords933 public readonly ordinal: int;
932 public static readonly MEDIUM = new Ecc(1, 0); // The QR Code can tolerate about 15% erroneous codewords934 public readonly formatBits: int;
933 public static readonly QUARTILE = new Ecc(2, 3); // The QR Code can tolerate about 25% erroneous codewords
934 public static readonly HIGH = new Ecc(3, 2); // The QR Code can tolerate about 30% erroneous codewords
935935
936 /*-- Constructor and fields --*/936 /*-- Constructor and fields --*/
937937
938 private constructor(938 private constructor(
939 // In the range 0 to 3 (unsigned 2-bit integer).939 // In the range 0 to 3 (unsigned 2-bit integer).
940 public readonly ordinal: int,940 ordinal: int,
941 // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).941 // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
942 public readonly formatBits: int,942 formatBits: int,
943 ) {}943 ) {
944 this.ordinal = ordinal;
945 this.formatBits = formatBits;
944 }946 }
945}947}
946948
947/*---- Public helper enumeration ----*/949/*---- Public helper enumeration ----*/
948950
949namespace qrcodegen.QrSegment {951class QrSegmentMode {
950 type int = number;952 /*-- Constants --*/
951953
952 /*954 public static readonly NUMERIC = new QrSegmentMode(0x1, [10, 12, 14]);
953 * Describes how a segment's data bits are interpreted. Immutable.955 public static readonly ALPHANUMERIC = new QrSegmentMode(0x2, [9, 11, 13]);
954 */956 public static readonly BYTE = new QrSegmentMode(0x4, [8, 16, 16]);
955 export class Mode {957 public static readonly KANJI = new QrSegmentMode(0x8, [8, 10, 12]);
956 /*-- Constants --*/958 public static readonly ECI = new QrSegmentMode(0x7, [0, 0, 0]);
957959
958 public static readonly NUMERIC = new Mode(0x1, [10, 12, 14]);960 public readonly modeBits: int;
959 public static readonly ALPHANUMERIC = new Mode(0x2, [9, 11, 13]);961 private readonly numBitsCharCount: [int, int, int];
960 public static readonly BYTE = new Mode(0x4, [8, 16, 16]);
961 public static readonly KANJI = new Mode(0x8, [8, 10, 12]);
962 public static readonly ECI = new Mode(0x7, [0, 0, 0]);
963962
964 /*-- Constructor and fields --*/963 /*-- Constructor and fields --*/
965964
966 private constructor(965 private constructor(
967 // The mode indicator bits, which is a uint4 value (range 0 to 15).966 // The mode indicator bits, which is a uint4 value (range 0 to 15).
968 public readonly modeBits: int,967 modeBits: int,
969 // Number of character count bits for three different version ranges.968 // Number of character count bits for three different version ranges.
970 private readonly numBitsCharCount: [int, int, int],969 numBitsCharCount: [int, int, int],
971 ) {}970 ) {
971 this.modeBits = modeBits;
972 this.numBitsCharCount = numBitsCharCount;
973 }
972974
973 /*-- Method --*/975 /*-- Method --*/
974976
975 // (Package-private) Returns the bit width of the character count field for a segment in977 // (Package-private) Returns the bit width of the character count field for a segment in
976 // this mode in a QR Code at the given version number. The result is in the range [0, 16].978 // this mode in a QR Code at the given version number. The result is in the range [0, 16].
977 public numCharCountBits(ver: int): int {979 public numCharCountBits(ver: int): int {
978 return this.numBitsCharCount[Math.floor((ver + 7) / 17)];980 return this.numBitsCharCount[Math.floor((ver + 7) / 17)]!;
979 }
980 }981 }
981}982}
982983
983export const QrCode = qrcodegen.QrCode;984QrCode.Ecc = QrCodeEcc;
984export const QrSegment = qrcodegen.QrSegment;985QrSegment.Mode = QrSegmentMode;
packages/shared/src/searchRanking.test.ts+1-1
...@@ -6,7 +6,7 @@ import {...@@ -6,7 +6,7 @@ import {
6 normalizeSearchQuery,6 normalizeSearchQuery,
7 scoreQueryMatch,7 scoreQueryMatch,
8 scoreSubsequenceMatch,8 scoreSubsequenceMatch,
9} from "./searchRanking";9} from "./searchRanking.ts";
1010
11describe("normalizeSearchQuery", () => {11describe("normalizeSearchQuery", () => {
12 it("trims and lowercases queries", () => {12 it("trims and lowercases queries", () => {
packages/shared/src/serverSettings.test.ts+1-1
...@@ -5,7 +5,7 @@ import {...@@ -5,7 +5,7 @@ import {
5 extractPersistedServerObservabilitySettings,5 extractPersistedServerObservabilitySettings,
6 normalizePersistedServerSettingString,6 normalizePersistedServerSettingString,
7 parsePersistedServerObservabilitySettings,7 parsePersistedServerObservabilitySettings,
8} from "./serverSettings";8} from "./serverSettings.ts";
99
10describe("serverSettings helpers", () => {10describe("serverSettings helpers", () => {
11 it("normalizes optional persisted strings", () => {11 it("normalizes optional persisted strings", () => {
packages/shared/src/serverSettings.ts+2-2
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";1import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";
2import { Schema } from "effect";2import { Schema } from "effect";
3import { deepMerge } from "./Struct";3import { deepMerge } from "./Struct.ts";
4import { fromLenientJson } from "./schemaJson";4import { fromLenientJson } from "./schemaJson.ts";
55
6const ServerSettingsJson = fromLenientJson(ServerSettings);6const ServerSettingsJson = fromLenientJson(ServerSettings);
77
packages/shared/src/shell.test.ts+1-1
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 readEnvironmentFromLoginShell,7 readEnvironmentFromLoginShell,
8 readPathFromLaunchctl,8 readPathFromLaunchctl,
9 readPathFromLoginShell,9 readPathFromLoginShell,
10} from "./shell";10} from "./shell.ts";
1111
12describe("extractPathFromShellOutput", () => {12describe("extractPathFromShellOutput", () => {
13 it("extracts the path between capture markers", () => {13 it("extracts the path between capture markers", () => {
scripts/build-desktop-artifact.ts+83-28
...@@ -1,9 +1,5 @@...@@ -1,9 +1,5 @@
1#!/usr/bin/env node1#!/usr/bin/env node
22
3import { spawnSync } from "node:child_process";
4import { existsSync } from "node:fs";
5import { join } from "node:path";
6
7import rootPackageJson from "../package.json" with { type: "json" };3import rootPackageJson from "../package.json" with { type: "json" };
8import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };4import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
9import serverPackageJson from "../apps/server/package.json" with { type: "json" };5import serverPackageJson from "../apps/server/package.json" with { type: "json" };
...@@ -14,7 +10,18 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";...@@ -14,7 +10,18 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
1410
15import * as NodeRuntime from "@effect/platform-node/NodeRuntime";11import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
16import * as NodeServices from "@effect/platform-node/NodeServices";12import * as NodeServices from "@effect/platform-node/NodeServices";
17import { Config, Data, Effect, FileSystem, Layer, Logger, Option, Path, Schema } from "effect";13import {
14 Config,
15 Data,
16 Effect,
17 FileSystem,
18 Layer,
19 Logger,
20 Option,
21 Path,
22 Schema,
23 Stream,
24} from "effect";
18import { Command, Flag } from "effect/unstable/cli";25import { Command, Flag } from "effect/unstable/cli";
19import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";26import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2027
...@@ -91,12 +98,49 @@ class BuildScriptError extends Data.TaggedError("BuildScriptError")<{...@@ -91,12 +98,49 @@ class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
91 readonly cause?: unknown;98 readonly cause?: unknown;
92}> {}99}> {}
93100
94function resolveGitCommitHash(repoRoot: string): string {101const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
95 const result = spawnSync("git", ["rev-parse", "--short=12", "HEAD"], {102 stream.pipe(
96 cwd: repoRoot,103 Stream.decodeText(),
97 encoding: "utf8",104 Stream.runFold(
98 });105 () => "",
99 if (result.status !== 0) {106 (acc, chunk) => acc + chunk,
107 ),
108 );
109
110const spawnAndCollectOutput = Effect.fn("spawnAndCollectOutput")(function* (
111 command: ChildProcess.Command,
112) {
113 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
114 const child = yield* spawner.spawn(command);
115
116 const [stdout, stderr, exitCode] = yield* Effect.all(
117 [
118 collectStreamAsString(child.stdout),
119 collectStreamAsString(child.stderr),
120 child.exitCode.pipe(Effect.map(Number)),
121 ],
122 { concurrency: "unbounded" },
123 );
124
125 return { stdout, stderr, exitCode } as const;
126});
127
128const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRoot: string) {
129 const result = yield* spawnAndCollectOutput(
130 ChildProcess.make("git", ["rev-parse", "--short=12", "HEAD"], {
131 cwd: repoRoot,
132 }),
133 ).pipe(
134 Effect.catch(() =>
135 Effect.succeed({
136 stdout: "",
137 stderr: "",
138 exitCode: 1,
139 }),
140 ),
141 );
142
143 if (result.exitCode !== 0) {
100 return "unknown";144 return "unknown";
101 }145 }
102 const hash = result.stdout.trim();146 const hash = result.stdout.trim();
...@@ -104,11 +148,13 @@ function resolveGitCommitHash(repoRoot: string): string {...@@ -104,11 +148,13 @@ function resolveGitCommitHash(repoRoot: string): string {
104 return "unknown";148 return "unknown";
105 }149 }
106 return hash.toLowerCase();150 return hash.toLowerCase();
107}151});
108152
109function resolvePythonForNodeGyp(): string | undefined {153const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () {
154 const fs = yield* FileSystem.FileSystem;
155 const path = yield* Path.Path;
110 const configured = process.env.npm_config_python ?? process.env.PYTHON;156 const configured = process.env.npm_config_python ?? process.env.PYTHON;
111 if (configured && existsSync(configured)) {157 if (configured && (yield* fs.exists(configured))) {
112 return configured;158 return configured;
113 }159 }
114160
...@@ -116,28 +162,37 @@ function resolvePythonForNodeGyp(): string | undefined {...@@ -116,28 +162,37 @@ function resolvePythonForNodeGyp(): string | undefined {
116 const localAppData = process.env.LOCALAPPDATA;162 const localAppData = process.env.LOCALAPPDATA;
117 if (localAppData) {163 if (localAppData) {
118 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {164 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {
119 const candidate = join(localAppData, "Programs", "Python", version, "python.exe");165 const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe");
120 if (existsSync(candidate)) {166 if (yield* fs.exists(candidate)) {
121 return candidate;167 return candidate;
122 }168 }
123 }169 }
124 }170 }
125 }171 }
126172
127 const probe = spawnSync("python", ["-c", "import sys;print(sys.executable)"], {173 const probe = yield* spawnAndCollectOutput(
128 encoding: "utf8",174 ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]),
129 });175 ).pipe(
130 if (probe.status !== 0) {176 Effect.catch(() =>
177 Effect.succeed({
178 stdout: "",
179 stderr: "",
180 exitCode: 1,
181 }),
182 ),
183 );
184
185 if (probe.exitCode !== 0) {
131 return undefined;186 return undefined;
132 }187 }
133188
134 const executable = probe.stdout.trim();189 const executable = probe.stdout.trim();
135 if (!executable || !existsSync(executable)) {190 if (!executable || !(yield* fs.exists(executable))) {
136 return undefined;191 return undefined;
137 }192 }
138193
139 return executable;194 return executable;
140}195});
141196
142interface ResolvedBuildOptions {197interface ResolvedBuildOptions {
143 readonly platform: typeof BuildPlatform.Type;198 readonly platform: typeof BuildPlatform.Type;
...@@ -654,7 +709,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -654,7 +709,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
654709
655 const appVersion = options.version ?? serverPackageJson.version;710 const appVersion = options.version ?? serverPackageJson.version;
656 const iconAssets = resolveDesktopBuildIconAssets(appVersion);711 const iconAssets = resolveDesktopBuildIconAssets(appVersion);
657 const commitHash = resolveGitCommitHash(repoRoot);712 const commitHash = yield* resolveGitCommitHash(repoRoot);
658 const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;713 const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;
659 const stageRoot = yield* mkdir({714 const stageRoot = yield* mkdir({
660 prefix: `t3code-desktop-${options.platform}-stage-`,715 prefix: `t3code-desktop-${options.platform}-stage-`,
...@@ -709,9 +764,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -709,9 +764,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
709 options.platform,764 options.platform,
710 stageResourcesDir,765 stageResourcesDir,
711 {766 {
712 macIconPng: join(repoRoot, iconAssets.macIconPng),767 macIconPng: path.join(repoRoot, iconAssets.macIconPng),
713 linuxIconPng: join(repoRoot, iconAssets.linuxIconPng),768 linuxIconPng: path.join(repoRoot, iconAssets.linuxIconPng),
714 windowsIconIco: join(repoRoot, iconAssets.windowsIconIco),769 windowsIconIco: path.join(repoRoot, iconAssets.windowsIconIco),
715 },770 },
716 options.verbose,771 options.verbose,
717 );772 );
...@@ -727,7 +782,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -727,7 +782,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
727 private: true,782 private: true,
728 description: "T3 Code desktop build",783 description: "T3 Code desktop build",
729 author: "T3 Tools",784 author: "T3 Tools",
730 main: "apps/desktop/dist-electron/main.js",785 main: "apps/desktop/dist-electron/main.cjs",
731 build: yield* createBuildConfig(786 build: yield* createBuildConfig(
732 options.platform,787 options.platform,
733 options.target,788 options.target,
...@@ -777,7 +832,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -777,7 +832,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
777 }832 }
778833
779 if (process.platform === "win32") {834 if (process.platform === "win32") {
780 const python = resolvePythonForNodeGyp();835 const python = yield* resolvePythonForNodeGyp();
781 if (python) {836 if (python) {
782 buildEnv.PYTHON = python;837 buildEnv.PYTHON = python;
783 buildEnv.npm_config_python = python;838 buildEnv.npm_config_python = python;
scripts/dev-runner.test.ts+10-7
...@@ -1,8 +1,7 @@...@@ -1,8 +1,7 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { homedir } from "node:os";2import * as NodeOS from "node:os";
3import { resolve } from "node:path";
4import { assert, describe, it } from "@effect/vitest";3import { assert, describe, it } from "@effect/vitest";
5import { Effect } from "effect";4import { Effect, Path } from "effect";
65
7import {6import {
8 checkPortAvailabilityOnHosts,7 checkPortAvailabilityOnHosts,
...@@ -49,6 +48,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -49,6 +48,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
49 describe("createDevRunnerEnv", () => {48 describe("createDevRunnerEnv", () => {
50 it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () =>49 it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () =>
51 Effect.gen(function* () {50 Effect.gen(function* () {
51 const path = yield* Path.Path;
52 const env = yield* createDevRunnerEnv({52 const env = yield* createDevRunnerEnv({
53 mode: "dev",53 mode: "dev",
54 baseEnv: {},54 baseEnv: {},
...@@ -63,12 +63,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -63,12 +63,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
63 devUrl: undefined,63 devUrl: undefined,
64 });64 });
6565
66 assert.equal(env.T3CODE_HOME, resolve(homedir(), ".t3"));66 assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".t3"));
67 }),67 }),
68 );68 );
6969
70 it.effect("supports explicit typed overrides", () =>70 it.effect("supports explicit typed overrides", () =>
71 Effect.gen(function* () {71 Effect.gen(function* () {
72 const path = yield* Path.Path;
72 const env = yield* createDevRunnerEnv({73 const env = yield* createDevRunnerEnv({
73 mode: "dev:server",74 mode: "dev:server",
74 baseEnv: {},75 baseEnv: {},
...@@ -83,7 +84,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -83,7 +84,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
83 devUrl: new URL("http://localhost:7331"),84 devUrl: new URL("http://localhost:7331"),
84 });85 });
8586
86 assert.equal(env.T3CODE_HOME, resolve("/tmp/custom-t3"));87 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/custom-t3"));
87 assert.equal(env.T3CODE_PORT, "4222");88 assert.equal(env.T3CODE_PORT, "4222");
88 assert.equal(env.VITE_HTTP_URL, "http://localhost:4222");89 assert.equal(env.VITE_HTTP_URL, "http://localhost:4222");
89 assert.equal(env.VITE_WS_URL, "ws://localhost:4222");90 assert.equal(env.VITE_WS_URL, "ws://localhost:4222");
...@@ -142,6 +143,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -142,6 +143,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
142143
143 it.effect("uses custom t3Home when provided", () =>144 it.effect("uses custom t3Home when provided", () =>
144 Effect.gen(function* () {145 Effect.gen(function* () {
146 const path = yield* Path.Path;
145 const env = yield* createDevRunnerEnv({147 const env = yield* createDevRunnerEnv({
146 mode: "dev",148 mode: "dev",
147 baseEnv: {},149 baseEnv: {},
...@@ -156,12 +158,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -156,12 +158,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
156 devUrl: undefined,158 devUrl: undefined,
157 });159 });
158160
159 assert.equal(env.T3CODE_HOME, resolve("/tmp/my-t3"));161 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3"));
160 }),162 }),
161 );163 );
162164
163 it.effect("pins desktop dev to a stable backend port and websocket url", () =>165 it.effect("pins desktop dev to a stable backend port and websocket url", () =>
164 Effect.gen(function* () {166 Effect.gen(function* () {
167 const path = yield* Path.Path;
165 const env = yield* createDevRunnerEnv({168 const env = yield* createDevRunnerEnv({
166 mode: "dev:desktop",169 mode: "dev:desktop",
167 baseEnv: {170 baseEnv: {
...@@ -182,7 +185,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {...@@ -182,7 +185,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
182 devUrl: undefined,185 devUrl: undefined,
183 });186 });
184187
185 assert.equal(env.T3CODE_HOME, resolve("/tmp/my-t3"));188 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3"));
186 assert.equal(env.PORT, "5733");189 assert.equal(env.PORT, "5733");
187 assert.equal(env.VITE_DEV_SERVER_URL, "http://127.0.0.1:5733");190 assert.equal(env.VITE_DEV_SERVER_URL, "http://127.0.0.1:5733");
188 assert.equal(env.HOST, "127.0.0.1");191 assert.equal(env.HOST, "127.0.0.1");
scripts/dev-runner.ts+7-8
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1#!/usr/bin/env node1#!/usr/bin/env node
22
3import { homedir } from "node:os";3import * as NodeOS from "node:os";
44
5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
6import * as NodeServices from "@effect/platform-node/NodeServices";6import * as NodeServices from "@effect/platform-node/NodeServices";
...@@ -17,7 +17,7 @@ const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";...@@ -17,7 +17,7 @@ const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
17const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const;17const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const;
1818
19export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) =>19export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) =>
20 path.join(homedir(), ".t3"),20 path.join(NodeOS.homedir(), ".t3"),
21);21);
2222
23const MODE_ARGS = {23const MODE_ARGS = {
...@@ -523,11 +523,10 @@ const cliRuntimeLayer = Layer.mergeAll(...@@ -523,11 +523,10 @@ const cliRuntimeLayer = Layer.mergeAll(
523 NetService.layer,523 NetService.layer,
524);524);
525525
526const runtimeProgram = Command.run(devRunnerCli, { version: "0.0.0" }).pipe(
527 Effect.scoped,
528 Effect.provide(cliRuntimeLayer),
529);
530
531if (import.meta.main) {526if (import.meta.main) {
532 NodeRuntime.runMain(runtimeProgram);527 Command.run(devRunnerCli, { version: "0.0.0" }).pipe(
528 Effect.scoped,
529 Effect.provide(cliRuntimeLayer),
530 NodeRuntime.runMain,
531 );
533}532}
scripts/merge-update-manifests.test.ts+116
...@@ -1,11 +1,17 @@...@@ -1,11 +1,17 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
1import { assert, describe, it } from "@effect/vitest";2import { assert, describe, it } from "@effect/vitest";
3import { Effect, FileSystem, Path } from "effect";
4import { Command, CliError } from "effect/unstable/cli";
25
3import {6import {
4 mergePlatformUpdateManifests,7 mergePlatformUpdateManifests,
8 mergeUpdateManifestsCommand,
5 parsePlatformUpdateManifest,9 parsePlatformUpdateManifest,
6 serializePlatformUpdateManifest,10 serializePlatformUpdateManifest,
7} from "./merge-update-manifests.ts";11} from "./merge-update-manifests.ts";
812
13const runCli = Command.runWith(mergeUpdateManifestsCommand, { version: "0.0.0" });
14
9describe("merge-update-manifests", () => {15describe("merge-update-manifests", () => {
10 it("merges arm64 and x64 macOS update manifests into one multi-arch manifest", () => {16 it("merges arm64 and x64 macOS update manifests into one multi-arch manifest", () => {
11 const arm64 = parsePlatformUpdateManifest(17 const arm64 = parsePlatformUpdateManifest(
...@@ -188,3 +194,113 @@ releaseDate: '2026-03-07T10:36:07.540Z'...@@ -188,3 +194,113 @@ releaseDate: '2026-03-07T10:36:07.540Z'
188 assert.equal(reparsed.version, "1.0");194 assert.equal(reparsed.version, "1.0");
189 });195 });
190});196});
197
198it.layer(NodeServices.layer)("merge-update-manifests cli", (it) => {
199 const arm64MacManifest = `version: 0.0.4
200files:
201 - url: T3-Code-0.0.4-arm64.zip
202 sha512: arm64zip
203 size: 125621344
204 - url: T3-Code-0.0.4-arm64.dmg
205 sha512: arm64dmg
206 size: 131754935
207path: T3-Code-0.0.4-arm64.zip
208sha512: arm64zip
209releaseDate: '2026-03-07T10:32:14.587Z'
210`;
211
212 const x64MacManifest = `version: 0.0.4
213files:
214 - url: T3-Code-0.0.4-x64.zip
215 sha512: x64zip
216 size: 132000112
217 - url: T3-Code-0.0.4-x64.dmg
218 sha512: x64dmg
219 size: 138148807
220path: T3-Code-0.0.4-x64.zip
221sha512: x64zip
222releaseDate: '2026-03-07T10:36:07.540Z'
223`;
224
225 it.effect("writes the merged manifest back to the primary path by default", () =>
226 Effect.gen(function* () {
227 const fs = yield* FileSystem.FileSystem;
228 const path = yield* Path.Path;
229 const baseDir = yield* fs.makeTempDirectoryScoped({
230 prefix: "merge-update-manifests-cli-",
231 });
232 const primaryPath = path.join(baseDir, "latest-mac.yml");
233 const secondaryPath = path.join(baseDir, "latest-mac-x64.yml");
234
235 yield* fs.writeFileString(primaryPath, arm64MacManifest);
236 yield* fs.writeFileString(secondaryPath, x64MacManifest);
237
238 yield* runCli(["--platform", "mac", primaryPath, secondaryPath]);
239
240 const merged = yield* fs.readFileString(primaryPath);
241 assert.ok(merged.includes("T3-Code-0.0.4-arm64.zip"));
242 assert.ok(merged.includes("T3-Code-0.0.4-x64.zip"));
243 assert.ok(!merged.includes("path:"));
244 }),
245 );
246
247 it.effect("writes the merged manifest to an explicit output path", () =>
248 Effect.gen(function* () {
249 const fs = yield* FileSystem.FileSystem;
250 const path = yield* Path.Path;
251 const baseDir = yield* fs.makeTempDirectoryScoped({
252 prefix: "merge-update-manifests-cli-output-",
253 });
254 const primaryPath = path.join(baseDir, "latest-win-arm64.yml");
255 const secondaryPath = path.join(baseDir, "latest-win-x64.yml");
256 const outputPath = path.join(baseDir, "latest-win.yml");
257
258 yield* fs.writeFileString(
259 primaryPath,
260 `version: 0.0.4
261files:
262 - url: T3-Code-0.0.4-arm64.exe
263 sha512: arm64exe
264 size: 125621344
265releaseDate: '2026-03-07T10:32:14.587Z'
266`,
267 );
268 yield* fs.writeFileString(
269 secondaryPath,
270 `version: 0.0.4
271files:
272 - url: T3-Code-0.0.4-x64.exe
273 sha512: x64exe
274 size: 132000112
275releaseDate: '2026-03-07T10:36:07.540Z'
276`,
277 );
278
279 yield* runCli(["--platform", "win", primaryPath, secondaryPath, outputPath]);
280
281 const merged = yield* fs.readFileString(outputPath);
282 assert.ok(merged.includes("T3-Code-0.0.4-arm64.exe"));
283 assert.ok(merged.includes("T3-Code-0.0.4-x64.exe"));
284 }),
285 );
286
287 it.effect("rejects invalid platform values during cli parsing", () =>
288 Effect.gen(function* () {
289 const error = yield* runCli(["--platform", "linux", "a.yml", "b.yml"]).pipe(Effect.flip);
290
291 if (!CliError.isCliError(error)) {
292 assert.fail(`Expected CliError, got ${String(error)}`);
293 }
294
295 const platformError =
296 error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error;
297
298 if (!platformError || platformError._tag !== "InvalidValue") {
299 assert.fail(`Expected InvalidValue, got ${String(platformError?._tag)}`);
300 }
301
302 assert.equal(platformError.option, "platform");
303 assert.equal(platformError.value, "linux");
304 }),
305 );
306});
scripts/merge-update-manifests.ts+57-40
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { readFileSync, writeFileSync } from "node:fs";1#!/usr/bin/env node
2import { resolve } from "node:path";2
3import { fileURLToPath } from "node:url";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Effect, FileSystem, Option, Path, Schema } from "effect";
6import { Argument, Command, Flag } from "effect/unstable/cli";
47
5import {8import {
6 mergeUpdateManifests,9 mergeUpdateManifests,
...@@ -9,7 +12,8 @@ import {...@@ -9,7 +12,8 @@ import {
9 type UpdateManifest,12 type UpdateManifest,
10} from "./lib/update-manifest.ts";13} from "./lib/update-manifest.ts";
1114
12export type UpdateManifestPlatform = "mac" | "win";15const UpdateManifestPlatform = Schema.Literals(["mac", "win"]);
16export type UpdateManifestPlatform = typeof UpdateManifestPlatform.Type;
1317
14function getPlatformLabel(platform: UpdateManifestPlatform): string {18function getPlatformLabel(platform: UpdateManifestPlatform): string {
15 return platform === "mac" ? "macOS" : "Windows";19 return platform === "mac" ? "macOS" : "Windows";
...@@ -40,52 +44,65 @@ export function serializePlatformUpdateManifest(...@@ -40,52 +44,65 @@ export function serializePlatformUpdateManifest(
40 });44 });
41}45}
4246
43function parseArgs(args: ReadonlyArray<string>): {47export const mergeUpdateManifestFiles = Effect.fn("mergeUpdateManifestFiles")(function* (
44 platform: UpdateManifestPlatform;48 platform: UpdateManifestPlatform,
45 primaryPath: string;49 primaryPathArg: string,
46 secondaryPath: string;50 secondaryPathArg: string,
47 outputPath: string;51 outputPathArg: string | undefined,
48} {52) {
49 const [platformFlag, platformValue, primaryPathArg, secondaryPathArg, outputPathArg] = args;53 const fs = yield* FileSystem.FileSystem;
50 if (platformFlag !== "--platform" || (platformValue !== "mac" && platformValue !== "win")) {54 const path = yield* Path.Path;
51 throw new Error(
52 "Usage: node scripts/merge-update-manifests.ts --platform <mac|win> <primary-path> <secondary-path> [output-path]",
53 );
54 }
55 if (!primaryPathArg || !secondaryPathArg) {
56 throw new Error(
57 "Usage: node scripts/merge-update-manifests.ts --platform <mac|win> <primary-path> <secondary-path> [output-path]",
58 );
59 }
60
61 const primaryPath = resolve(primaryPathArg);
62 const secondaryPath = resolve(secondaryPathArg);
63 const outputPath = resolve(outputPathArg ?? primaryPathArg);
6455
65 return {56 const primaryPath = path.resolve(primaryPathArg);
66 platform: platformValue,57 const secondaryPath = path.resolve(secondaryPathArg);
67 primaryPath,58 const outputPath = path.resolve(outputPathArg ?? primaryPathArg);
68 secondaryPath,
69 outputPath,
70 };
71}
7259
73function main(args: ReadonlyArray<string>): void {
74 const { platform, primaryPath, secondaryPath, outputPath } = parseArgs(args);
75 const primaryManifest = parsePlatformUpdateManifest(60 const primaryManifest = parsePlatformUpdateManifest(
76 platform,61 platform,
77 readFileSync(primaryPath, "utf8"),62 yield* fs.readFileString(primaryPath),
78 primaryPath,63 primaryPath,
79 );64 );
80 const secondaryManifest = parsePlatformUpdateManifest(65 const secondaryManifest = parsePlatformUpdateManifest(
81 platform,66 platform,
82 readFileSync(secondaryPath, "utf8"),67 yield* fs.readFileString(secondaryPath),
83 secondaryPath,68 secondaryPath,
84 );69 );
85 const merged = mergePlatformUpdateManifests(platform, primaryManifest, secondaryManifest);70 const merged = mergePlatformUpdateManifests(platform, primaryManifest, secondaryManifest);
86 writeFileSync(outputPath, serializePlatformUpdateManifest(platform, merged));
87}
8871
89if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {72 yield* fs.writeFileString(outputPath, serializePlatformUpdateManifest(platform, merged));
90 main(process.argv.slice(2));73});
74
75export const mergeUpdateManifestsCommand = Command.make(
76 "merge-update-manifests",
77 {
78 platform: Flag.choice("platform", UpdateManifestPlatform.literals).pipe(
79 Flag.withDescription("Update manifest platform."),
80 ),
81 primaryPath: Argument.string("primary-path").pipe(
82 Argument.withDescription("Primary update manifest path. Defaults to the output path."),
83 ),
84 secondaryPath: Argument.string("secondary-path").pipe(
85 Argument.withDescription(
86 "Secondary update manifest path to merge into the primary manifest.",
87 ),
88 ),
89 outputPath: Argument.string("output-path").pipe(
90 Argument.withDescription("Optional output path for the merged manifest."),
91 Argument.optional,
92 ),
93 },
94 ({ platform, primaryPath, secondaryPath, outputPath }) =>
95 mergeUpdateManifestFiles(
96 platform,
97 primaryPath,
98 secondaryPath,
99 Option.getOrUndefined(outputPath),
100 ),
101).pipe(Command.withDescription("Merge two Electron updater manifests into a multi-arch manifest."));
102
103if (import.meta.main) {
104 Command.run(mergeUpdateManifestsCommand, { version: "0.0.0" }).pipe(
105 Effect.provide(NodeServices.layer),
106 NodeRuntime.runMain,
107 );
91}108}
scripts/mock-update-server.test.ts created+104
...@@ -0,0 +1,104 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { NodeHttpServer } from "@effect/platform-node";
3import { assert, it } from "@effect/vitest";
4import { Effect, FileSystem, Layer, Path } from "effect";
5import { HttpClient, HttpRouter } from "effect/unstable/http";
6
7import { makeMockUpdateRouteLayer } from "./mock-update-server.ts";
8
9const withMockUpdateServer = <A, E, R>(rootRealPath: string, effect: Effect.Effect<A, E, R>) =>
10 effect.pipe(
11 Effect.provide(
12 HttpRouter.serve(makeMockUpdateRouteLayer(rootRealPath), {
13 disableListenLog: true,
14 disableLogger: true,
15 }).pipe(Layer.provideMerge(NodeHttpServer.layerTest)),
16 ),
17 );
18
19it.layer(NodeServices.layer)("mock-update-server", (it) => {
20 it.effect("serves files from the configured root", () =>
21 Effect.gen(function* () {
22 const fileSystem = yield* FileSystem.FileSystem;
23 const path = yield* Path.Path;
24 const root = yield* fileSystem.makeTempDirectoryScoped({
25 prefix: "mock-update-server-root-",
26 });
27 const rootRealPath = yield* fileSystem.realPath(root);
28 const filePath = path.join(root, "latest.yml");
29
30 yield* fileSystem.writeFileString(filePath, "version: 0.0.1\n");
31
32 yield* withMockUpdateServer(
33 rootRealPath,
34 Effect.gen(function* () {
35 const client = yield* HttpClient.HttpClient;
36 const response = yield* client.get("/latest.yml");
37
38 assert.equal(response.status, 200);
39 assert.equal(response.headers["content-type"], "text/yaml");
40 assert.equal(yield* response.text, "version: 0.0.1\n");
41 }),
42 );
43 }),
44 );
45
46 it.effect("rejects encoded path traversal outside the configured root", () =>
47 Effect.gen(function* () {
48 const fileSystem = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50 const root = yield* fileSystem.makeTempDirectoryScoped({
51 prefix: "mock-update-server-root-",
52 });
53 const outside = yield* fileSystem.makeTempDirectoryScoped({
54 prefix: "mock-update-server-outside-",
55 });
56 const rootRealPath = yield* fileSystem.realPath(root);
57
58 yield* fileSystem.writeFileString(path.join(outside, "secret.txt"), "nope\n");
59
60 yield* withMockUpdateServer(
61 rootRealPath,
62 Effect.gen(function* () {
63 const client = yield* HttpClient.HttpClient;
64 const response = yield* client.get("/%2e%2e/secret.txt");
65
66 assert.equal(response.status, 404);
67 assert.equal(yield* response.text, "Not Found");
68 }),
69 );
70 }),
71 );
72
73 it.effect("rejects symlinked files that escape the configured root", () =>
74 Effect.gen(function* () {
75 const fileSystem = yield* FileSystem.FileSystem;
76 const path = yield* Path.Path;
77 const root = yield* fileSystem.makeTempDirectoryScoped({
78 prefix: "mock-update-server-root-",
79 });
80 const outside = yield* fileSystem.makeTempDirectoryScoped({
81 prefix: "mock-update-server-outside-",
82 });
83 const rootRealPath = yield* fileSystem.realPath(root);
84 const outsideFile = path.join(outside, "outside.yml");
85 const linksDir = path.join(root, "links");
86 const symlinkPath = path.join(linksDir, "outside.yml");
87
88 yield* fileSystem.writeFileString(outsideFile, "version: outside\n");
89 yield* fileSystem.makeDirectory(linksDir, { recursive: true });
90 yield* fileSystem.symlink(outsideFile, symlinkPath);
91
92 yield* withMockUpdateServer(
93 rootRealPath,
94 Effect.gen(function* () {
95 const client = yield* HttpClient.HttpClient;
96 const response = yield* client.get("/links/outside.yml");
97
98 assert.equal(response.status, 404);
99 assert.equal(yield* response.text, "Not Found");
100 }),
101 );
102 }),
103 );
104});
scripts/mock-update-server.ts+147-37
...@@ -1,44 +1,154 @@...@@ -1,44 +1,154 @@
1import { resolve, relative } from "node:path";1import * as NodeHttp from "node:http";
2import { realpathSync } from "node:fs";
32
4const port = Number(process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000);3import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
5const root =4import * as NodeServices from "@effect/platform-node/NodeServices";
6 process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT ??5import { Config, Effect, FileSystem, Layer, Path } from "effect";
7 resolve(import.meta.dirname, "..", "release-mock");6import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
87
9const mockServerLog = (level: "info" | "warn" | "error" = "info", message: string) => {8interface MockUpdateServerConfig {
10 console[level](`[mock-update-server] ${message}`);9 readonly port: number;
11};10 readonly rootRealPath: string;
12
13function isWithinRoot(filePath: string): boolean {
14 try {
15 return !relative(realpathSync(root), realpathSync(filePath)).startsWith(".");
16 } catch (error) {
17 mockServerLog("error", `Error checking if file is within root: ${error}`);
18 return false;
19 }
20}11}
2112
22Bun.serve({13const resolveMockUpdateServerConfig = Effect.gen(function* () {
23 port,14 const fileSystem = yield* FileSystem.FileSystem;
24 hostname: "localhost",15 const path = yield* Path.Path;
25 fetch: async (request) => {16 const config = yield* Config.all({
26 const url = new URL(request.url);17 port: Config.port("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.withDefault(3000)),
27 const path = url.pathname;18 root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe(
28 mockServerLog("info", `Request received for path: ${path}`);19 Config.withDefault("../release-mock"),
29 const filePath = resolve(root, `.${path}`);20 ),
30 if (!isWithinRoot(filePath)) {21 }).asEffect();
31 mockServerLog("warn", `Attempted to access file outside of root: ${filePath}`);22
32 return new Response("Not Found", { status: 404 });23 const resolvedRoot = path.resolve(import.meta.dirname, config.root);
24
25 return {
26 port: config.port,
27 rootRealPath: yield* fileSystem.realPath(resolvedRoot),
28 } satisfies MockUpdateServerConfig;
29});
30
31const isOutsideRoot = (rootRealPath: string, filePath: string) =>
32 Effect.gen(function* () {
33 const path = yield* Path.Path;
34 const relativePath = path.relative(rootRealPath, filePath);
35 return (
36 relativePath === ".." || relativePath.startsWith("../") || relativePath.startsWith("..\\")
37 );
38 });
39
40const isWithinRoot = (rootRealPath: string, filePath: string) =>
41 Effect.gen(function* () {
42 const fileSystem = yield* FileSystem.FileSystem;
43 const resolvedFilePath = yield* fileSystem.realPath(filePath).pipe(
44 Effect.match({
45 onFailure: () => undefined,
46 onSuccess: (resolvedPath) => resolvedPath,
47 }),
48 );
49
50 return (
51 resolvedFilePath !== undefined && !(yield* isOutsideRoot(rootRealPath, resolvedFilePath))
52 );
53 });
54
55const resolveRequestedFilePath = (rootRealPath: string, requestUrl: string | undefined) =>
56 Effect.gen(function* () {
57 const path = yield* Path.Path;
58 const rawPath = (requestUrl ?? "/").split("?", 1)[0] ?? "/";
59 const decodedPath = yield* Effect.try({
60 try: () => decodeURIComponent(rawPath),
61 catch: () => null,
62 }).pipe(
63 Effect.match({
64 onFailure: () => undefined,
65 onSuccess: (value) => value,
66 }),
67 );
68
69 if (!decodedPath) {
70 return undefined;
33 }71 }
34 const file = Bun.file(filePath);72
35 if (!(await file.exists())) {73 if (decodedPath.includes("\0")) {
36 mockServerLog("warn", `Attempted to access non-existent file: ${filePath}`);74 return undefined;
37 return new Response("Not Found", { status: 404 });
38 }75 }
39 mockServerLog("info", `Serving file: ${filePath}`);
40 return new Response(file.stream());
41 },
42});
4376
44mockServerLog("info", `running on http://localhost:${port}`);77 const filePath = path.resolve(
78 rootRealPath,
79 `.${decodedPath.startsWith("/") ? decodedPath : `/${decodedPath}`}`,
80 );
81
82 return (yield* isOutsideRoot(rootRealPath, filePath)) ? undefined : filePath;
83 });
84
85const isServableFile = (rootRealPath: string, filePath: string) =>
86 Effect.gen(function* () {
87 const fileSystem = yield* FileSystem.FileSystem;
88 const stat = yield* fileSystem.stat(filePath).pipe(
89 Effect.match({
90 onFailure: () => undefined,
91 onSuccess: (info) => info,
92 }),
93 );
94
95 if (stat?.type !== "File") {
96 return false;
97 }
98
99 return yield* isWithinRoot(rootRealPath, filePath);
100 });
101
102export const makeMockUpdateRouteLayer = (rootRealPath: string) => {
103 return HttpRouter.add(
104 "*",
105 "*",
106 Effect.gen(function* () {
107 const request = yield* HttpServerRequest.HttpServerRequest;
108 const requestPath = (request.url ?? "/").split("?", 1)[0] ?? "/";
109 yield* Effect.logInfo(`Request received for path: ${requestPath}`);
110
111 const filePath = yield* resolveRequestedFilePath(rootRealPath, request.url);
112 if (!filePath) {
113 yield* Effect.logWarning(`Attempted to access file outside of root: ${request.url ?? "/"}`);
114 return HttpServerResponse.text("Not Found", { status: 404 });
115 }
116
117 if (!(yield* isServableFile(rootRealPath, filePath))) {
118 yield* Effect.logWarning(`Attempted to access invalid file: ${filePath}`);
119 return HttpServerResponse.text("Not Found", { status: 404 });
120 }
121
122 yield* Effect.logInfo(`Serving file: ${filePath}`);
123 return yield* HttpServerResponse.file(filePath, { status: 200 });
124 }).pipe(
125 Effect.catchCause((cause) =>
126 Effect.gen(function* () {
127 yield* Effect.logError(`Unhandled mock update request failure: ${cause}`);
128 return HttpServerResponse.text("Internal Server Error", { status: 500 });
129 }),
130 ),
131 ),
132 );
133};
134
135const makeMockUpdateServerLayer = (config: MockUpdateServerConfig) =>
136 HttpRouter.serve(makeMockUpdateRouteLayer(config.rootRealPath)).pipe(
137 Layer.provideMerge(
138 NodeHttpServer.layer(NodeHttp.createServer, {
139 host: "localhost",
140 port: config.port,
141 }),
142 ),
143 Layer.provideMerge(NodeServices.layer),
144 );
145
146if (import.meta.main) {
147 resolveMockUpdateServerConfig.pipe(
148 Effect.map(makeMockUpdateServerLayer),
149 Layer.unwrap,
150 Layer.launch,
151 Effect.provide(NodeServices.layer),
152 NodeRuntime.runMain,
153 );
154}
scripts/resolve-previous-release-tag.ts+7-5
...@@ -196,8 +196,10 @@ const command = Command.make(...@@ -196,8 +196,10 @@ const command = Command.make(
196 ),196 ),
197).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series."));197).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series."));
198198
199Command.run(command, { version: "0.0.0" }).pipe(199if (import.meta.main) {
200 Effect.scoped,200 Command.run(command, { version: "0.0.0" }).pipe(
201 Effect.provide(NodeServices.layer),201 Effect.scoped,
202 NodeRuntime.runMain,202 Effect.provide(NodeServices.layer),
203);203 NodeRuntime.runMain,
204 );
205}
scripts/tsconfig.json+2-4
...@@ -2,10 +2,8 @@...@@ -2,10 +2,8 @@
2 "extends": "../tsconfig.base.json",2 "extends": "../tsconfig.base.json",
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "bun"],5 "types": ["node"],
6 "lib": ["ES2023", "esnext.disposable"],6 "lib": ["ESNext", "esnext.disposable"],
7 "noEmit": true,
8 "allowImportingTsExtensions": true,
9 "plugins": [7 "plugins": [
10 {8 {
11 "name": "@effect/language-service"9 "name": "@effect/language-service"
scripts/update-release-package-versions.test.ts+199-57
...@@ -1,71 +1,213 @@...@@ -1,71 +1,213 @@
1import { describe, expect, it } from "vitest";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import { ConfigProvider, Effect, FileSystem, Layer, Path, Schema, SchemaGetter } from "effect";
4import { Command, CliError } from "effect/unstable/cli";
5import * as TestConsole from "effect/testing/TestConsole";
26
3import { parseArgs } from "./update-release-package-versions.ts";7import {
8 releasePackageFiles,
9 updateReleasePackageVersions,
10 updateReleasePackageVersionsCommand,
11} from "./update-release-package-versions.ts";
412
5describe("parseArgs", () => {13const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);
6 it("parses version only", () => {14const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });
7 expect(parseArgs(["1.2.3"])).toEqual({15const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
8 version: "1.2.3",16const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
9 rootDir: undefined,17 SchemaGetter.stringifyJson({ space: 2 }),
10 writeGithubOutput: false,18);
11 });19const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
12 });20 Schema.encode({
21 decode: PrettyJsonString,
22 encode: PrettyJsonString,
23 }),
24);
25const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
26const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
1327
14 it("parses version with --root", () => {28const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (
15 expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({29 rootDir: string,
16 version: "1.2.3",30 version: string,
17 rootDir: "/path",31) {
18 writeGithubOutput: false,32 const fs = yield* FileSystem.FileSystem;
19 });33 const path = yield* Path.Path;
20 });
2134
22 it("parses version with --github-output", () => {35 for (const relativePath of releasePackageFiles) {
23 expect(parseArgs(["1.2.3", "--github-output"])).toEqual({36 const filePath = path.join(rootDir, relativePath);
24 version: "1.2.3",37 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
25 rootDir: undefined,38 yield* fs.writeFileString(
26 writeGithubOutput: true,39 filePath,
27 });40 `${encodePackageJson({
28 });41 name: relativePath,
42 version,
43 private: true,
44 })}\n`,
45 );
46 }
47});
2948
30 it("parses version with --root and --github-output", () => {49const readReleaseVersions = Effect.fn("readReleaseVersions")(function* (rootDir: string) {
31 expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({50 const fs = yield* FileSystem.FileSystem;
32 version: "1.2.3",51 const path = yield* Path.Path;
33 rootDir: "/path",52 const versions = new Map<string, string>();
34 writeGithubOutput: true,
35 });
36 });
3753
38 it("accepts flags before the version positional", () => {54 for (const relativePath of releasePackageFiles) {
39 expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({55 const filePath = path.join(rootDir, relativePath);
40 version: "1.2.3",56 const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson));
41 rootDir: "/path",57 versions.set(relativePath, String(packageJson.version));
42 writeGithubOutput: true,58 }
43 });
44 });
4559
46 it("throws on missing version", () => {60 return versions;
47 expect(() => parseArgs([])).toThrow("Usage:");61});
48 });
4962
50 it("throws on duplicate version", () => {63const captureLogs = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
51 expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow(64 Effect.gen(function* () {
52 "Only one release version can be provided.",65 const result = yield* effect;
66 const logs = (yield* TestConsole.logLines).filter(
67 (line): line is string => typeof line === "string",
53 );68 );
69 return { result, logs };
54 });70 });
5571
56 it("throws on unknown flag", () => {72it.layer(ScriptTestLayer)("update-release-package-versions", (it) => {
57 expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown");73 it.effect("updates all release package versions under the provided root", () =>
58 });74 Effect.gen(function* () {
75 const fs = yield* FileSystem.FileSystem;
76 const baseDir = yield* fs.makeTempDirectoryScoped({
77 prefix: "update-release-package-versions-",
78 });
5979
60 it("throws on --root without value", () => {80 yield* writePackageJsonFixtures(baseDir, "0.0.1");
61 expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root.");
62 });
6381
64 it("does not consume version as --github-output value", () => {82 const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir });
65 expect(parseArgs(["--github-output", "1.2.3"])).toEqual({83 const versions = yield* readReleaseVersions(baseDir);
66 version: "1.2.3",84
67 rootDir: undefined,85 assert.deepStrictEqual(result, { changed: true });
68 writeGithubOutput: true,86 assert.deepStrictEqual(
69 });87 Array.from(versions.entries()),
70 });88 releasePackageFiles.map((relativePath) => [relativePath, "1.2.3"]),
89 );
90 }),
91 );
92
93 it.effect("returns changed=false when all versions already match", () =>
94 Effect.gen(function* () {
95 const fs = yield* FileSystem.FileSystem;
96 const baseDir = yield* fs.makeTempDirectoryScoped({
97 prefix: "update-release-package-versions-unchanged-",
98 });
99
100 yield* writePackageJsonFixtures(baseDir, "1.2.3");
101
102 const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir });
103
104 assert.deepStrictEqual(result, { changed: false });
105 }),
106 );
107
108 it.effect("accepts flags before the version positional and appends changed output", () =>
109 Effect.gen(function* () {
110 const fs = yield* FileSystem.FileSystem;
111 const path = yield* Path.Path;
112 const baseDir = yield* fs.makeTempDirectoryScoped({
113 prefix: "update-release-package-versions-cli-",
114 });
115 const githubOutputPath = path.join(baseDir, "github-output.txt");
116
117 yield* writePackageJsonFixtures(baseDir, "0.0.1");
118
119 yield* runCli(["--github-output", "--root", baseDir, "2.0.0"]).pipe(
120 Effect.provide(
121 ConfigProvider.layer(
122 ConfigProvider.fromEnv({
123 env: {
124 GITHUB_OUTPUT: githubOutputPath,
125 },
126 }),
127 ),
128 ),
129 );
130
131 const githubOutput = yield* fs.readFileString(githubOutputPath);
132 assert.equal(githubOutput, "changed=true\n");
133 }),
134 );
135
136 it.effect("logs when nothing changed", () =>
137 captureLogs(
138 Effect.gen(function* () {
139 const fs = yield* FileSystem.FileSystem;
140 const baseDir = yield* fs.makeTempDirectoryScoped({
141 prefix: "update-release-package-versions-cli-log-",
142 });
143
144 yield* writePackageJsonFixtures(baseDir, "3.0.0");
145 yield* runCli(["3.0.0", "--root", baseDir]);
146 }),
147 ).pipe(
148 Effect.tap(({ logs }) => {
149 assert.deepStrictEqual(logs, ["All package.json versions already match release version."]);
150 return Effect.void;
151 }),
152 ),
153 );
154
155 it.effect("requires GITHUB_OUTPUT when --github-output is set", () =>
156 Effect.gen(function* () {
157 const fs = yield* FileSystem.FileSystem;
158 const baseDir = yield* fs.makeTempDirectoryScoped({
159 prefix: "update-release-package-versions-cli-missing-output-",
160 });
161
162 yield* writePackageJsonFixtures(baseDir, "0.0.1");
163
164 const error = yield* runCli(["4.0.0", "--root", baseDir, "--github-output"]).pipe(
165 Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))),
166 Effect.flip,
167 );
168
169 assert.equal(
170 error.message,
171 'SchemaError(Expected string, got undefined\n at ["GITHUB_OUTPUT"])',
172 );
173 }),
174 );
175
176 it.effect("rejects unknown flags during cli parsing", () =>
177 Effect.gen(function* () {
178 const error = yield* runCli(["1.2.3", "--unknown"]).pipe(Effect.flip);
179
180 if (!CliError.isCliError(error)) {
181 assert.fail(`Expected CliError, got ${String(error)}`);
182 }
183
184 const optionError =
185 error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error;
186
187 if (!optionError || optionError._tag !== "UnrecognizedOption") {
188 assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`);
189 }
190
191 assert.equal(optionError.option, "--unknown");
192 }),
193 );
194
195 it.effect("rejects a missing version positional during cli parsing", () =>
196 Effect.gen(function* () {
197 const error = yield* runCli(["--github-output"]).pipe(Effect.flip);
198
199 if (!CliError.isCliError(error)) {
200 assert.fail(`Expected CliError, got ${String(error)}`);
201 }
202
203 const versionError =
204 error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error;
205
206 if (!versionError || versionError._tag !== "MissingArgument") {
207 assert.fail(`Expected MissingArgument, got ${String(versionError?._tag)}`);
208 }
209
210 assert.equal(versionError.argument, "version");
211 }),
212 );
71});213});
scripts/update-release-package-versions.ts+67-72
...@@ -1,8 +1,9 @@...@@ -1,8 +1,9 @@
1import { appendFileSync, readFileSync, writeFileSync } from "node:fs";1#!/usr/bin/env node
2import { resolve } from "node:path";
3import { fileURLToPath } from "node:url";
42
5import { parseCliArgs } from "@t3tools/shared/cliArgs";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Console, Effect, FileSystem, Option, Path, Schema, SchemaGetter } from "effect";
6import { Argument, Command, Flag } from "effect/unstable/cli";
67
7export const releasePackageFiles = [8export const releasePackageFiles = [
8 "apps/server/package.json",9 "apps/server/package.json",
...@@ -12,88 +13,82 @@ export const releasePackageFiles = [...@@ -12,88 +13,82 @@ export const releasePackageFiles = [
12] as const;13] as const;
1314
14interface UpdateReleasePackageVersionsOptions {15interface UpdateReleasePackageVersionsOptions {
15 readonly rootDir?: string;16 readonly rootDir?: string | undefined;
16}17}
1718
18interface MutablePackageJson {19const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
19 version?: string;20const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
20 [key: string]: unknown;21 SchemaGetter.stringifyJson({ space: 2 }),
21}22);
2223const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
23export function updateReleasePackageVersions(24 Schema.encode({
25 decode: PrettyJsonString,
26 encode: PrettyJsonString,
27 }),
28);
29const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
30const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
31
32export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* (
24 version: string,33 version: string,
25 options: UpdateReleasePackageVersionsOptions = {},34 options: UpdateReleasePackageVersionsOptions = {},
26): { changed: boolean } {35) {
27 const rootDir = resolve(options.rootDir ?? process.cwd());36 const fs = yield* FileSystem.FileSystem;
37 const path = yield* Path.Path;
38 const rootDir = path.resolve(options.rootDir ?? process.cwd());
28 let changed = false;39 let changed = false;
2940
30 for (const relativePath of releasePackageFiles) {41 for (const relativePath of releasePackageFiles) {
31 const filePath = resolve(rootDir, relativePath);42 const filePath = path.join(rootDir, relativePath);
32 const packageJson = JSON.parse(readFileSync(filePath, "utf8")) as MutablePackageJson;43 const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson));
33 if (packageJson.version === version) {44 if (packageJson.version === version) {
34 continue;45 continue;
35 }46 }
3647
37 packageJson.version = version;48 yield* fs.writeFileString(filePath, `${encodePackageJson({ ...packageJson, version })}\n`);
38 writeFileSync(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
39 changed = true;49 changed = true;
40 }50 }
4151
42 return { changed };52 return { changed };
43}53});
4454
45export function parseArgs(argv: ReadonlyArray<string>): {55const writeGithubOutput = Effect.fn("writeGithubOutput")(function* (changed: boolean) {
46 version: string;56 const fs = yield* FileSystem.FileSystem;
47 rootDir: string | undefined;57 const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT");
48 writeGithubOutput: boolean;58 yield* fs.writeFileString(githubOutputPath, `changed=${changed}\n`, { flag: "a" });
49} {59});
50 const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] });60
5161export const updateReleasePackageVersionsCommand = Command.make(
52 const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root");62 "update-release-package-versions",
53 if (unknownFlags.length > 0) {63 {
54 throw new Error(`Unknown argument: --${unknownFlags[0]}`);64 version: Argument.string("version").pipe(
55 }65 Argument.withDescription("Release version to write into each releasable package.json."),
5666 ),
57 if ("root" in flags && flags.root === null) {67 root: Flag.string("root").pipe(
58 throw new Error("Missing value for --root.");68 Flag.withDescription("Workspace root used to resolve the release package manifests."),
59 }69 Flag.optional,
6070 ),
61 if (positionals.length > 1) {71 githubOutput: Flag.boolean("github-output").pipe(
62 throw new Error("Only one release version can be provided.");72 Flag.withDescription("Append changed=<boolean> to GITHUB_OUTPUT."),
63 }73 Flag.withDefault(false),
6474 ),
65 if (positionals.length !== 1 || !positionals[0]) {75 },
66 throw new Error(76 ({ version, root, githubOutput }) =>
67 "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",77 updateReleasePackageVersions(version, {
68 );78 rootDir: Option.getOrUndefined(root),
69 }79 }).pipe(
7080 Effect.tap(({ changed }) =>
71 return {81 changed
72 version: positionals[0],82 ? Effect.void
73 rootDir: flags.root ?? undefined,83 : Console.log("All package.json versions already match release version."),
74 writeGithubOutput: "github-output" in flags,84 ),
75 };85 Effect.tap(({ changed }) => (githubOutput ? writeGithubOutput(changed) : Effect.void)),
76}86 ),
7787).pipe(Command.withDescription("Update release package versions across the workspace."));
78const isMain =88
79 process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url);89if (import.meta.main) {
8090 Command.run(updateReleasePackageVersionsCommand, { version: "0.0.0" }).pipe(
81if (isMain) {91 Effect.provide(NodeServices.layer),
82 const { version, rootDir, writeGithubOutput } = parseArgs(process.argv.slice(2));92 NodeRuntime.runMain,
83 const { changed } = updateReleasePackageVersions(
84 version,
85 rootDir === undefined ? {} : { rootDir },
86 );93 );
87
88 if (!changed) {
89 console.log("All package.json versions already match release version.");
90 }
91
92 if (writeGithubOutput) {
93 const githubOutputPath = process.env.GITHUB_OUTPUT;
94 if (!githubOutputPath) {
95 throw new Error("GITHUB_OUTPUT is required when --github-output is set.");
96 }
97 appendFileSync(githubOutputPath, `changed=${changed}\n`);
98 }
99}94}
tsconfig.base.json+8-3
...@@ -1,8 +1,13 @@...@@ -1,8 +1,13 @@
1{1{
2 "compilerOptions": {2 "compilerOptions": {
3 "target": "ES2023",3 "target": "ESNext",
4 "module": "ESNext",4 "module": "NodeNext",
5 "moduleResolution": "Bundler",5 "moduleResolution": "NodeNext",
6 "noEmit": true,
7 "allowImportingTsExtensions": true,
8 "rewriteRelativeImportExtensions": true,
9 "erasableSyntaxOnly": true,
10 "verbatimModuleSyntax": true,
6 "strict": true,11 "strict": true,
7 "noUncheckedIndexedAccess": true,12 "noUncheckedIndexedAccess": true,
8 "exactOptionalPropertyTypes": true,13 "exactOptionalPropertyTypes": true,