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:
7171
7272 - name: Verify preload bundle output
7373 run: |
74 test -f apps/desktop/dist-electron/preload.js
75 grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js
74 test -f apps/desktop/dist-electron/preload.cjs
75 grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.cjs
7676
7777 release_smoke:
7878 name: Release Smoke
apps/desktop/package.json+4-3
......@@ -2,13 +2,14 @@
22 "name": "@t3tools/desktop",
33 "version": "0.0.20",
44 "private": true,
5 "main": "dist-electron/main.js",
5 "type": "module",
6 "main": "dist-electron/main.cjs",
67 "scripts": {
78 "dev": "bun run --parallel dev:bundle dev:electron",
89 "dev:bundle": "tsdown --watch",
9 "dev:electron": "bun run scripts/dev-electron.mjs",
10 "dev:electron": "node scripts/dev-electron.mjs",
1011 "build": "tsdown",
11 "start": "bun run scripts/start-electron.mjs",
12 "start": "node scripts/start-electron.mjs",
1213 "typecheck": "tsc --noEmit",
1314 "test": "vitest run --passWithNoTests",
1415 "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) {
1717}
1818
1919const requiredFiles = [
20 "dist-electron/main.js",
21 "dist-electron/preload.js",
20 "dist-electron/main.cjs",
21 "dist-electron/preload.cjs",
2222 "../server/dist/bin.mjs",
2323];
2424const watchedDirectories = [
25 { directory: "dist-electron", files: new Set(["main.js", "preload.js"]) },
25 { directory: "dist-electron", files: new Set(["main.cjs", "preload.cjs"]) },
2626 { directory: "../server/dist", files: new Set(["bin.mjs"]) },
2727];
2828const forcedShutdownTimeoutMs = 1_500;
......@@ -69,7 +69,7 @@ function startApp() {
6969
7070 const app = spawn(
7171 resolveElectronPath(),
72 [`--t3code-dev-root=${desktopDir}`, "dist-electron/main.js"],
72 [`--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"],
7373 {
7474 cwd: desktopDir,
7575 env: childEnv,
apps/desktop/scripts/electron-launcher.mjs+7-37
......@@ -8,7 +8,6 @@ import {
88 mkdirSync,
99 mkdtempSync,
1010 readFileSync,
11 readdirSync,
1211 rmSync,
1312 statSync,
1413 writeFileSync,
......@@ -20,7 +19,7 @@ import { fileURLToPath } from "node:url";
2019const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
2120const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
2221const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";
23const LAUNCHER_VERSION = 1;
22const LAUNCHER_VERSION = 2;
2423
2524const __dirname = dirname(fileURLToPath(import.meta.url));
2625export const desktopDir = resolve(__dirname, "..");
......@@ -121,40 +120,6 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) {
121120 copyFileSync(iconPath, join(resourcesDir, "electron.icns"));
122121}
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
158123function readJson(path) {
159124 try {
160125 return JSON.parse(readFileSync(path, "utf8"));
......@@ -192,7 +157,6 @@ function buildMacLauncher(electronBinaryPath) {
192157 rmSync(targetAppBundlePath, { recursive: true, force: true });
193158 cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
194159 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
195 patchHelperBundleInfoPlists(targetAppBundlePath);
196160 writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
197161
198162 return targetBinaryPath;
......@@ -206,5 +170,11 @@ export function resolveElectronPath() {
206170 return electronBinaryPath;
207171 }
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
209179 return buildMacLauncher(electronBinaryPath);
210180}
apps/desktop/scripts/smoke-test.mjs+1-1
......@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
55const __dirname = dirname(fileURLToPath(import.meta.url));
66const desktopDir = resolve(__dirname, "..");
77const electronBin = resolve(desktopDir, "node_modules/.bin/electron");
8const mainJs = resolve(desktopDir, "dist-electron/main.js");
8const mainJs = resolve(desktopDir, "dist-electron/main.cjs");
99
1010console.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";
55const childEnv = { ...process.env };
66delete childEnv.ELECTRON_RUN_AS_NODE;
77
8const child = spawn(resolveElectronPath(), ["dist-electron/main.js"], {
8const child = spawn(resolveElectronPath(), ["dist-electron/main.cjs"], {
99 stdio: "inherit",
1010 cwd: desktopDir,
1111 env: childEnv,
apps/desktop/src/appBranding.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding";
3import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding.ts";
44
55describe("resolveDesktopAppStageLabel", () => {
66 it("uses Dev in desktop development", () => {
apps/desktop/src/appBranding.ts+1-1
......@@ -1,6 +1,6 @@
11import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";
22
3import { isNightlyDesktopVersion } from "./updateChannels";
3import { isNightlyDesktopVersion } from "./updateChannels.ts";
44
55const APP_BASE_NAME = "T3 Code";
66
apps/desktop/src/backendPort.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it, vi } from "vitest";
22
3import { resolveDesktopBackendPort } from "./backendPort";
3import { resolveDesktopBackendPort } from "./backendPort.ts";
44
55describe("resolveDesktopBackendPort", () => {
66 it("returns the starting port when it is available", async () => {
apps/desktop/src/backendReadiness.test.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 BackendReadinessAbortedError,
55 isBackendReadinessAborted,
66 waitForHttpReady,
7} from "./backendReadiness";
7} from "./backendReadiness.ts";
88
99describe("waitForHttpReady", () => {
1010 it("returns once the backend serves the requested readiness path", async () => {
apps/desktop/src/clientPersistence.test.ts+1-1
......@@ -18,7 +18,7 @@ import {
1818 writeSavedEnvironmentRegistry,
1919 writeSavedEnvironmentSecret,
2020 type DesktopSecretStorage,
21} from "./clientPersistence";
21} from "./clientPersistence.ts";
2222
2323const tempDirectories: string[] = [];
2424
apps/desktop/src/confirmDialog.test.ts+1-1
......@@ -11,7 +11,7 @@ vi.mock("electron", () => ({
1111 },
1212}));
1313
14import { showDesktopConfirmDialog } from "./confirmDialog";
14import { showDesktopConfirmDialog } from "./confirmDialog.ts";
1515
1616describe("showDesktopConfirmDialog", () => {
1717 beforeEach(() => {
apps/desktop/src/desktopSettings.test.ts+1-1
......@@ -11,7 +11,7 @@ import {
1111 setDesktopServerExposurePreference,
1212 setDesktopUpdateChannelPreference,
1313 writeDesktopSettings,
14} from "./desktopSettings";
14} from "./desktopSettings.ts";
1515
1616const tempDirectories: string[] = [];
1717
apps/desktop/src/desktopSettings.ts+1-1
......@@ -2,7 +2,7 @@ import * as FS from "node:fs";
22import * as Path from "node:path";
33import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";
44
5import { resolveDefaultDesktopUpdateChannel } from "./updateChannels";
5import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts";
66
77export interface DesktopSettings {
88 readonly serverExposureMode: DesktopServerExposureMode;
apps/desktop/src/main.ts+14-14
......@@ -36,14 +36,14 @@ import { autoUpdater } from "electron-updater";
3636import type { ContextMenuItem } from "@t3tools/contracts";
3737import { RotatingFileSink } from "@t3tools/shared/logging";
3838import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
39import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort";
39import { DEFAULT_DESKTOP_BACKEND_PORT, resolveDesktopBackendPort } from "./backendPort.ts";
4040import {
4141 DEFAULT_DESKTOP_SETTINGS,
4242 readDesktopSettings,
4343 setDesktopServerExposurePreference,
4444 setDesktopUpdateChannelPreference,
4545 writeDesktopSettings,
46} from "./desktopSettings";
46} from "./desktopSettings.ts";
4747import {
4848 readClientSettings,
4949 readSavedEnvironmentRegistry,
......@@ -52,14 +52,14 @@ import {
5252 writeClientSettings,
5353 writeSavedEnvironmentRegistry,
5454 writeSavedEnvironmentSecret,
55} from "./clientPersistence";
56import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness";
57import { showDesktopConfirmDialog } from "./confirmDialog";
58import { resolveDesktopServerExposure } from "./serverExposure";
59import { syncShellEnvironment } from "./syncShellEnvironment";
60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
61import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels";
62import { ServerListeningDetector } from "./serverListeningDetector";
55} from "./clientPersistence.ts";
56import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness.ts";
57import { showDesktopConfirmDialog } from "./confirmDialog.ts";
58import { resolveDesktopServerExposure } from "./serverExposure.ts";
59import { syncShellEnvironment } from "./syncShellEnvironment.ts";
60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState.ts";
61import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels.ts";
62import { ServerListeningDetector } from "./serverListeningDetector.ts";
6363import {
6464 createInitialDesktopUpdateState,
6565 reduceDesktopUpdateStateOnCheckFailure,
......@@ -71,9 +71,9 @@ import {
7171 reduceDesktopUpdateStateOnInstallFailure,
7272 reduceDesktopUpdateStateOnNoUpdate,
7373 reduceDesktopUpdateStateOnUpdateAvailable,
74} from "./updateMachine";
75import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";
76import { resolveDesktopAppBranding } from "./appBranding";
74} from "./updateMachine.ts";
75import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch.ts";
76import { resolveDesktopAppBranding } from "./appBranding.ts";
7777
7878syncShellEnvironment();
7979
......@@ -1967,7 +1967,7 @@ function createWindow(): BrowserWindow {
19671967 title: APP_DISPLAY_NAME,
19681968 ...getWindowTitleBarOptions(),
19691969 webPreferences: {
1970 preload: Path.join(__dirname, "preload.js"),
1970 preload: Path.join(__dirname, "preload.cjs"),
19711971 contextIsolation: true,
19721972 nodeIntegration: false,
19731973 sandbox: true,
apps/desktop/src/runtimeArch.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";
3import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch.ts";
44
55describe("resolveDesktopRuntimeInfo", () => {
66 it("detects Rosetta-translated Intel builds on Apple Silicon", () => {
apps/desktop/src/serverExposure.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { resolveDesktopServerExposure, resolveLanAdvertisedHost } from "./serverExposure";
3import { resolveDesktopServerExposure, resolveLanAdvertisedHost } from "./serverExposure.ts";
44
55describe("resolveLanAdvertisedHost", () => {
66 it("prefers an explicit host override", () => {
apps/desktop/src/serverListeningDetector.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { ServerListeningDetector } from "./serverListeningDetector";
3import { ServerListeningDetector } from "./serverListeningDetector.ts";
44
55describe("ServerListeningDetector", () => {
66 it("resolves when the server logs the listening line", async () => {
apps/desktop/src/syncShellEnvironment.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it, vi } from "vitest";
22
3import { syncShellEnvironment } from "./syncShellEnvironment";
3import { syncShellEnvironment } from "./syncShellEnvironment.ts";
44
55describe("syncShellEnvironment", () => {
66 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 {
33 mergePathEntries,
44 readPathFromLaunchctl,
55 readEnvironmentFromLoginShell,
6 ShellEnvironmentReader,
76} from "@t3tools/shared/shell";
7import type { ShellEnvironmentReader } from "@t3tools/shared/shell";
88
99const LOGIN_SHELL_ENV_NAMES = [
1010 "PATH",
apps/desktop/src/updateChannels.test.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 doesVersionMatchDesktopUpdateChannel,
55 isNightlyDesktopVersion,
66 resolveDefaultDesktopUpdateChannel,
7} from "./updateChannels";
7} from "./updateChannels.ts";
88
99describe("isNightlyDesktopVersion", () => {
1010 it("detects packaged nightly versions", () => {
apps/desktop/src/updateMachine.test.ts+1-1
......@@ -11,7 +11,7 @@ import {
1111 reduceDesktopUpdateStateOnInstallFailure,
1212 reduceDesktopUpdateStateOnNoUpdate,
1313 reduceDesktopUpdateStateOnUpdateAvailable,
14} from "./updateMachine";
14} from "./updateMachine.ts";
1515
1616const runtimeInfo = {
1717 hostArch: "x64",
apps/desktop/src/updateMachine.ts+1-1
......@@ -4,7 +4,7 @@ import type {
44 DesktopUpdateState,
55} from "@t3tools/contracts";
66
7import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState";
7import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState.ts";
88
99export function createInitialDesktopUpdateState(
1010 currentVersion: string,
apps/desktop/src/updateState.test.ts+1-1
......@@ -6,7 +6,7 @@ import {
66 getAutoUpdateDisabledReason,
77 nextStatusAfterDownloadFailure,
88 shouldBroadcastDownloadProgress,
9} from "./updateState";
9} from "./updateState.ts";
1010
1111const baseState: DesktopUpdateState = {
1212 enabled: true,
apps/desktop/tsconfig.json+1-1
......@@ -3,7 +3,7 @@
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node", "electron"],
6 "lib": ["ES2023", "DOM", "esnext.disposable"]
6 "lib": ["ESNext", "DOM", "esnext.disposable"]
77 },
88 "include": ["src", "tsdown.config.ts"]
99}
apps/desktop/tsdown.config.ts+1-1
......@@ -4,7 +4,7 @@ const shared = {
44 format: "cjs" as const,
55 outDir: "dist-electron",
66 sourcemap: true,
7 outExtensions: () => ({ js: ".js" }),
7 outExtensions: () => ({ js: ".cjs" }),
88};
99
1010export default defineConfig([
apps/server/package.json+2-1
......@@ -15,8 +15,9 @@
1515 ],
1616 "type": "module",
1717 "scripts": {
18 "dev": "bun run src/bin.ts",
18 "dev": "node --watch src/bin.ts",
1919 "build": "node scripts/cli.ts build",
20 "build:bundle": "tsdown",
2021 "start": "node dist/bin.mjs",
2122 "prepare": "effect-language-service patch",
2223 "typecheck": "tsc --noEmit",
apps/server/scripts/cli.ts+15-20
......@@ -147,13 +147,13 @@ const buildCmd = Command.make(
147147
148148 yield* Effect.log("[cli] Running tsdown...");
149149 yield* runCommand(
150 ChildProcess.make({
150 ChildProcess.make(process.execPath, ["--run", "build:bundle"], {
151151 cwd: serverDir,
152152 stdout: config.verbose ? "inherit" : "ignore",
153153 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.
155155 shell: process.platform === "win32",
156 })`bun tsdown`,
156 }),
157157 );
158158
159159 const webDist = path.join(repoRoot, "apps/web/dist");
......@@ -203,10 +203,8 @@ const publishCmd = Command.make(
203203 }
204204
205205 yield* Effect.acquireUseRelease(
206 // Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts
206 // Acquire: backup package.json, resolve catalog dependencies, and strip devDependencies/scripts
207207 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.
210208 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
211209 const pkg: PackageJson = {
212210 name: serverPackageJson.name,
......@@ -216,25 +214,22 @@ const publishCmd = Command.make(
216214 version,
217215 engines: serverPackageJson.engines,
218216 files: serverPackageJson.files,
219 dependencies: serverPackageJson.dependencies,
220 overrides: rootPackageJson.overrides,
217 dependencies: resolveCatalogDependencies(
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 ),
221227 };
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
234229 const original = yield* fs.readFileString(packageJsonPath);
235230 yield* fs.writeFileString(backupPath, original);
236231 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
239234 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
240235 return { iconBackups };
apps/server/src/auth/Layers/AuthControlPlane.test.ts+1-1
......@@ -2,7 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
33import { Effect, Layer } from "effect";
44
5import { ServerConfigShape } from "../../config.ts";
5import type { ServerConfigShape } from "../../config.ts";
66import { ServerConfig } from "../../config.ts";
77import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";
88import { ServerSecretStoreLive } from "./ServerSecretStore.ts";
apps/server/src/auth/Layers/AuthControlPlane.ts+3-1
......@@ -10,8 +10,10 @@ import { layerConfig as SqlitePersistenceLayerLive } from "../../persistence/Lay
1010import {
1111 AuthControlPlane,
1212 AuthControlPlaneError,
13 AuthControlPlaneShape,
1413 DEFAULT_SESSION_SUBJECT,
14} from "../Services/AuthControlPlane.ts";
15import type {
16 AuthControlPlaneShape,
1517 IssuedBearerSession,
1618 IssuedPairingLink,
1719} from "../Services/AuthControlPlane.ts";
apps/server/src/auth/Services/AuthControlPlane.ts+1-1
......@@ -5,7 +5,7 @@ import type {
55 AuthSessionId,
66} from "@t3tools/contracts";
77import { Data, DateTime, Duration, Effect, Context } from "effect";
8import { SessionRole } from "./SessionCredentialService";
8import type { SessionRole } from "./SessionCredentialService.ts";
99
1010export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";
1111
apps/server/src/auth/utils.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { deriveAuthClientMetadata } from "./utils";
3import { deriveAuthClientMetadata } from "./utils.ts";
44
55describe("deriveAuthClientMetadata", () => {
66 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";
55import { Command } from "effect/unstable/cli";
66
77import { NetService } from "@t3tools/shared/Net";
8import { cli } from "./cli";
9import { version } from "../package.json" with { type: "json" };
8import { cli } from "./cli.ts";
9import packageJson from "../package.json" with { type: "json" };
1010
1111const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
1212
13Command.run(cli, { version }).pipe(
13Command.run(cli, { version: packageJson.version }).pipe(
1414 Effect.scoped,
1515 Effect.provide(CliRuntimeLayer),
1616 NodeRuntime.runMain,
apps/server/src/bootstrap.test.ts+1-1
......@@ -10,7 +10,7 @@ import * as Fiber from "effect/Fiber";
1010import { TestClock } from "effect/testing";
1111import { vi } from "vitest";
1212
13import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap";
13import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap.ts";
1414import { assertNone, assertSome } from "@effect/vitest/utils";
1515
1616const 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"
55
66import { NetService } from "@t3tools/shared/Net";
77import * as NodeServices from "@effect/platform-node/NodeServices";
8import { deriveServerPaths } from "./config";
9import { resolveServerConfig } from "./cli";
8import { deriveServerPaths } from "./config.ts";
9import { resolveServerConfig } from "./cli.ts";
1010
1111it.layer(NodeServices.layer)("cli config resolution", (it) => {
1212 const defaultObservabilityConfig = {
apps/server/src/cli.ts+12-11
......@@ -40,30 +40,31 @@ import {
4040 RuntimeMode,
4141 type ServerConfigShape,
4242 type StartupPresentation,
43} from "./config";
44import { readBootstrapEnvelope } from "./bootstrap";
45import { expandHomePath, resolveBaseDir } from "./os-jank";
46import { runServer } from "./server";
43} from "./config.ts";
44import { readBootstrapEnvelope } from "./bootstrap.ts";
45import { expandHomePath, resolveBaseDir } from "./os-jank.ts";
46import { runServer } from "./server.ts";
4747import { AuthControlPlaneRuntimeLive } from "./auth/Layers/AuthControlPlane.ts";
4848import {
4949 formatIssuedPairingCredential,
5050 formatIssuedSession,
5151 formatPairingCredentialList,
5252 formatSessionList,
53} from "./cliAuthFormat";
54import { AuthControlPlane, AuthControlPlaneShape } from "./auth/Services/AuthControlPlane.ts";
53} from "./cliAuthFormat.ts";
54import { AuthControlPlane } from "./auth/Services/AuthControlPlane.ts";
55import type { AuthControlPlaneShape } from "./auth/Services/AuthControlPlane.ts";
5556import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
5657import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
57import { OrchestrationLayerLive } from "./orchestration/runtimeLayer";
58import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
5859import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
5960import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
60import { getAutoBootstrapDefaultModelSelection } from "./serverRuntimeStartup";
61import { getAutoBootstrapDefaultModelSelection } from "./serverRuntimeStartup.ts";
6162import {
6263 clearPersistedServerRuntimeState,
6364 readPersistedServerRuntimeState,
64} from "./serverRuntimeState";
65import { WorkspacePaths } from "./workspace/Services/WorkspacePaths";
66import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths";
65} from "./serverRuntimeState.ts";
66import { WorkspacePaths } from "./workspace/Services/WorkspacePaths.ts";
67import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
6768
6869const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
6970
apps/server/src/codexAppServerManager.test.ts+1-1
......@@ -15,7 +15,7 @@ import {
1515 normalizeCodexModelSlug,
1616 readCodexAccountSnapshot,
1717 resolveCodexModelForAccount,
18} from "./codexAppServerManager";
18} from "./codexAppServerManager.ts";
1919
2020const asThreadId = (value: string): ThreadId => ThreadId.make(value);
2121
apps/server/src/codexAppServerManager.ts+5-5
......@@ -25,16 +25,16 @@ import {
2525 formatCodexCliUpgradeMessage,
2626 isCodexCliVersionSupported,
2727 parseCodexCliVersion,
28} from "./provider/codexCliVersion";
28} from "./provider/codexCliVersion.ts";
2929import {
3030 readCodexAccountSnapshot,
3131 resolveCodexModelForAccount,
3232 type CodexAccountSnapshot,
33} from "./provider/codexAccount";
34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer";
33} from "./provider/codexAccount.ts";
34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer.ts";
3535
36export { buildCodexInitializeParams } from "./provider/codexAppServer";
37export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount";
36export { buildCodexInitializeParams } from "./provider/codexAppServer.ts";
37export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount.ts";
3838
3939type PendingRequestKey = string;
4040
apps/server/src/environment/Layers/ServerEnvironment.ts+2-2
......@@ -3,7 +3,7 @@ import { Effect, FileSystem, Layer, Path, Random } from "effect";
33
44import { ServerConfig } from "../../config.ts";
55import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";
6import { version } from "../../../package.json" with { type: "json" };
6import packageJson from "../../../package.json" with { type: "json" };
77import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
88
99function platformOs(): ExecutionEnvironmentDescriptor["platform"]["os"] {
......@@ -77,7 +77,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function
7777 os: platformOs(),
7878 arch: platformArch(),
7979 },
80 serverVersion: version,
80 serverVersion: packageJson.version,
8181 capabilities: {
8282 repositoryIdentity: true,
8383 },
apps/server/src/git/Layers/GitHubCli.test.ts+1-1
......@@ -6,7 +6,7 @@ vi.mock("../../processRunner", () => ({
66 runProcess: vi.fn(),
77}));
88
9import { runProcess } from "../../processRunner";
9import { runProcess } from "../../processRunner.ts";
1010import { GitHubCli } from "../Services/GitHubCli.ts";
1111import { GitHubCliLive } from "./GitHubCli.ts";
1212
apps/server/src/git/Layers/GitHubCli.ts+1-1
......@@ -1,7 +1,7 @@
11import { Effect, Layer, Result, Schema, SchemaIssue } from "effect";
22import { TrimmedNonEmptyString } from "@t3tools/contracts";
33
4import { runProcess } from "../../processRunner";
4import { runProcess } from "../../processRunner.ts";
55import { GitHubCliError } from "@t3tools/contracts";
66import {
77 GitHubCli,
apps/server/src/git/Layers/GitManager.ts+2-1
......@@ -38,7 +38,8 @@ import {
3838 type GitManagerShape,
3939 type GitRunStackedActionOptions,
4040} from "../Services/GitManager.ts";
41import { GitCore, GitStatusDetails } from "../Services/GitCore.ts";
41import { GitCore } from "../Services/GitCore.ts";
42import type { GitStatusDetails } from "../Services/GitCore.ts";
4243import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";
4344import { TextGeneration } from "../Services/TextGeneration.ts";
4445import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";
apps/server/src/git/Services/GitHubCli.ts+1-1
......@@ -8,7 +8,7 @@
88import { Context } from "effect";
99import type { Effect } from "effect";
1010
11import type { ProcessRunResult } from "../../processRunner";
11import type { ProcessRunResult } from "../../processRunner.ts";
1212import type { GitHubCliError } from "@t3tools/contracts";
1313
1414export interface GitHubPullRequestSummary {
apps/server/src/http.ts+4-4
......@@ -15,12 +15,12 @@ import {
1515 ATTACHMENTS_ROUTE_PREFIX,
1616 normalizeAttachmentRelativePath,
1717 resolveAttachmentRelativePath,
18} from "./attachmentPaths";
19import { resolveAttachmentPathById } from "./attachmentStore";
20import { resolveStaticDir, ServerConfig } from "./config";
18} from "./attachmentPaths.ts";
19import { resolveAttachmentPathById } from "./attachmentStore.ts";
20import { resolveStaticDir, ServerConfig } from "./config.ts";
2121import { decodeOtlpTraceRecords } from "./observability/TraceRecord.ts";
2222import { BrowserTraceCollector } from "./observability/Services/BrowserTraceCollector.ts";
23import { ProjectFaviconResolver } from "./project/Services/ProjectFaviconResolver";
23import { ProjectFaviconResolver } from "./project/Services/ProjectFaviconResolver.ts";
2424import { ServerAuth } from "./auth/Services/ServerAuth.ts";
2525import { respondToAuthError } from "./auth/http.ts";
2626import { 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";
33import { assert, it } from "@effect/vitest";
44import { assertFailure } from "@effect/vitest/utils";
55import { Cause, Effect, FileSystem, Layer, Logger, Path, Schema } from "effect";
6import { ServerConfig } from "./config";
6import { ServerConfig } from "./config.ts";
77
88import {
99 DEFAULT_KEYBINDINGS,
......@@ -13,7 +13,7 @@ import {
1313 compileResolvedKeybindingRule,
1414 compileResolvedKeybindingsConfig,
1515 parseKeybindingShortcut,
16} from "./keybindings";
16} from "./keybindings.ts";
1717import { KeybindingsConfigError } from "@t3tools/contracts";
1818
1919const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
apps/server/src/keybindings.ts+2-2
......@@ -19,7 +19,7 @@ import {
1919 THREAD_JUMP_KEYBINDING_COMMANDS,
2020 type ServerConfigIssue,
2121} from "@t3tools/contracts";
22import { Mutable } from "effect/Types";
22import type { Mutable } from "effect/Types";
2323import {
2424 Array,
2525 Cache,
......@@ -44,7 +44,7 @@ import {
4444 Stream,
4545} from "effect";
4646import * as Semaphore from "effect/Semaphore";
47import { ServerConfig } from "./config";
47import { ServerConfig } from "./config.ts";
4848import { fromLenientJson } from "@t3tools/shared/schemaJson";
4949
5050type WhenToken =
apps/server/src/observability/LocalFileTracer.ts+8-3
......@@ -1,7 +1,8 @@
11import type * as Exit from "effect/Exit";
22import { Effect, Option, Tracer } from "effect";
33
4import { EffectTraceRecord, spanToTraceRecord } from "./TraceRecord.ts";
4import { spanToTraceRecord } from "./TraceRecord.ts";
5import type { EffectTraceRecord } from "./TraceRecord.ts";
56import { makeTraceSink, type TraceSink } from "./TraceSink.ts";
67
78export interface LocalFileTracerOptions {
......@@ -27,12 +28,16 @@ class LocalFileSpan implements Tracer.Span {
2728 status: Tracer.SpanStatus;
2829 attributes: Map<string, unknown>;
2930 events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]>;
31 private readonly delegate: Tracer.Span;
32 private readonly push: (record: EffectTraceRecord) => void;
3033
3134 constructor(
3235 options: Parameters<Tracer.Tracer["span"]>[0],
33 private readonly delegate: Tracer.Span,
34 private readonly push: (record: EffectTraceRecord) => void,
36 delegate: Tracer.Span,
37 push: (record: EffectTraceRecord) => void,
3538 ) {
39 this.delegate = delegate;
40 this.push = push;
3641 this.name = delegate.name;
3742 this.spanId = delegate.spanId;
3843 this.traceId = delegate.traceId;
apps/server/src/open.test.ts+1-1
......@@ -8,7 +8,7 @@ import {
88 launchDetached,
99 resolveAvailableEditors,
1010 resolveEditorLaunch,
11} from "./open";
11} from "./open.ts";
1212
1313it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
1414 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";
2121import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts";
2222import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
2323import { RuntimeReceiptBus } from "../Services/RuntimeReceiptBus.ts";
24import { CheckpointStoreError } from "../../checkpointing/Errors.ts";
25import { OrchestrationDispatchError } from "../Errors.ts";
24import type { CheckpointStoreError } from "../../checkpointing/Errors.ts";
25import type { OrchestrationDispatchError } from "../Errors.ts";
2626import { isGitRepository } from "../../git/Utils.ts";
2727import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";
2828import { 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";
1919import { GitCore } from "../../git/Services/GitCore.ts";
2020import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";
2121import { 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";
2324import { TextGeneration } from "../../git/Services/TextGeneration.ts";
2425import { ProviderService } from "../../provider/Services/ProviderService.ts";
2526import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
apps/server/src/orchestration/Normalizer.ts+4-4
......@@ -6,10 +6,10 @@ import {
66 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
77} from "@t3tools/contracts";
88
9import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore";
10import { ServerConfig } from "../config";
11import { parseBase64DataUrl } from "../imageMime";
12import { WorkspacePaths } from "../workspace/Services/WorkspacePaths";
9import { createAttachmentId, resolveAttachmentPath } from "../attachmentStore.ts";
10import { ServerConfig } from "../config.ts";
11import { parseBase64DataUrl } from "../imageMime.ts";
12import { WorkspacePaths } from "../workspace/Services/WorkspacePaths.ts";
1313
1414export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
1515 Effect.gen(function* () {
apps/server/src/os-jank.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it, vi } from "vitest";
22
3import { fixPath } from "./os-jank";
3import { fixPath } from "./os-jank.ts";
44
55describe("fixPath", () => {
66 it("hydrates PATH on linux using the resolved login shell", () => {
apps/server/src/processRunner.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { runProcess } from "./processRunner";
3import { runProcess } from "./processRunner.ts";
44
55describe("runProcess", () => {
66 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 {
1717 type SDKResultMessage,
1818 type SettingSource,
1919 type SDKUserMessage,
20 ModelUsage,
20 type ModelUsage,
2121} from "@anthropic-ai/claude-agent-sdk";
2222import { parseCliArgs } from "@t3tools/shared/cliArgs";
2323import {
apps/server/src/provider/Layers/ClaudeProvider.ts+5-5
......@@ -25,11 +25,11 @@ import {
2525 providerModelsFromSettings,
2626 spawnAndCollect,
2727 type CommandResult,
28} from "../providerSnapshot";
29import { compareCliVersions } from "../cliVersion";
30import { makeManagedServerProvider } from "../makeManagedServerProvider";
31import { ClaudeProvider } from "../Services/ClaudeProvider";
32import { ServerSettingsService } from "../../serverSettings";
28} from "../providerSnapshot.ts";
29import { compareCliVersions } from "../cliVersion.ts";
30import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
31import { ClaudeProvider } from "../Services/ClaudeProvider.ts";
32import { ServerSettingsService } from "../../serverSettings.ts";
3333import { ServerSettingsError } from "@t3tools/contracts";
3434
3535const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = {
apps/server/src/provider/Layers/CodexProvider.ts+7-7
......@@ -32,22 +32,22 @@ import {
3232 providerModelsFromSettings,
3333 spawnAndCollect,
3434 type CommandResult,
35} from "../providerSnapshot";
36import { makeManagedServerProvider } from "../makeManagedServerProvider";
35} from "../providerSnapshot.ts";
36import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
3737import {
3838 formatCodexCliUpgradeMessage,
3939 isCodexCliVersionSupported,
4040 parseCodexCliVersion,
41} from "../codexCliVersion";
41} from "../codexCliVersion.ts";
4242import {
4343 adjustCodexModelsForAccount,
4444 codexAuthSubLabel,
4545 codexAuthSubType,
4646 type CodexAccountSnapshot,
47} from "../codexAccount";
48import { probeCodexDiscovery } from "../codexAppServer";
49import { CodexProvider } from "../Services/CodexProvider";
50import { ServerSettingsService } from "../../serverSettings";
47} from "../codexAccount.ts";
48import { probeCodexDiscovery } from "../codexAppServer.ts";
49import { CodexProvider } from "../Services/CodexProvider.ts";
50import { ServerSettingsService } from "../../serverSettings.ts";
5151import { ServerSettingsError } from "@t3tools/contracts";
5252
5353const 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";
44
55import { Effect, Layer, Stream } from "effect";
66
7import { ClaudeAdapter, ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
8import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
7import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
8import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
9import { CodexAdapter } from "../Services/CodexAdapter.ts";
10import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
911import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
1012import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
1113import { ProviderUnsupportedError } from "../Errors.ts";
apps/server/src/provider/Layers/ProviderRegistry.test.ts+6-6
......@@ -28,12 +28,12 @@ import {
2828 hasCustomModelProvider,
2929 parseAuthStatusFromOutput,
3030 readCodexConfigModelProvider,
31} from "./CodexProvider";
32import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./ClaudeProvider";
33import { haveProvidersChanged, ProviderRegistryLive } from "./ProviderRegistry";
34import { ServerConfig } from "../../config";
35import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings";
36import { ProviderRegistry } from "../Services/ProviderRegistry";
31} from "./CodexProvider.ts";
32import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./ClaudeProvider.ts";
33import { haveProvidersChanged, ProviderRegistryLive } from "./ProviderRegistry.ts";
34import { ServerConfig } from "../../config.ts";
35import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";
36import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
3737
3838// ── Test helpers ────────────────────────────────────────────────────
3939
apps/server/src/provider/Layers/ProviderRegistry.ts+9-9
......@@ -6,14 +6,14 @@
66import type { ProviderKind, ServerProvider } from "@t3tools/contracts";
77import { Effect, Equal, FileSystem, Layer, Path, PubSub, Ref, Stream } from "effect";
88
9import { ServerConfig } from "../../config";
10import { ClaudeProviderLive } from "./ClaudeProvider";
11import { CodexProviderLive } from "./CodexProvider";
12import type { ClaudeProviderShape } from "../Services/ClaudeProvider";
13import { ClaudeProvider } from "../Services/ClaudeProvider";
14import type { CodexProviderShape } from "../Services/CodexProvider";
15import { CodexProvider } from "../Services/CodexProvider";
16import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry";
9import { ServerConfig } from "../../config.ts";
10import { ClaudeProviderLive } from "./ClaudeProvider.ts";
11import { CodexProviderLive } from "./CodexProvider.ts";
12import type { ClaudeProviderShape } from "../Services/ClaudeProvider.ts";
13import { ClaudeProvider } from "../Services/ClaudeProvider.ts";
14import type { CodexProviderShape } from "../Services/CodexProvider.ts";
15import { CodexProvider } from "../Services/CodexProvider.ts";
16import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry.ts";
1717import {
1818 hydrateCachedProvider,
1919 PROVIDER_CACHE_IDS,
......@@ -21,7 +21,7 @@ import {
2121 readProviderStatusCache,
2222 resolveProviderStatusCachePath,
2323 writeProviderStatusCache,
24} from "../providerStatusCache";
24} from "../providerStatusCache.ts";
2525
2626const loadProviders = (
2727 codexProvider: CodexProviderShape,
apps/server/src/provider/Services/ClaudeProvider.ts+1-1
......@@ -1,6 +1,6 @@
11import { Context } from "effect";
22
3import type { ServerProviderShape } from "./ServerProvider";
3import type { ServerProviderShape } from "./ServerProvider.ts";
44
55export interface ClaudeProviderShape extends ServerProviderShape {}
66
apps/server/src/provider/Services/CodexProvider.ts+1-1
......@@ -1,6 +1,6 @@
11import { Context } from "effect";
22
3import type { ServerProviderShape } from "./ServerProvider";
3import type { ServerProviderShape } from "./ServerProvider.ts";
44
55export interface CodexProviderShape extends ServerProviderShape {}
66
apps/server/src/provider/cliVersion.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { assert, describe, it } from "@effect/vitest";
22
3import { compareCliVersions, normalizeCliVersion } from "./cliVersion";
3import { compareCliVersions, normalizeCliVersion } from "./cliVersion.ts";
44
55describe("cliVersion", () => {
66 it("normalizes versions with a missing patch segment", () => {
apps/server/src/provider/codexAppServer.ts+1-1
......@@ -1,7 +1,7 @@
11import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
22import readline from "node:readline";
33import type { ServerProviderSkill } from "@t3tools/contracts";
4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount";
4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount.ts";
55
66interface JsonRpcProbeResponse {
77 readonly id?: unknown;
apps/server/src/provider/codexCliVersion.ts+1-1
......@@ -1,4 +1,4 @@
1import { compareCliVersions, normalizeCliVersion } from "./cliVersion";
1import { compareCliVersions, normalizeCliVersion } from "./cliVersion.ts";
22
33const 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";
22import { Duration, Effect, PubSub, Ref, Scope, Stream } from "effect";
33import * as Semaphore from "effect/Semaphore";
44
5import type { ServerProviderShape } from "./Services/ServerProvider";
5import type { ServerProviderShape } from "./Services/ServerProvider.ts";
66import { ServerSettingsError } from "@t3tools/contracts";
77
88export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(function* <
apps/server/src/provider/providerSnapshot.ts+1-1
......@@ -10,7 +10,7 @@ import type {
1010import { Effect, Stream } from "effect";
1111import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1212import { normalizeModelSlug } from "@t3tools/shared/model";
13import { isWindowsCommandNotFound } from "../processRunner";
13import { isWindowsCommandNotFound } from "../processRunner.ts";
1414
1515export const DEFAULT_TIMEOUT_MS = 4_000;
1616
apps/server/src/provider/providerStatusCache.test.ts+1-1
......@@ -8,7 +8,7 @@ import {
88 readProviderStatusCache,
99 resolveProviderStatusCachePath,
1010 writeProviderStatusCache,
11} from "./providerStatusCache";
11} from "./providerStatusCache.ts";
1212
1313const makeProvider = (
1414 provider: ServerProvider["provider"],
apps/server/src/server.ts+49-49
......@@ -1,7 +1,7 @@
11import { Effect, Layer } from "effect";
22import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
33
4import { ServerConfig } from "./config";
4import { ServerConfig } from "./config.ts";
55import {
66 attachmentsRouteLayer,
77 otlpTracesProxyRouteLayer,
......@@ -9,46 +9,46 @@ import {
99 serverEnvironmentRouteLayer,
1010 staticAndDevRouteLayer,
1111 browserApiCorsLayer,
12} from "./http";
13import { fixPath } from "./os-jank";
14import { websocketRpcRouteLayer } from "./ws";
15import { OpenLive } from "./open";
16import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite";
17import { ServerLifecycleEventsLive } from "./serverLifecycleEvents";
18import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService";
19import { makeEventNdjsonLogger } from "./provider/Layers/EventNdjsonLogger";
20import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
21import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime";
22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
23import { makeClaudeAdapterLive } from "./provider/Layers/ClaudeAdapter";
24import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
25import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper";
27import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery";
28import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore";
29import { GitCoreLive } from "./git/Layers/GitCore";
30import { GitHubCliLive } from "./git/Layers/GitHubCli";
31import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster";
32import { RoutingTextGenerationLive } from "./git/Layers/RoutingTextGeneration";
33import { TerminalManagerLive } from "./terminal/Layers/Manager";
34import { GitManagerLive } from "./git/Layers/GitManager";
35import { KeybindingsLive } from "./keybindings";
36import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup";
37import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor";
38import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus";
39import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion";
40import { ProviderCommandReactorLive } from "./orchestration/Layers/ProviderCommandReactor";
41import { CheckpointReactorLive } from "./orchestration/Layers/CheckpointReactor";
42import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry";
43import { ServerSettingsLive } from "./serverSettings";
44import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver";
45import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver";
46import { WorkspaceEntriesLive } from "./workspace/Layers/WorkspaceEntries";
47import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem";
48import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths";
49import { ProjectSetupScriptRunnerLive } from "./project/Layers/ProjectSetupScriptRunner";
50import { ObservabilityLive } from "./observability/Layers/Observability";
51import { ServerEnvironmentLive } from "./environment/Layers/ServerEnvironment";
12} from "./http.ts";
13import { fixPath } from "./os-jank.ts";
14import { websocketRpcRouteLayer } from "./ws.ts";
15import { OpenLive } from "./open.ts";
16import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
17import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
18import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
19import { makeEventNdjsonLogger } from "./provider/Layers/EventNdjsonLogger.ts";
20import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
21import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";
22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter.ts";
23import { makeClaudeAdapterLive } from "./provider/Layers/ClaudeAdapter.ts";
24import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
25import { makeProviderServiceLive } from "./provider/Layers/ProviderService.ts";
26import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts";
27import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts";
28import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts";
29import { GitCoreLive } from "./git/Layers/GitCore.ts";
30import { GitHubCliLive } from "./git/Layers/GitHubCli.ts";
31import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.ts";
32import { RoutingTextGenerationLive } from "./git/Layers/RoutingTextGeneration.ts";
33import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
34import { GitManagerLive } from "./git/Layers/GitManager.ts";
35import { KeybindingsLive } from "./keybindings.ts";
36import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
37import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
38import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
39import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
40import { ProviderCommandReactorLive } from "./orchestration/Layers/ProviderCommandReactor.ts";
41import { CheckpointReactorLive } from "./orchestration/Layers/CheckpointReactor.ts";
42import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts";
43import { ServerSettingsLive } from "./serverSettings.ts";
44import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
45import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
46import { WorkspaceEntriesLive } from "./workspace/Layers/WorkspaceEntries.ts";
47import { WorkspaceFileSystemLive } from "./workspace/Layers/WorkspaceFileSystem.ts";
48import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
49import { ProjectSetupScriptRunnerLive } from "./project/Layers/ProjectSetupScriptRunner.ts";
50import { ObservabilityLive } from "./observability/Layers/Observability.ts";
51import { ServerEnvironmentLive } from "./environment/Layers/ServerEnvironment.ts";
5252import {
5353 authBearerBootstrapRouteLayer,
5454 authBootstrapRouteLayer,
......@@ -60,27 +60,27 @@ import {
6060 authPairingCredentialRouteLayer,
6161 authSessionRouteLayer,
6262 authWebSocketTokenRouteLayer,
63} from "./auth/http";
64import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore";
65import { ServerAuthLive } from "./auth/Layers/ServerAuth";
66import { OrchestrationLayerLive } from "./orchestration/runtimeLayer";
63} from "./auth/http.ts";
64import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
65import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";
66import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
6767import {
6868 clearPersistedServerRuntimeState,
6969 makePersistedServerRuntimeState,
7070 persistServerRuntimeState,
71} from "./serverRuntimeState";
71} from "./serverRuntimeState.ts";
7272import {
7373 orchestrationDispatchRouteLayer,
7474 orchestrationSnapshotRouteLayer,
75} from "./orchestration/http";
75} from "./orchestration/http.ts";
7676
7777const PtyAdapterLive = Layer.unwrap(
7878 Effect.gen(function* () {
7979 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"));
8181 return BunPTY.layer;
8282 } else {
83 const NodePTY = yield* Effect.promise(() => import("./terminal/Layers/NodePTY"));
83 const NodePTY = yield* Effect.promise(() => import("./terminal/Layers/NodePTY.ts"));
8484 return NodePTY.layer;
8585 }
8686 }),
apps/server/src/serverLogger.ts+1-1
......@@ -1,6 +1,6 @@
11import { Effect, Logger, References, Layer } from "effect";
22
3import { ServerConfig } from "./config";
3import { ServerConfig } from "./config.ts";
44
55export const ServerLoggerLive = Effect.gen(function* () {
66 const config = yield* ServerConfig;
apps/server/src/serverRuntimeStartup.ts+13-13
......@@ -21,24 +21,24 @@ import {
2121 Console,
2222} from "effect";
2323
24import { ServerConfig } from "./config";
25import { Keybindings } from "./keybindings";
26import { Open } from "./open";
27import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine";
28import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
29import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor";
30import { ServerLifecycleEvents } from "./serverLifecycleEvents";
31import { ServerSettingsService } from "./serverSettings";
32import { ServerEnvironment } from "./environment/Services/ServerEnvironment";
33import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
34import { ServerAuth } from "./auth/Services/ServerAuth";
35import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper";
24import { ServerConfig } from "./config.ts";
25import { Keybindings } from "./keybindings.ts";
26import { Open } from "./open.ts";
27import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
28import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
29import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts";
30import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";
31import { ServerSettingsService } from "./serverSettings.ts";
32import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
33import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
34import { ServerAuth } from "./auth/Services/ServerAuth.ts";
35import { ProviderSessionReaper } from "./provider/Services/ProviderSessionReaper.ts";
3636import {
3737 formatHeadlessServeOutput,
3838 formatHostForUrl,
3939 isWildcardHost,
4040 issueHeadlessServeAccessInfo,
41} from "./startupAccess";
41} from "./startupAccess.ts";
4242
4343export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{
4444 readonly message: string;
apps/server/src/serverRuntimeState.ts+2-2
......@@ -1,7 +1,7 @@
11import { Effect, FileSystem, Option, Path, Schema } from "effect";
22
3import { type ServerConfigShape } from "./config";
4import { formatHostForUrl, isWildcardHost } from "./startupAccess";
3import { type ServerConfigShape } from "./config.ts";
4import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
55
66export const PersistedServerRuntimeState = Schema.Struct({
77 version: Schema.Literal(1),
apps/server/src/serverSettings.test.ts+2-2
......@@ -2,8 +2,8 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
22import { DEFAULT_SERVER_SETTINGS, ServerSettingsPatch } from "@t3tools/contracts";
33import { assert, it } from "@effect/vitest";
44import { Effect, FileSystem, Layer, Schema } from "effect";
5import { ServerConfig } from "./config";
6import { ServerSettingsLive, ServerSettingsService } from "./serverSettings";
5import { ServerConfig } from "./config.ts";
6import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
77
88const makeServerSettingsLayer = () =>
99 ServerSettingsLive.pipe(
apps/server/src/serverSettings.ts+1-1
......@@ -39,7 +39,7 @@ import {
3939 Cause,
4040} from "effect";
4141import * as Semaphore from "effect/Semaphore";
42import { ServerConfig } from "./config";
42import { ServerConfig } from "./config.ts";
4343import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
4444import { fromLenientJson } from "@t3tools/shared/schemaJson";
4545import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
apps/server/src/startupAccess.test.ts+1-1
......@@ -7,7 +7,7 @@ import {
77 resolveHeadlessConnectionHost,
88 resolveHeadlessConnectionString,
99 resolveListeningPort,
10} from "./startupAccess";
10} from "./startupAccess.ts";
1111
1212it("prefers localhost when no explicit host is configured", () => {
1313 expect(resolveHeadlessConnectionHost(undefined)).toBe("localhost");
apps/server/src/startupAccess.ts+2-2
......@@ -4,8 +4,8 @@ import { QrCode } from "@t3tools/shared/qrCode";
44import { Effect } from "effect";
55import { HttpServer } from "effect/unstable/http";
66
7import { ServerConfig } from "./config";
8import { ServerAuth } from "./auth/Services/ServerAuth";
7import { ServerConfig } from "./config.ts";
8import { ServerAuth } from "./auth/Services/ServerAuth.ts";
99
1010export interface HeadlessServeAccessInfo {
1111 readonly connectionString: string;
apps/server/src/telemetry/Identify.ts+1-1
......@@ -1,7 +1,7 @@
11import { Effect, FileSystem, Path, Random, Schema } from "effect";
22import * as Crypto from "node:crypto";
33import { homedir } from "node:os";
4import { ServerConfig } from "../config";
4import { ServerConfig } from "../config.ts";
55
66const CodexAuthJsonSchema = Schema.Struct({
77 tokens: Schema.Struct({
apps/server/src/telemetry/Layers/AnalyticsService.ts+2-2
......@@ -13,7 +13,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab
1313import { ServerConfig } from "../../config.ts";
1414import { AnalyticsService, type AnalyticsServiceShape } from "../Services/AnalyticsService.ts";
1515import { getTelemetryIdentifier } from "../Identify.ts";
16import { version } from "../../../package.json" with { type: "json" };
16import packageJson from "../../../package.json" with { type: "json" };
1717
1818interface BufferedAnalyticsEvent {
1919 readonly event: string;
......@@ -86,7 +86,7 @@ const makeAnalyticsService = Effect.gen(function* () {
8686 platform: process.platform,
8787 wsl: process.env.WSL_DISTRO_NAME,
8888 arch: process.arch,
89 t3CodeVersion: version,
89 t3CodeVersion: packageJson.version,
9090 clientType,
9191 },
9292 timestamp: event.capturedAt,
apps/server/src/terminal/Layers/BunPTY.ts+5-2
......@@ -1,13 +1,16 @@
11import { 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
45class BunPtyProcess implements PtyProcess {
56 private readonly dataListeners = new Set<(data: string) => void>();
67 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();
78 private readonly decoder = new TextDecoder();
9 private readonly process: Bun.Subprocess;
810 private didExit = false;
911
10 constructor(private readonly process: Bun.Subprocess) {
12 constructor(process: Bun.Subprocess) {
13 this.process = process;
1114 void this.process.exited
1215 .then((exitCode) => {
1316 this.emitExit({
apps/server/src/terminal/Layers/Manager.test.ts+11-5
......@@ -24,25 +24,28 @@ import {
2424import { TestClock } from "effect/testing";
2525import { expect } from "vitest";
2626
27import type { TerminalManagerShape } from "../Services/Manager";
27import type { TerminalManagerShape } from "../Services/Manager.ts";
2828import {
2929 type PtyAdapterShape,
3030 type PtyExitEvent,
3131 type PtyProcess,
3232 type PtySpawnInput,
3333 PtySpawnError,
34} from "../Services/PTY";
35import { makeTerminalManagerWithOptions } from "./Manager";
34} from "../Services/PTY.ts";
35import { makeTerminalManagerWithOptions } from "./Manager.ts";
3636
3737class FakePtyProcess implements PtyProcess {
3838 readonly writes: string[] = [];
3939 readonly resizeCalls: Array<{ cols: number; rows: number }> = [];
4040 readonly killSignals: Array<string | undefined> = [];
41 readonly pid: number;
4142 private readonly dataListeners = new Set<(data: string) => void>();
4243 private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();
4344 killed = false;
4445
45 constructor(readonly pid: number) {}
46 constructor(pid: number) {
47 this.pid = pid;
48 }
4649
4750 write(data: string): void {
4851 this.writes.push(data);
......@@ -88,9 +91,12 @@ class FakePtyAdapter implements PtyAdapterShape {
8891 readonly spawnInputs: PtySpawnInput[] = [];
8992 readonly processes: FakePtyProcess[] = [];
9093 readonly spawnFailures: Error[] = [];
94 private readonly mode: "sync" | "async";
9195 private nextPid = 9000;
9296
93 constructor(private readonly mode: "sync" | "async" = "sync") {}
97 constructor(mode: "sync" | "async" = "sync") {
98 this.mode = mode;
99 }
94100
95101 spawn(input: PtySpawnInput): Effect.Effect<PtyProcess, PtySpawnError> {
96102 this.spawnInputs.push(input);
apps/server/src/terminal/Layers/Manager.ts+5-5
......@@ -22,13 +22,13 @@ import {
2222 SynchronizedRef,
2323} from "effect";
2424
25import { ServerConfig } from "../../config";
25import { ServerConfig } from "../../config.ts";
2626import {
2727 increment,
2828 terminalRestartsTotal,
2929 terminalSessionsTotal,
30} from "../../observability/Metrics";
31import { runProcess } from "../../processRunner";
30} from "../../observability/Metrics.ts";
31import { runProcess } from "../../processRunner.ts";
3232import {
3333 TerminalCwdError,
3434 TerminalHistoryError,
......@@ -36,14 +36,14 @@ import {
3636 TerminalNotRunningError,
3737 TerminalSessionLookupError,
3838 type TerminalManagerShape,
39} from "../Services/Manager";
39} from "../Services/Manager.ts";
4040import {
4141 PtyAdapter,
4242 PtySpawnError,
4343 type PtyAdapterShape,
4444 type PtyExitEvent,
4545 type PtyProcess,
46} from "../Services/PTY";
46} from "../Services/PTY.ts";
4747
4848const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
4949const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
apps/server/src/terminal/Layers/NodePTY.test.ts+1-1
......@@ -1,7 +1,7 @@
11import { FileSystem, Path, Effect } from "effect";
22import { assert, it } from "@effect/vitest";
33
4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY";
4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY.ts";
55import * as NodeServices from "@effect/platform-node/NodeServices";
66
77it.layer(NodeServices.layer)("ensureNodePtySpawnHelperExecutable", (it) => {
apps/server/src/terminal/Layers/NodePTY.ts+7-2
......@@ -1,7 +1,8 @@
11import { createRequire } from "node:module";
22
33import { 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
67let didEnsureSpawnHelperExecutable = false;
78
......@@ -46,7 +47,11 @@ export const ensureNodePtySpawnHelperExecutable = Effect.fn(function* (explicitP
4647});
4748
4849class 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
5156 get pid(): number {
5257 return this.process.pid;
apps/server/src/terminal/Services/Manager.ts+1-1
......@@ -22,7 +22,7 @@ import {
2222 TerminalSessionStatus,
2323 TerminalWriteInput,
2424} from "@t3tools/contracts";
25import { PtyProcess } from "./PTY";
25import type { PtyProcess } from "./PTY.ts";
2626import { Effect, Context } from "effect";
2727
2828export {
apps/server/src/ws.ts+26-26
......@@ -27,42 +27,42 @@ import { clamp } from "effect/Number";
2727import { HttpRouter, HttpServerRequest } from "effect/unstable/http";
2828import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
2929
30import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery";
31import { ServerConfig } from "./config";
32import { GitCore } from "./git/Services/GitCore";
33import { GitManager } from "./git/Services/GitManager";
34import { GitStatusBroadcaster } from "./git/Services/GitStatusBroadcaster";
35import { Keybindings } from "./keybindings";
36import { Open, resolveAvailableEditors } from "./open";
37import { normalizeDispatchCommand } from "./orchestration/Normalizer";
38import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine";
39import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
30import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery.ts";
31import { ServerConfig } from "./config.ts";
32import { GitCore } from "./git/Services/GitCore.ts";
33import { GitManager } from "./git/Services/GitManager.ts";
34import { GitStatusBroadcaster } from "./git/Services/GitStatusBroadcaster.ts";
35import { Keybindings } from "./keybindings.ts";
36import { Open, resolveAvailableEditors } from "./open.ts";
37import { normalizeDispatchCommand } from "./orchestration/Normalizer.ts";
38import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
39import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
4040import {
4141 observeRpcEffect,
4242 observeRpcStream,
4343 observeRpcStreamEffect,
44} from "./observability/RpcInstrumentation";
45import { ProviderRegistry } from "./provider/Services/ProviderRegistry";
46import { ServerLifecycleEvents } from "./serverLifecycleEvents";
47import { ServerRuntimeStartup } from "./serverRuntimeStartup";
48import { ServerSettingsService } from "./serverSettings";
49import { TerminalManager } from "./terminal/Services/Manager";
50import { WorkspaceEntries } from "./workspace/Services/WorkspaceEntries";
51import { WorkspaceFileSystem } from "./workspace/Services/WorkspaceFileSystem";
52import { WorkspacePathOutsideRootError } from "./workspace/Services/WorkspacePaths";
53import { ProjectSetupScriptRunner } from "./project/Services/ProjectSetupScriptRunner";
54import { RepositoryIdentityResolver } from "./project/Services/RepositoryIdentityResolver";
55import { ServerEnvironment } from "./environment/Services/ServerEnvironment";
56import { ServerAuth } from "./auth/Services/ServerAuth";
44} from "./observability/RpcInstrumentation.ts";
45import { ProviderRegistry } from "./provider/Services/ProviderRegistry.ts";
46import { ServerLifecycleEvents } from "./serverLifecycleEvents.ts";
47import { ServerRuntimeStartup } from "./serverRuntimeStartup.ts";
48import { ServerSettingsService } from "./serverSettings.ts";
49import { TerminalManager } from "./terminal/Services/Manager.ts";
50import { WorkspaceEntries } from "./workspace/Services/WorkspaceEntries.ts";
51import { WorkspaceFileSystem } from "./workspace/Services/WorkspaceFileSystem.ts";
52import { WorkspacePathOutsideRootError } from "./workspace/Services/WorkspacePaths.ts";
53import { ProjectSetupScriptRunner } from "./project/Services/ProjectSetupScriptRunner.ts";
54import { RepositoryIdentityResolver } from "./project/Services/RepositoryIdentityResolver.ts";
55import { ServerEnvironment } from "./environment/Services/ServerEnvironment.ts";
56import { ServerAuth } from "./auth/Services/ServerAuth.ts";
5757import {
5858 BootstrapCredentialService,
5959 type BootstrapCredentialChange,
60} from "./auth/Services/BootstrapCredentialService";
60} from "./auth/Services/BootstrapCredentialService.ts";
6161import {
6262 SessionCredentialService,
6363 type SessionCredentialChange,
64} from "./auth/Services/SessionCredentialService";
65import { respondToAuthError } from "./auth/http";
64} from "./auth/Services/SessionCredentialService.ts";
65import { respondToAuthError } from "./auth/http.ts";
6666
6767function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<
6868 OrchestrationEvent,
apps/server/tsconfig.json+1-3
......@@ -3,9 +3,7 @@
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node", "bun"],
6 "lib": ["ES2023", "esnext.disposable"],
7 "noEmit": true,
8 "allowImportingTsExtensions": true,
6 "lib": ["ESNext", "esnext.disposable"],
97 "plugins": [
108 {
119 "name": "@effect/language-service",
apps/server/vitest.config.ts+1-1
......@@ -1,6 +1,6 @@
11import { defineConfig, mergeConfig } from "vitest/config";
22
3import baseConfig from "../../vitest.config";
3import baseConfig from "../../vitest.config.ts";
44
55export default mergeConfig(
66 baseConfig,
apps/web/tsconfig.json+4
......@@ -2,6 +2,10 @@
22 "extends": "../../tsconfig.base.json",
33 "compilerOptions": {
44 "composite": true,
5 "module": "Preserve",
6 "moduleResolution": "Bundler",
7 "erasableSyntaxOnly": false,
8 "verbatimModuleSyntax": false,
59 "jsx": "react-jsx",
610 "lib": ["ES2023", "DOM", "DOM.Iterable"],
711 "types": ["vite/client"],
docs/observability.md+4-4
......@@ -69,11 +69,11 @@ npx t3
6969```
7070
7171```bash
72bun dev
72node --run dev
7373```
7474
7575```bash
76bun dev:desktop
76node --run dev:desktop
7777```
7878
7979### Option 2: Run With A Local LGTM Stack
......@@ -122,13 +122,13 @@ npx t3
122122Monorepo web/server dev:
123123
124124```bash
125bun dev
125node --run dev
126126```
127127
128128Monorepo desktop dev:
129129
130130```bash
131bun dev:desktop
131node --run dev:desktop
132132```
133133
134134Packaged desktop app:
package.json+1-2
......@@ -33,14 +33,13 @@
3333 "start": "turbo run start --filter=t3",
3434 "start:desktop": "turbo run start --filter=@t3tools/desktop",
3535 "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",
3737 "build": "turbo run build",
3838 "build:marketing": "turbo run build --filter=@t3tools/marketing",
3939 "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=t3",
4040 "typecheck": "turbo run typecheck",
4141 "lint": "oxlint --report-unused-disable-directives",
4242 "test": "turbo run test",
43 "test:process-reaper": "bun run --cwd apps/server test:process-reaper",
4443 "test:desktop-smoke": "turbo run smoke-test --filter=@t3tools/desktop",
4544 "fmt": "oxfmt",
4645 "fmt:check": "oxfmt --check",
packages/client-runtime/src/index.ts+2-2
......@@ -1,2 +1,2 @@
1export * from "./knownEnvironment";
2export * from "./scoped";
1export * from "./knownEnvironment.ts";
2export * from "./scoped.ts";
packages/client-runtime/src/knownEnvironment.test.ts+2-2
......@@ -1,7 +1,7 @@
11import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
22import { describe, expect, it } from "vitest";
33
4import { createKnownEnvironment, getKnownEnvironmentHttpBaseUrl } from "./knownEnvironment";
4import { createKnownEnvironment, getKnownEnvironmentHttpBaseUrl } from "./knownEnvironment.ts";
55import {
66 parseScopedProjectKey,
77 parseScopedThreadKey,
......@@ -10,7 +10,7 @@ import {
1010 scopedThreadKey,
1111 scopeProjectRef,
1212 scopeThreadRef,
13} from "./scoped";
13} from "./scoped.ts";
1414
1515describe("known environment bootstrap helpers", () => {
1616 it("creates known environments from explicit server base urls", () => {
packages/contracts/src/auth.ts+1-1
......@@ -1,6 +1,6 @@
11import { Schema } from "effect";
22
3import { AuthSessionId, TrimmedNonEmptyString } from "./baseSchemas";
3import { AuthSessionId, TrimmedNonEmptyString } from "./baseSchemas.ts";
44
55/**
66 * Declares the server's overall authentication posture.
packages/contracts/src/editor.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
44export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]);
55export type EditorLaunchStyle = typeof EditorLaunchStyle.Type;
packages/contracts/src/environment.ts+1-1
......@@ -1,6 +1,6 @@
11import { Effect, Schema } from "effect";
22
3import { EnvironmentId, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas";
3import { EnvironmentId, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
44
55export const ExecutionEnvironmentPlatformOs = Schema.Literals([
66 "darwin",
packages/contracts/src/filesystem.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
44const FILESYSTEM_PATH_MAX_LENGTH = 512;
55
packages/contracts/src/git.test.ts+1-1
......@@ -7,7 +7,7 @@ import {
77 GitRunStackedActionResult,
88 GitRunStackedActionInput,
99 GitResolvePullRequestResult,
10} from "./git";
10} from "./git.ts";
1111
1212const decodeCreateWorktreeInput = Schema.decodeUnknownSync(GitCreateWorktreeInput);
1313const decodePreparePullRequestThreadInput = Schema.decodeUnknownSync(
packages/contracts/src/git.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { NonNegativeInt, PositiveInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas";
2import { NonNegativeInt, PositiveInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
33
44const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
55const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
packages/contracts/src/index.ts+17-17
......@@ -1,17 +1,17 @@
1export * from "./baseSchemas";
2export * from "./auth";
3export * from "./environment";
4export * from "./ipc";
5export * from "./terminal";
6export * from "./provider";
7export * from "./providerRuntime";
8export * from "./model";
9export * from "./keybindings";
10export * from "./server";
11export * from "./settings";
12export * from "./git";
13export * from "./orchestration";
14export * from "./editor";
15export * from "./project";
16export * from "./filesystem";
17export * from "./rpc";
1export * from "./baseSchemas.ts";
2export * from "./auth.ts";
3export * from "./environment.ts";
4export * from "./ipc.ts";
5export * from "./terminal.ts";
6export * from "./provider.ts";
7export * from "./providerRuntime.ts";
8export * from "./model.ts";
9export * from "./keybindings.ts";
10export * from "./server.ts";
11export * from "./settings.ts";
12export * from "./git.ts";
13export * from "./orchestration.ts";
14export * from "./editor.ts";
15export * from "./project.ts";
16export * from "./filesystem.ts";
17export * from "./rpc.ts";
packages/contracts/src/ipc.ts+10-10
......@@ -17,19 +17,19 @@ import type {
1717 GitStatusInput,
1818 GitStatusResult,
1919 GitCreateBranchResult,
20} from "./git";
21import type { FilesystemBrowseInput, FilesystemBrowseResult } from "./filesystem";
20} from "./git.ts";
21import type { FilesystemBrowseInput, FilesystemBrowseResult } from "./filesystem.ts";
2222import type {
2323 ProjectSearchEntriesInput,
2424 ProjectSearchEntriesResult,
2525 ProjectWriteFileInput,
2626 ProjectWriteFileResult,
27} from "./project";
27} from "./project.ts";
2828import type {
2929 ServerConfig,
3030 ServerProviderUpdatedPayload,
3131 ServerUpsertKeybindingResult,
32} from "./server";
32} from "./server.ts";
3333import type {
3434 TerminalClearInput,
3535 TerminalCloseInput,
......@@ -39,8 +39,8 @@ import type {
3939 TerminalRestartInput,
4040 TerminalSessionSnapshot,
4141 TerminalWriteInput,
42} from "./terminal";
43import type { ServerUpsertKeybindingInput } from "./server";
42} from "./terminal.ts";
43import type { ServerUpsertKeybindingInput } from "./server.ts";
4444import type {
4545 ClientOrchestrationCommand,
4646 OrchestrationGetFullThreadDiffInput,
......@@ -50,10 +50,10 @@ import type {
5050 OrchestrationShellStreamItem,
5151 OrchestrationSubscribeThreadInput,
5252 OrchestrationThreadStreamItem,
53} from "./orchestration";
54import type { EnvironmentId } from "./baseSchemas";
55import { EditorId } from "./editor";
56import { ClientSettings, ServerSettings, ServerSettingsPatch } from "./settings";
53} from "./orchestration.ts";
54import type { EnvironmentId } from "./baseSchemas.ts";
55import { EditorId } from "./editor.ts";
56import { ServerSettings, type ClientSettings, type ServerSettingsPatch } from "./settings.ts";
5757
5858export interface ContextMenuItem<T extends string = string> {
5959 id: T;
packages/contracts/src/keybindings.test.ts+1-1
......@@ -7,7 +7,7 @@ import {
77 KeybindingRule,
88 ResolvedKeybindingRule,
99 ResolvedKeybindingsConfig,
10} from "./keybindings";
10} from "./keybindings.ts";
1111
1212const decode = <S extends Schema.Top>(
1313 schema: S,
packages/contracts/src/keybindings.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { TrimmedString } from "./baseSchemas";
2import { TrimmedString } from "./baseSchemas.ts";
33
44export const MAX_KEYBINDING_VALUE_LENGTH = 64;
55const MAX_KEYBINDING_WHEN_LENGTH = 256;
packages/contracts/src/model.ts+2-2
......@@ -1,6 +1,6 @@
11import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";
3import type { ProviderKind } from "./orchestration";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
3import type { ProviderKind } from "./orchestration.ts";
44
55export const CodexReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low"]);
66export type CodexReasoningEffort = typeof CodexReasoningEffort.Type;
packages/contracts/src/orchestration.test.ts+1-1
......@@ -19,7 +19,7 @@ import {
1919 ThreadCreatedPayload,
2020 ThreadTurnDiff,
2121 ThreadTurnStartRequestedPayload,
22} from "./orchestration";
22} from "./orchestration.ts";
2323
2424const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput);
2525const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff);
packages/contracts/src/orchestration.ts+3-3
......@@ -1,6 +1,6 @@
11import { Effect, Option, Schema, SchemaIssue, Struct } from "effect";
2import { ClaudeModelOptions, CodexModelOptions } from "./model";
3import { RepositoryIdentity } from "./environment";
2import { ClaudeModelOptions, CodexModelOptions } from "./model.ts";
3import { RepositoryIdentity } from "./environment.ts";
44import {
55 ApprovalRequestId,
66 CheckpointRef,
......@@ -14,7 +14,7 @@ import {
1414 ThreadId,
1515 TrimmedNonEmptyString,
1616 TurnId,
17} from "./baseSchemas";
17} from "./baseSchemas.ts";
1818
1919export const ORCHESTRATION_WS_METHODS = {
2020 dispatchCommand: "orchestration.dispatchCommand",
packages/contracts/src/project.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";
2import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
33
44const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
55const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
packages/contracts/src/provider.test.ts+1-1
......@@ -1,7 +1,7 @@
11import { describe, expect, it } from "vitest";
22import { Schema } from "effect";
33
4import { ProviderSendTurnInput, ProviderSessionStartInput } from "./provider";
4import { ProviderSendTurnInput, ProviderSessionStartInput } from "./provider.ts";
55
66const decodeProviderSessionStartInput = Schema.decodeUnknownSync(ProviderSessionStartInput);
77const decodeProviderSendTurnInput = Schema.decodeUnknownSync(ProviderSendTurnInput);
packages/contracts/src/provider.ts+3-3
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33import {
44 ApprovalRequestId,
55 EventId,
......@@ -7,7 +7,7 @@ import {
77 ProviderItemId,
88 ThreadId,
99 TurnId,
10} from "./baseSchemas";
10} from "./baseSchemas.ts";
1111import {
1212 ChatAttachment,
1313 ModelSelection,
......@@ -21,7 +21,7 @@ import {
2121 ProviderSandboxMode,
2222 ProviderUserInputAnswers,
2323 RuntimeMode,
24} from "./orchestration";
24} from "./orchestration.ts";
2525
2626const ProviderSessionStatus = Schema.Literals([
2727 "connecting",
packages/contracts/src/providerRuntime.test.ts+1-1
......@@ -1,7 +1,7 @@
11import { describe, expect, it } from "vitest";
22import { Schema } from "effect";
33
4import { ProviderRuntimeEvent } from "./providerRuntime";
4import { ProviderRuntimeEvent } from "./providerRuntime.ts";
55
66const decodeRuntimeEvent = Schema.decodeUnknownSync(ProviderRuntimeEvent);
77
packages/contracts/src/providerRuntime.ts+2-2
......@@ -11,8 +11,8 @@ import {
1111 ThreadId,
1212 TrimmedNonEmptyString,
1313 TurnId,
14} from "./baseSchemas";
15import { ProviderKind } from "./orchestration";
14} from "./baseSchemas.ts";
15import { ProviderKind } from "./orchestration.ts";
1616
1717const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
1818const UnknownRecordSchema = Schema.Record(Schema.String, Schema.Unknown);
packages/contracts/src/rpc.ts+14-10
......@@ -2,9 +2,13 @@ import { Schema } from "effect";
22import * as Rpc from "effect/unstable/rpc/Rpc";
33import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
44
5import { OpenError, OpenInEditorInput } from "./editor";
6import { AuthAccessStreamEvent } from "./auth";
7import { FilesystemBrowseInput, FilesystemBrowseResult, FilesystemBrowseError } from "./filesystem";
5import { OpenError, OpenInEditorInput } from "./editor.ts";
6import { AuthAccessStreamEvent } from "./auth.ts";
7import {
8 FilesystemBrowseInput,
9 FilesystemBrowseResult,
10 FilesystemBrowseError,
11} from "./filesystem.ts";
812import {
913 GitActionProgressEvent,
1014 GitCheckoutInput,
......@@ -29,8 +33,8 @@ import {
2933 GitStatusInput,
3034 GitStatusResult,
3135 GitStatusStreamEvent,
32} from "./git";
33import { KeybindingsConfigError } from "./keybindings";
36} from "./git.ts";
37import { KeybindingsConfigError } from "./keybindings.ts";
3438import {
3539 ClientOrchestrationCommand,
3640 ORCHESTRATION_WS_METHODS,
......@@ -43,7 +47,7 @@ import {
4347 OrchestrationReplayEventsError,
4448 OrchestrationReplayEventsInput,
4549 OrchestrationRpcSchemas,
46} from "./orchestration";
50} from "./orchestration.ts";
4751import {
4852 ProjectSearchEntriesError,
4953 ProjectSearchEntriesInput,
......@@ -51,7 +55,7 @@ import {
5155 ProjectWriteFileError,
5256 ProjectWriteFileInput,
5357 ProjectWriteFileResult,
54} from "./project";
58} from "./project.ts";
5559import {
5660 TerminalClearInput,
5761 TerminalCloseInput,
......@@ -62,7 +66,7 @@ import {
6266 TerminalRestartInput,
6367 TerminalSessionSnapshot,
6468 TerminalWriteInput,
65} from "./terminal";
69} from "./terminal.ts";
6670import {
6771 ServerConfigStreamEvent,
6872 ServerConfig,
......@@ -70,8 +74,8 @@ import {
7074 ServerProviderUpdatedPayload,
7175 ServerUpsertKeybindingInput,
7276 ServerUpsertKeybindingResult,
73} from "./server";
74import { ServerSettings, ServerSettingsError, ServerSettingsPatch } from "./settings";
77} from "./server.ts";
78import { ServerSettings, ServerSettingsError, ServerSettingsPatch } from "./settings.ts";
7579
7680export const WS_METHODS = {
7781 // Project registry methods
packages/contracts/src/server.test.ts+1-1
......@@ -1,7 +1,7 @@
11import { Schema } from "effect";
22import { describe, expect, it } from "vitest";
33
4import { ServerProvider } from "./server";
4import { ServerProvider } from "./server.ts";
55
66const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider);
77
packages/contracts/src/server.ts+8-8
......@@ -1,18 +1,18 @@
11import { Effect, Schema } from "effect";
2import { ExecutionEnvironmentDescriptor } from "./environment";
3import { ServerAuthDescriptor } from "./auth";
2import { ExecutionEnvironmentDescriptor } from "./environment.ts";
3import { ServerAuthDescriptor } from "./auth.ts";
44import {
55 IsoDateTime,
66 NonNegativeInt,
77 ProjectId,
88 ThreadId,
99 TrimmedNonEmptyString,
10} from "./baseSchemas";
11import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings";
12import { EditorId } from "./editor";
13import { ModelCapabilities } from "./model";
14import { ProviderKind } from "./orchestration";
15import { ServerSettings } from "./settings";
10} from "./baseSchemas.ts";
11import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings.ts";
12import { EditorId } from "./editor.ts";
13import { ModelCapabilities } from "./model.ts";
14import { ProviderKind } from "./orchestration.ts";
15import { ServerSettings } from "./settings.ts";
1616
1717const KeybindingsMalformedConfigIssue = Schema.Struct({
1818 kind: Schema.Literal("keybindings.malformed-config"),
packages/contracts/src/settings.ts+3-3
......@@ -1,13 +1,13 @@
11import { Effect } from "effect";
22import * as Schema from "effect/Schema";
33import * as SchemaTransformation from "effect/SchemaTransformation";
4import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas";
4import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
55import {
66 ClaudeModelOptions,
77 CodexModelOptions,
88 DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
9} from "./model";
10import { ModelSelection } from "./orchestration";
9} from "./model.ts";
10import { ModelSelection } from "./orchestration.ts";
1111
1212// ── Client Settings (local-only) ───────────────────────────────
1313
packages/contracts/src/terminal.test.ts+1-1
......@@ -11,7 +11,7 @@ import {
1111 TerminalSessionSnapshot,
1212 TerminalThreadInput,
1313 TerminalWriteInput,
14} from "./terminal";
14} from "./terminal.ts";
1515
1616function decodeSync<S extends Schema.Top>(schema: S, input: unknown): Schema.Schema.Type<S> {
1717 return Schema.decodeUnknownSync(schema as never)(input) as Schema.Schema.Type<S>;
packages/contracts/src/terminal.ts+1-1
......@@ -1,5 +1,5 @@
11import { Effect, Schema } from "effect";
2import { TrimmedNonEmptyString } from "./baseSchemas";
2import { TrimmedNonEmptyString } from "./baseSchemas.ts";
33
44export const DEFAULT_TERMINAL_ID = "default";
55
packages/shared/src/DrainableWorker.test.ts+1-1
......@@ -2,7 +2,7 @@ import { it } from "@effect/vitest";
22import { describe, expect } from "vitest";
33import { Deferred, Effect } from "effect";
44
5import { makeDrainableWorker } from "./DrainableWorker";
5import { makeDrainableWorker } from "./DrainableWorker.ts";
66
77describe("makeDrainableWorker", () => {
88 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";
22import { describe, expect } from "vitest";
33import { Deferred, Effect } from "effect";
44
5import { makeKeyedCoalescingWorker } from "./KeyedCoalescingWorker";
5import { makeKeyedCoalescingWorker } from "./KeyedCoalescingWorker.ts";
66
77describe("makeKeyedCoalescingWorker", () => {
88 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";
33import { assert, describe, it } from "@effect/vitest";
44import { Effect } from "effect";
55
6import { NetError, NetService } from "./Net";
6import { NetError, NetService } from "./Net.ts";
77
88const closeServer = (server: Net.Server) =>
99 Effect.sync(() => {
packages/shared/src/String.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { truncate } from "./String";
3import { truncate } from "./String.ts";
44
55describe("truncate", () => {
66 it("trims surrounding whitespace", () => {
packages/shared/src/cliArgs.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vitest";
22
3import { parseCliArgs } from "./cliArgs";
3import { parseCliArgs } from "./cliArgs.ts";
44
55describe("parseCliArgs", () => {
66 it("returns empty result for empty string", () => {
packages/shared/src/git.test.ts+1-1
......@@ -8,7 +8,7 @@ import {
88 normalizeGitRemoteUrl,
99 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,
1010 WORKTREE_BRANCH_PREFIX,
11} from "./git";
11} from "./git.ts";
1212
1313describe("normalizeGitRemoteUrl", () => {
1414 it("canonicalizes equivalent GitHub remotes across protocol variants", () => {
packages/shared/src/model.test.ts+1-1
......@@ -18,7 +18,7 @@ import {
1818 resolveModelSlugForProvider,
1919 resolveSelectableModel,
2020 trimOrNull,
21} from "./model";
21} from "./model.ts";
2222
2323const codexCaps: ModelCapabilities = {
2424 reasoningEffortLevels: [
packages/shared/src/path.test.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 isUncPath,
55 isWindowsAbsolutePath,
66 isWindowsDrivePath,
7} from "./path";
7} from "./path.ts";
88
99describe("path helpers", () => {
1010 it("detects windows drive paths", () => {
packages/shared/src/qrCode.ts+856-855
......@@ -1,4 +1,3 @@
1// @ts-nocheck
21/* oxlint-disable eslint/no-useless-escape */
32/*
43 * QR Code generator library (TypeScript)
......@@ -25,960 +24,962 @@
2524
2625"use strict";
2726
28namespace qrcodegen {
29 type bit = number;
30 type byte = 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 }
27type bit = number;
28type byte = number;
29type int = number;
6330
64 // Returns a QR Code representing the given binary data at the given error correction level.
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 }
31/*---- QR Code symbol class ----*/
7232
73 /*-- Static factory functions (mid level) --*/
74
75 // Returns a QR Code representing the given segments with the given encoding parameters.
76 // The smallest possible QR Code version within the given range is automatically
77 // chosen for the output. Iff boostEcl is true, then the ECC level of the result
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: QrCode.Ecc,
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
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 }
33/*
34 * A QR Code symbol, which is a type of two-dimension barcode.
35 * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
36 * Instances of this class represent an immutable square grid of dark and light cells.
37 * The class provides static factory functions to create a QR Code from text or binary data.
38 * The class covers the QR Code Model 2 specification, supporting all versions (sizes)
39 * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
40 *
41 * Ways to create a QR Code object:
42 * - High level: Take the payload data and call QrCode.encodeText() or QrCode.encodeBinary().
43 * - Mid level: Custom-make the list of segments and call QrCode.encodeSegments().
44 * - Low level: Custom-make the array of data codeword bytes (including
45 * segment headers and final padding, excluding error correction codewords),
46 * supply the appropriate version number, and call the QrCode() constructor.
47 * (Note that all ways require supplying the desired error correction level.)
48 */
49export class QrCode {
50 public static Ecc: typeof QrCodeEcc;
51
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: QrCodeEcc): QrCode {
60 const segs: Array<QrSegment> = QrSegment.makeSegments(text);
61 return QrCode.encodeSegments(segs, ecl);
62 }
11763
118 // Increase the error correction level while the data still fits in the current version number
119 for (const newEcl of [QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH]) {
120 // From low to high
121 if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8)
122 ecl = newEcl;
123 }
64 // Returns a QR Code representing the given binary data at the given error correction level.
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: QrCodeEcc): QrCode {
69 const seg: QrSegment = QrSegment.makeBytes(data);
70 return QrCode.encodeSegments([seg], ecl);
71 }
12472
125 // Concatenate all segments to create the data bit string
126 let bb: Array<bit> = [];
127 for (const seg of segs) {
128 appendBits(seg.mode.modeBits, 4, bb);
129 appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
130 for (const b of seg.getData()) bb.push(b);
73 /*-- Static factory functions (mid level) --*/
74
75 // Returns a QR Code representing the given segments with the given encoding parameters.
76 // The smallest possible QR Code version within the given range is automatically
77 // chosen for the output. Iff boostEcl is true, then the ECC level of the result
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
131112 }
132 assert(bb.length == dataUsedBits);
133
134 // Add terminator and pad up to a byte if applicable
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);
113 if (version >= maxVersion)
114 // All versions in the range could not fit the given data
115 throw new RangeError("Data too long");
152116 }
153117
154 /*-- Fields --*/
155
156 // The width and height of this QR Code, measured in modules, between
157 // 21 and 177 (inclusive). This is equal to version * 4 + 17.
158 public readonly size: int;
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
118 // Increase the error correction level while the data still fits in the current version number
119 for (const newEcl of [QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH]) {
120 // From low to high
121 if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8) ecl = newEcl;
122 }
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);
230130 }
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 false
235 // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
236 // If the given coordinates are out of bounds, then false (light) is returned.
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 }
140 // Pad with alternating bytes until data capacity is reached
141 for (let padByte = 0xec; bb.length < dataCapacityBits; padByte ^= 0xec ^ 0x11)
142 appendBits(padByte, 8, bb);
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.
244 private drawFunctionPatterns(): void {
245 // Draw horizontal and vertical timing patterns
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 }
149 // Create the QR Code object
150 return new QrCode(version, ecl, dataCodewords, mask);
151 }
250152
251 // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
252 this.drawFinderPattern(3, 3);
253 this.drawFinderPattern(this.size - 4, 3);
254 this.drawFinderPattern(3, this.size - 4);
255
256 // Draw numerous alignment patterns
257 const alignPatPos: Array<int> = this.getAlignmentPatternPositions();
258 const numAlign: int = alignPatPos.length;
259 for (let i = 0; i < numAlign; i++) {
260 for (let j = 0; j < numAlign; j++) {
261 // Don't draw on the three finder corners
262 if (
263 !((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))
264 )
265 this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
153 /*-- Fields --*/
154
155 // The width and height of this QR Code, measured in modules, between
156 // 21 and 177 (inclusive). This is equal to version * 4 + 17.
157 public readonly version: int;
158 public readonly errorCorrectionLevel: QrCodeEcc;
159 public readonly size: int;
160
161 // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
162 // Even if a QR Code is created with automatic masking requested (mask = -1),
163 // the resulting object still has a mask value between 0 and 7.
164 public readonly mask: int;
165
166 // The modules of this QR Code (false = light, true = dark).
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;
266224 }
225 this.applyMask(i); // Undoes the mask due to XOR
267226 }
268
269 // Draw configuration data
270 this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
271 this.drawVersion();
272227 }
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)
275 // based on the given mask and this object's error correction level field.
276 private drawFormatBits(mask: int): void {
277 // Calculate error correction code and pack bits
278 const data: int = (this.errorCorrectionLevel.formatBits << 3) | mask; // errCorrLvl is uint2, mask is uint3
279 let rem: int = data;
280 for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
281 const bits = ((data << 10) | rem) ^ 0x5412; // uint15
282 assert(bits >>> 15 == 0);
283
284 // Draw first copy
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 }
233 this.isFunction = [];
234 }
235
236 /*-- Accessor methods --*/
237
238 // Returns the color of the module (pixel) at the given coordinates, which is false
239 // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
240 // If the given coordinates are out of bounds, then false (light) is returned.
241 public getModule(x: int, y: int): boolean {
242 return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y]![x]!;
243 }
296244
297 // Draws two copies of the version bits (with its own error correction code),
298 // based on this object's version field, iff 7 <= version <= 40.
299 private drawVersion(): void {
300 if (this.version < 7) return;
301
302 // Calculate error correction code and pack bits
303 let rem: int = this.version; // version is uint6, in the range [7, 40]
304 for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
305 const bits: int = (this.version << 12) | rem; // uint18
306 assert(bits >>> 18 == 0);
307
308 // Draw two copies
309 for (let i = 0; i < 18; i++) {
310 const color: boolean = getBit(bits, i);
311 const a: int = this.size - 11 + (i % 3);
312 const b: int = Math.floor(i / 3);
313 this.setFunctionModule(a, b, color);
314 this.setFunctionModule(b, a, color);
245 /*-- Private helper methods for constructor: Drawing function modules --*/
246
247 // Reads this object's version field, and draws and marks all function modules.
248 private drawFunctionPatterns(): void {
249 // Draw horizontal and vertical timing patterns
250 for (let i = 0; i < this.size; i++) {
251 this.setFunctionModule(6, i, i % 2 == 0);
252 this.setFunctionModule(i, 6, i % 2 == 0);
253 }
254
255 // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
256 this.drawFinderPattern(3, 3);
257 this.drawFinderPattern(this.size - 4, 3);
258 this.drawFinderPattern(3, this.size - 4);
259
260 // Draw numerous alignment patterns
261 const alignPatPos: Array<int> = this.getAlignmentPatternPositions();
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]!);
315268 }
316269 }
317270
318 // Draws a 9*9 finder pattern including the border separator,
319 // with the center module at (x, y). Modules can be out of bounds.
320 private drawFinderPattern(x: int, y: int): void {
321 for (let dy = -4; dy <= 4; dy++) {
322 for (let dx = -4; dx <= 4; dx++) {
323 const dist: int = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm
324 const xx: int = x + dx;
325 const yy: int = y + dy;
326 if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size)
327 this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
328 }
329 }
271 // Draw configuration data
272 this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
273 this.drawVersion();
274 }
275
276 // Draws two copies of the format bits (with its own error correction code)
277 // based on the given mask and this object's error correction level field.
278 private drawFormatBits(mask: int): void {
279 // Calculate error correction code and pack bits
280 const data: int = (this.errorCorrectionLevel.formatBits << 3) | mask; // errCorrLvl is uint2, mask is uint3
281 let rem: int = data;
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);
330317 }
318 }
331319
332 // Draws a 5*5 alignment pattern, with the center module
333 // at (x, y). All modules must be in bounds.
334 private drawAlignmentPattern(x: int, y: int): void {
335 for (let dy = -2; dy <= 2; dy++) {
336 for (let dx = -2; dx <= 2; dx++)
337 this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
320 // Draws a 9*9 finder pattern including the border separator,
321 // with the center module at (x, y). Modules can be out of bounds.
322 private drawFinderPattern(x: int, y: int): void {
323 for (let dy = -4; dy <= 4; dy++) {
324 for (let dx = -4; dx <= 4; dx++) {
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);
338330 }
339331 }
332 }
340333
341 // Sets the color of a module and marks it as a function module.
342 // Only used by the constructor. Coordinates must be in bounds.
343 private setFunctionModule(x: int, y: int, isDark: boolean): void {
344 this.modules[y][x] = isDark;
345 this.isFunction[y][x] = true;
334 // Draws a 5*5 alignment pattern, with the center module
335 // at (x, y). All modules must be in bounds.
336 private drawAlignmentPattern(x: int, y: int): void {
337 for (let dy = -2; dy <= 2; dy++) {
338 for (let dx = -2; dx <= 2; dx++)
339 this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
346340 }
341 }
347342
348 /*-- Private helper methods for constructor: Codewords and masking --*/
349
350 // Returns a new byte string representing the given data with the appropriate error correction
351 // codewords appended to it, based on this object's version and error correction level.
352 private addEccAndInterleave(data: Readonly<Array<byte>>): Array<byte> {
353 const ver: int = this.version;
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 }
343 // Sets the color of a module and marks it as a function module.
344 // Only used by the constructor. Coordinates must be in bounds.
345 private setFunctionModule(x: int, y: int, isDark: boolean): void {
346 this.modules[y]![x] = isDark;
347 this.isFunction[y]![x] = true;
348 }
378349
379 // Interleave (not concatenate) the bytes from every block into a single sequence
380 let result: Array<byte> = [];
381 for (let i = 0; i < blocks[0].length; i++) {
382 blocks.forEach((block, j) => {
383 // Skip the padding byte in short blocks
384 if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]);
385 });
386 }
387 assert(result.length == rawCodewords);
388 return result;
389 }
350 /*-- Private helper methods for constructor: Codewords and masking --*/
351
352 // Returns a new byte string representing the given data with the appropriate error correction
353 // codewords appended to it, based on this object's version and error correction level.
354 private addEccAndInterleave(data: Readonly<Array<byte>>): Array<byte> {
355 const ver: int = this.version;
356 const ecl: QrCodeEcc = this.errorCorrectionLevel;
357 if (data.length != QrCode.getNumDataCodewords(ver, ecl))
358 throw new RangeError("Invalid argument");
359
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 entire
392 // 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 {
394 if (data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8))
395 throw new RangeError("Invalid argument");
396 let i: int = 0; // Bit index into the data
397 // Do the funny zigzag scan
398 for (let right = this.size - 1; right >= 1; right -= 2) {
399 // Index of right column in each column pair
400 if (right == 6) right = 5;
401 for (let vert = 0; vert < this.size; vert++) {
402 // Vertical counter
403 for (let j = 0; j < 2; j++) {
404 const x: int = right - j; // Actual x coordinate
405 const upward: boolean = ((right + 1) & 2) == 0;
406 const y: int = upward ? this.size - 1 - vert : vert; // Actual y coordinate
407 if (!this.isFunction[y][x] && i < data.length * 8) {
408 this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
409 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
393 // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
394 // data area of this QR Code. Function modules need to be marked off before this is called.
395 private drawCodewords(data: Readonly<Array<byte>>): void {
396 if (data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8))
397 throw new RangeError("Invalid argument");
398 let i: int = 0; // Bit index into the data
399 // Do the funny zigzag scan
400 for (let right = this.size - 1; right >= 1; right -= 2) {
401 // Index of right column in each column pair
402 if (right == 6) right = 5;
403 for (let vert = 0; vert < this.size; vert++) {
404 // Vertical counter
405 for (let j = 0; j < 2; j++) {
406 const x: int = right - j; // Actual x coordinate
407 const upward: boolean = ((right + 1) & 2) == 0;
408 const y: int = upward ? this.size - 1 - vert : vert; // Actual y coordinate
409 if (!this.isFunction[y]![x]! && i < data.length * 8) {
410 this.modules[y]![x] = getBit(data[i >>> 3]!, 7 - (i & 7));
411 i++;
413412 }
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
414415 }
415416 }
416 assert(i == data.length * 8);
417417 }
418 assert(i == data.length * 8);
419 }
418420
419 // 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 drawn
421 // 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-formed
423 // QR Code needs exactly one (not zero, two, etc.) mask applied.
424 private applyMask(mask: int): void {
425 if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");
426 for (let y = 0; y < this.size; y++) {
427 for (let x = 0; x < this.size; x++) {
428 let invert: boolean;
429 switch (mask) {
430 case 0:
431 invert = (x + y) % 2 == 0;
432 break;
433 case 1:
434 invert = y % 2 == 0;
435 break;
436 case 2:
437 invert = x % 3 == 0;
438 break;
439 case 3:
440 invert = (x + y) % 3 == 0;
441 break;
442 case 4:
443 invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
444 break;
445 case 5:
446 invert = ((x * y) % 2) + ((x * y) % 3) == 0;
447 break;
448 case 6:
449 invert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
450 break;
451 case 7:
452 invert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
453 break;
454 default:
455 throw new Error("Unreachable");
456 }
457 if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x];
421 // XORs the codeword modules in this QR Code with the given mask pattern.
422 // The function modules must be marked and the codeword bits must be drawn
423 // before masking. Due to the arithmetic of XOR, calling applyMask() with
424 // the same mask value a second time will undo the mask. A final well-formed
425 // QR Code needs exactly one (not zero, two, etc.) mask applied.
426 private applyMask(mask: int): void {
427 if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");
428 for (let y = 0; y < this.size; y++) {
429 for (let x = 0; x < this.size; x++) {
430 let invert: boolean;
431 switch (mask) {
432 case 0:
433 invert = (x + y) % 2 == 0;
434 break;
435 case 1:
436 invert = y % 2 == 0;
437 break;
438 case 2:
439 invert = x % 3 == 0;
440 break;
441 case 3:
442 invert = (x + y) % 3 == 0;
443 break;
444 case 4:
445 invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
446 break;
447 case 5:
448 invert = ((x * y) % 2) + ((x * y) % 3) == 0;
449 break;
450 case 6:
451 invert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
452 break;
453 case 7:
454 invert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
455 break;
456 default:
457 throw new Error("Unreachable");
458458 }
459 if (!this.isFunction[y]![x]! && invert) this.modules[y]![x] = !this.modules[y]![x]!;
459460 }
460461 }
462 }
461463
462 // 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.
464 private getPenaltyScore(): int {
465 let result: int = 0;
464 // Calculates and returns the penalty score based on state of this QR Code's current modules.
465 // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
466 private getPenaltyScore(): int {
467 let result: int = 0;
466468
467 // Adjacent modules in row having same color, and finder-like patterns
468 for (let y = 0; y < this.size; y++) {
469 let runColor = false;
470 let runX = 0;
471 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
469 // Adjacent modules in row having same color, and finder-like patterns
470 for (let y = 0; y < this.size; y++) {
471 let runColor = false;
472 let runX = 0;
473 let runHistory = [0, 0, 0, 0, 0, 0, 0];
489474 for (let x = 0; x < this.size; x++) {
490 let runColor = false;
491 let runY = 0;
492 let runHistory = [0, 0, 0, 0, 0, 0, 0];
493 for (let y = 0; y < this.size; y++) {
494 if (this.modules[y][x] == runColor) {
495 runY++;
496 if (runY == 5) result += QrCode.PENALTY_N1;
497 else if (runY > 5) result++;
498 } else {
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 }
475 if (this.modules[y]![x] === runColor) {
476 runX++;
477 if (runX == 5) result += QrCode.PENALTY_N1;
478 else if (runX > 5) result++;
479 } else {
480 this.finderPenaltyAddHistory(runX, runHistory);
481 if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
482 runColor = this.modules[y]![x]!;
483 runX = 1;
505484 }
506 result +=
507 this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * QrCode.PENALTY_N3;
508485 }
509
510 // 2*2 blocks of modules having same color
511 for (let y = 0; y < this.size - 1; y++) {
512 for (let x = 0; x < this.size - 1; x++) {
513 const color: boolean = this.modules[y][x];
514 if (
515 color == this.modules[y][x + 1] &&
516 color == this.modules[y + 1][x] &&
517 color == this.modules[y + 1][x + 1]
518 )
519 result += QrCode.PENALTY_N2;
486 result += 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++) {
490 let runColor = false;
491 let runY = 0;
492 let runHistory = [0, 0, 0, 0, 0, 0, 0];
493 for (let y = 0; y < this.size; y++) {
494 if (this.modules[y]![x] === runColor) {
495 runY++;
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;
520503 }
521504 }
522
523 // Balance of dark and light modules
524 let dark: int = 0;
525 for (const row of this.modules)
526 dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
527 const total: int = this.size * this.size; // Note that size is odd, so dark/total != 1/2
528 // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
529 const k: int = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
530 assert(0 <= k && k <= 9);
531 result += k * QrCode.PENALTY_N4;
532 assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
533 return result;
534 }
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;
505 result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * QrCode.PENALTY_N3;
506 }
507
508 // 2*2 blocks of modules having same color
509 for (let y = 0; y < this.size - 1; y++) {
510 for (let x = 0; x < this.size - 1; x++) {
511 const color: boolean = this.modules[y]![x]!;
512 if (
513 color == this.modules[y]![x + 1]! &&
514 color == this.modules[y + 1]![x]! &&
515 color == this.modules[y + 1]![x + 1]!
516 )
517 result += QrCode.PENALTY_N2;
551518 }
552519 }
553520
554 // Returns the number of data bits that can be stored in a QR Code of the given version number, after
555 // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
556 // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
557 private static getNumRawDataModules(ver: int): int {
558 if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)
559 throw new RangeError("Version number out of range");
560 let result: int = (16 * ver + 128) * ver + 64;
561 if (ver >= 2) {
562 const numAlign: int = Math.floor(ver / 7) + 2;
563 result -= (25 * numAlign - 10) * numAlign - 55;
564 if (ver >= 7) result -= 36;
565 }
566 assert(208 <= result && result <= 29648);
521 // Balance of dark and light modules
522 let dark: int = 0;
523 for (const row of this.modules) dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
524 const total: int = this.size * this.size; // Note that size is odd, so dark/total != 1/2
525 // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
526 const k: int = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
527 assert(0 <= k && k <= 9);
528 result += k * QrCode.PENALTY_N4;
529 assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
530 return result;
531 }
532
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);
567546 return result;
568547 }
548 }
569549
570 // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
571 // QR Code of the given version number and error correction level, with remainder bits discarded.
572 // This stateless pure function could be implemented as a (40*4)-cell lookup table.
573 private static getNumDataCodewords(ver: int, ecl: QrCode.Ecc): int {
574 return (
575 Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
576 QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] *
577 QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]
578 );
579 }
550 // Returns the number of data bits that can be stored in a QR Code of the given version number, after
551 // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
552 // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
553 private static getNumRawDataModules(ver: int): int {
554 if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)
555 throw new RangeError("Version number out of range");
556 let result: int = (16 * ver + 128) * ver + 64;
557 if (ver >= 2) {
558 const numAlign: int = Math.floor(ver / 7) + 2;
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 be
582 // implemented as a lookup table over all possible parameter values, instead of as an algorithm.
583 private static reedSolomonComputeDivisor(degree: int): Array<byte> {
584 if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");
585 // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
586 // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
587 let result: Array<byte> = [];
588 for (let i = 0; i < degree - 1; i++) result.push(0);
589 result.push(1); // Start off with the monomial x^0
590
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 }
566 // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
567 // QR Code of the given version number and error correction level, with remainder bits discarded.
568 // This stateless pure function could be implemented as a (40*4)-cell lookup table.
569 private static getNumDataCodewords(ver: int, ecl: QrCodeEcc): int {
570 return (
571 Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
572 QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal]![ver]! *
573 QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal]![ver]!
574 );
575 }
605576
606 // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
607 private static reedSolomonComputeRemainder(
608 data: Readonly<Array<byte>>,
609 divisor: Readonly<Array<byte>>,
610 ): Array<byte> {
611 let result: Array<byte> = divisor.map((_) => 0);
612 for (const b of data) {
613 // Polynomial division
614 const factor: byte = b ^ (result.shift() as byte);
615 result.push(0);
616 divisor.forEach((coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)));
577 // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be
578 // implemented as a lookup table over all possible parameter values, instead of as an algorithm.
579 private static reedSolomonComputeDivisor(degree: int): Array<byte> {
580 if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");
581 // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
582 // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
583 let result: Array<byte> = [];
584 for (let i = 0; i < degree - 1; i++) result.push(0);
585 result.push(1); // Start off with the monomial x^0
586
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]!;
617596 }
618 return result;
597 root = QrCode.reedSolomonMultiply(root, 0x02);
619598 }
599 return result;
600 }
620601
621 // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result
622 // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
623 private static reedSolomonMultiply(x: byte, y: byte): byte {
624 if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");
625 // Russian peasant multiplication
626 let z: int = 0;
627 for (let i = 7; i >= 0; i--) {
628 z = (z << 1) ^ ((z >>> 7) * 0x11d);
629 z ^= ((y >>> i) & 1) * x;
630 }
631 assert(z >>> 8 == 0);
632 return z as byte;
633 }
602 // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
603 private static reedSolomonComputeRemainder(
604 data: Readonly<Array<byte>>,
605 divisor: Readonly<Array<byte>>,
606 ): Array<byte> {
607 let result: Array<byte> = divisor.map((_) => 0);
608 for (const b of data) {
609 // Polynomial division
610 const factor: byte = b ^ (result.shift() as byte);
611 result.push(0);
612 divisor.forEach((coef, i) => (result[i]! ^= QrCode.reedSolomonMultiply(coef, factor)));
613 }
614 return result;
615 }
634616
635 // Can only be called immediately after a light run is added, and
636 // returns either 0, 1, or 2. A helper function for getPenaltyScore().
637 private finderPenaltyCountPatterns(runHistory: Readonly<Array<int>>): int {
638 const n: int = runHistory[1];
639 assert(n <= this.size * 3);
640 const core: boolean =
641 n > 0 &&
642 runHistory[2] == n &&
643 runHistory[3] == n * 3 &&
644 runHistory[4] == n &&
645 runHistory[5] == n;
646 return (
647 (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) +
648 (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0)
649 );
650 }
617 // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result
618 // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
619 private static reedSolomonMultiply(x: byte, y: byte): byte {
620 if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");
621 // Russian peasant multiplication
622 let z: int = 0;
623 for (let i = 7; i >= 0; i--) {
624 z = (z << 1) ^ ((z >>> 7) * 0x11d);
625 z ^= ((y >>> i) & 1) * x;
626 }
627 assert(z >>> 8 == 0);
628 return z as byte;
629 }
651630
652 // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
653 private finderPenaltyTerminateAndCount(
654 currentRunColor: boolean,
655 currentRunLength: int,
656 runHistory: Array<int>,
657 ): int {
658 if (currentRunColor) {
659 // Terminate dark run
660 this.finderPenaltyAddHistory(currentRunLength, runHistory);
661 currentRunLength = 0;
662 }
663 currentRunLength += this.size; // Add light border to final run
631 // Can only be called immediately after a light run is added, and
632 // returns either 0, 1, or 2. A helper function for getPenaltyScore().
633 private finderPenaltyCountPatterns(runHistory: Readonly<Array<int>>): int {
634 const n: int = runHistory[1]!;
635 assert(n <= this.size * 3);
636 const core: boolean =
637 n > 0 &&
638 runHistory[2] === n &&
639 runHistory[3] === n * 3 &&
640 runHistory[4] === n &&
641 runHistory[5] === n;
642 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
664656 this.finderPenaltyAddHistory(currentRunLength, runHistory);
665 return this.finderPenaltyCountPatterns(runHistory);
657 currentRunLength = 0;
666658 }
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().
669 private finderPenaltyAddHistory(currentRunLength: int, runHistory: Array<int>): void {
670 if (runHistory[0] == 0) currentRunLength += this.size; // Add light border to initial run
671 runHistory.pop();
672 runHistory.unshift(currentRunLength);
673 }
664 // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
665 private finderPenaltyAddHistory(currentRunLength: int, runHistory: Array<int>): void {
666 if (runHistory[0] === 0) currentRunLength += this.size; // Add light border to initial run
667 runHistory.pop();
668 runHistory.unshift(currentRunLength);
669 }
674670
675 /*-- Constants and tables --*/
676
677 // The minimum version number supported in the QR Code Model 2 standard.
678 public static readonly MIN_VERSION: int = 1;
679 // The maximum version number supported in the QR Code Model 2 standard.
680 public static readonly MAX_VERSION: int = 40;
681
682 // For use in getPenaltyScore(), when evaluating which mask is best.
683 private static readonly PENALTY_N1: int = 3;
684 private static readonly PENALTY_N2: int = 3;
685 private static readonly PENALTY_N3: int = 40;
686 private static readonly PENALTY_N4: int = 10;
687
688 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)
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 level
691 [
692 -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28,
693 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
694 ], // Low
695 [
696 -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,
698 ], // Medium
699 [
700 -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,
702 ], // Quartile
703 [
704 -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,
706 ], // High
707 ];
708
709 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)
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 level
712 [
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,
714 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25,
715 ], // Low
716 [
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,
718 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49,
719 ], // Medium
720 [
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,
722 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68,
723 ], // Quartile
724 [
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,
726 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81,
727 ], // High
728 ];
729 }
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 }
671 /*-- Constants and tables --*/
672
673 // The minimum version number supported in the QR Code Model 2 standard.
674 public static readonly MIN_VERSION: int = 1;
675 // The maximum version number supported in the QR Code Model 2 standard.
676 public static readonly MAX_VERSION: int = 40;
677
678 // For use in getPenaltyScore(), when evaluating which mask is best.
679 private static readonly PENALTY_N1: int = 3;
680 private static readonly PENALTY_N2: int = 3;
681 private static readonly PENALTY_N3: int = 40;
682 private static readonly PENALTY_N4: int = 10;
683
684 private static readonly ECC_CODEWORDS_PER_BLOCK: Array<Array<int>> = [
685 // Version: (note that index 0 is for padding, and is set to an illegal value)
686 //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
687 [
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,
689 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
690 ], // Low
691 [
692 -1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28,
693 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
694 ], // Medium
695 [
696 -1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30,
697 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
698 ], // Quartile
699 [
700 -1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24,
701 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
702 ], // High
703 ];
704
705 private static readonly NUM_ERROR_CORRECTION_BLOCKS: Array<Array<int>> = [
706 // Version: (note that index 0 is for padding, and is set to an illegal value)
707 //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
708 [
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,
710 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25,
711 ], // Low
712 [
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,
714 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49,
715 ], // Medium
716 [
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,
718 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68,
719 ], // Quartile
720 [
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,
722 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81,
723 ], // High
724 ];
725}
777726
778 // Returns a segment representing the given string of decimal digits encoded in numeric mode.
779 public static makeNumeric(digits: string): QrSegment {
780 if (!QrSegment.isNumeric(digits))
781 throw new RangeError("String contains non-numeric characters");
782 let bb: Array<bit> = [];
783 for (let i = 0; i < digits.length; ) {
784 // Consume up to 3 digits per iteration
785 const n: int = Math.min(digits.length - i, 3);
786 appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
787 i += n;
788 }
789 return new QrSegment(QrSegment.Mode.NUMERIC, digits.length, bb);
790 }
727// Appends the given number of low-order bits of the given value
728// to the given buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
729function appendBits(val: int, len: int, bb: Array<bit>): void {
730 if (len < 0 || len > 31 || val >>> len != 0) throw new RangeError("Value out of range");
731 for (
732 let i = len - 1;
733 i >= 0;
734 i-- // Append bit by bit
735 )
736 bb.push((val >>> i) & 1);
737}
791738
792 // Returns a segment representing the given text string encoded in alphanumeric mode.
793 // The characters allowed are: 0 to 9, A to Z (uppercase only), space,
794 // dollar, percent, asterisk, plus, hyphen, period, slash, colon.
795 public static makeAlphanumeric(text: string): QrSegment {
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 }
739// Returns true iff the i'th bit of x is set to 1.
740function getBit(x: int, i: int): boolean {
741 return ((x >>> i) & 1) != 0;
742}
811743
812 // Returns a new mutable list of zero or more segments to represent the given Unicode text string.
813 // The result may use various segment modes and switch modes to optimize the length of the bit stream.
814 public static makeSegments(text: string): Array<QrSegment> {
815 // Select the most efficient segment encoding automatically
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 }
744// Throws an exception if the given condition is false.
745function assert(cond: boolean): void {
746 if (!cond) throw new Error("Assertion error");
747}
821748
822 // Returns a segment representing an Extended Channel Interpretation
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 }
749/*---- Data segment class ----*/
837750
838 // Tests whether the given string can be encoded as a segment in numeric mode.
839 // A string is encodable iff each character is in the range 0 to 9.
840 public static isNumeric(text: string): boolean {
841 return QrSegment.NUMERIC_REGEX.test(text);
842 }
751/*
752 * A segment of character/binary/control data in a QR Code symbol.
753 * Instances of this class are immutable.
754 * The mid-level way to create a segment is to take the payload data
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.
845 // A string is encodable iff each character is in the following set: 0 to 9, A to Z
846 // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
847 public static isAlphanumeric(text: string): boolean {
848 return QrSegment.ALPHANUMERIC_REGEX.test(text);
849 }
776 // Returns a segment representing the given string of decimal digits encoded in numeric mode.
777 public static makeNumeric(digits: string): QrSegment {
778 if (!QrSegment.isNumeric(digits))
779 throw new RangeError("String contains non-numeric characters");
780 let bb: Array<bit> = [];
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 --*/
852
853 // Creates a new QR Code segment with the given attributes and data.
854 // The character count (numChars) must agree with the mode and the bit buffer length,
855 // but the constraint isn't checked. The given bit buffer is cloned and stored.
856 public constructor(
857 // The mode indicator of this segment.
858 public readonly mode: QrSegment.Mode,
859
860 // The length of this segment's unencoded data. Measured in characters for
861 // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
862 // Always zero or positive. Not the same as the data's bit length.
863 public readonly numChars: int,
864
865 // The data bits of this segment. Accessed through getData().
866 private readonly bitData: Array<bit>,
867 ) {
868 if (numChars < 0) throw new RangeError("Invalid argument");
869 this.bitData = bitData.slice(); // Make defensive copy
870 }
790 // Returns a segment representing the given text string encoded in alphanumeric mode.
791 // The characters allowed are: 0 to 9, A to Z (uppercase only), space,
792 // dollar, percent, asterisk, plus, hyphen, period, slash, colon.
793 public static makeAlphanumeric(text: string): QrSegment {
794 if (!QrSegment.isAlphanumeric(text))
795 throw new RangeError("String contains unencodable characters in alphanumeric mode");
796 let bb: Array<bit> = [];
797 let i: int;
798 for (i = 0; i + 2 <= text.length; i += 2) {
799 // Process groups of 2
800 let temp: int = QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
801 temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
802 appendBits(temp, 11, bb);
803 }
804 if (i < text.length)
805 // 1 character remaining
806 appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
807 return new QrSegment(QrSegment.Mode.ALPHANUMERIC, text.length, bb);
808 }
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.
875 public getData(): Array<bit> {
876 return this.bitData.slice(); // Make defensive copy
877 }
820 // Returns a segment representing an Extended Channel Interpretation
821 // (ECI) designator with the given assignment value.
822 public static makeEci(assignVal: int): QrSegment {
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 at
880 // the given version. The result is infinity if a segment has too many characters to fit its length field.
881 public static getTotalBits(segs: Readonly<Array<QrSegment>>, version: int): number {
882 let result: number = 0;
883 for (const seg of segs) {
884 const ccbits: int = seg.mode.numCharCountBits(version);
885 if (seg.numChars >= 1 << ccbits) return Infinity; // The segment's length doesn't fit the field's bit width
886 result += 4 + ccbits + seg.bitData.length;
887 }
888 return result;
836 // Tests whether the given string can be encoded as a segment in numeric mode.
837 // A string is encodable iff each character is in the range 0 to 9.
838 public static isNumeric(text: string): boolean {
839 return QrSegment.NUMERIC_REGEX.test(text);
840 }
841
842 // Tests whether the given string can be encoded as a segment in alphanumeric mode.
843 // A string is encodable iff each character is in the following set: 0 to 9, A to Z
844 // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
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;
889891 }
892 return result;
893 }
890894
891 // Returns a new array of bytes representing the given string encoded in UTF-8.
892 private static toUtf8ByteArray(str: string): Array<byte> {
893 str = encodeURI(str);
894 let result: Array<byte> = [];
895 for (let i = 0; i < str.length; i++) {
896 if (str.charAt(i) != "%") result.push(str.charCodeAt(i));
897 else {
898 result.push(parseInt(str.substring(i + 1, i + 3), 16));
899 i += 2;
900 }
895 // Returns a new array of bytes representing the given string encoded in UTF-8.
896 private static toUtf8ByteArray(str: string): Array<byte> {
897 str = encodeURI(str);
898 let result: Array<byte> = [];
899 for (let i = 0; i < str.length; i++) {
900 if (str.charAt(i) != "%") result.push(str.charCodeAt(i));
901 else {
902 result.push(parseInt(str.substring(i + 1, i + 3), 16));
903 i += 2;
901904 }
902 return result;
903905 }
906 return result;
907 }
904908
905 /*-- Constants --*/
909 /*-- Constants --*/
906910
907 // Describes precisely all strings that are encodable in numeric mode.
908 private static readonly NUMERIC_REGEX: RegExp = /^[0-9]*$/;
911 // Describes precisely all strings that are encodable in numeric mode.
912 private static readonly NUMERIC_REGEX: RegExp = /^[0-9]*$/;
909913
910 // Describes precisely all strings that are encodable in alphanumeric mode.
911 private static readonly ALPHANUMERIC_REGEX: RegExp = /^[A-Z0-9 $%*+.\/:-]*$/;
914 // Describes precisely all strings that are encodable in alphanumeric mode.
915 private static readonly ALPHANUMERIC_REGEX: RegExp = /^[A-Z0-9 $%*+.\/:-]*$/;
912916
913 // The set of all legal characters in alphanumeric mode,
914 // where each character value maps to the index in the string.
915 private static readonly ALPHANUMERIC_CHARSET: string =
916 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
917 }
917 // The set of all legal characters in alphanumeric mode,
918 // where each character value maps to the index in the string.
919 private static readonly ALPHANUMERIC_CHARSET: string =
920 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
918921}
919922
920923/*---- Public helper enumeration ----*/
921924
922namespace qrcodegen.QrCode {
923 type int = number;
925class QrCodeEcc {
926 /*-- Constants --*/
924927
925 /*
926 * The error correction level in a QR Code symbol. Immutable.
927 */
928 export class Ecc {
929 /*-- Constants --*/
928 public static readonly LOW = new QrCodeEcc(0, 1); // The QR Code can tolerate about 7% erroneous codewords
929 public static readonly MEDIUM = new QrCodeEcc(1, 0); // The QR Code can tolerate about 15% erroneous codewords
930 public static readonly QUARTILE = new QrCodeEcc(2, 3); // The QR Code can tolerate about 25% erroneous codewords
931 public static readonly HIGH = new QrCodeEcc(3, 2); // The QR Code can tolerate about 30% erroneous codewords
930932
931 public static readonly LOW = new Ecc(0, 1); // The QR Code can tolerate about 7% erroneous codewords
932 public static readonly MEDIUM = new Ecc(1, 0); // The QR Code can tolerate about 15% erroneous codewords
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
933 public readonly ordinal: int;
934 public readonly formatBits: int;
935935
936 /*-- Constructor and fields --*/
936 /*-- Constructor and fields --*/
937937
938 private constructor(
939 // In the range 0 to 3 (unsigned 2-bit integer).
940 public readonly ordinal: int,
941 // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
942 public readonly formatBits: int,
943 ) {}
938 private constructor(
939 // In the range 0 to 3 (unsigned 2-bit integer).
940 ordinal: int,
941 // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
942 formatBits: int,
943 ) {
944 this.ordinal = ordinal;
945 this.formatBits = formatBits;
944946 }
945947}
946948
947949/*---- Public helper enumeration ----*/
948950
949namespace qrcodegen.QrSegment {
950 type int = number;
951class QrSegmentMode {
952 /*-- Constants --*/
951953
952 /*
953 * Describes how a segment's data bits are interpreted. Immutable.
954 */
955 export class Mode {
956 /*-- Constants --*/
954 public static readonly NUMERIC = new QrSegmentMode(0x1, [10, 12, 14]);
955 public static readonly ALPHANUMERIC = new QrSegmentMode(0x2, [9, 11, 13]);
956 public static readonly BYTE = new QrSegmentMode(0x4, [8, 16, 16]);
957 public static readonly KANJI = new QrSegmentMode(0x8, [8, 10, 12]);
958 public static readonly ECI = new QrSegmentMode(0x7, [0, 0, 0]);
957959
958 public static readonly NUMERIC = new Mode(0x1, [10, 12, 14]);
959 public static readonly ALPHANUMERIC = new Mode(0x2, [9, 11, 13]);
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]);
960 public readonly modeBits: int;
961 private readonly numBitsCharCount: [int, int, int];
963962
964 /*-- Constructor and fields --*/
963 /*-- Constructor and fields --*/
965964
966 private constructor(
967 // The mode indicator bits, which is a uint4 value (range 0 to 15).
968 public readonly modeBits: int,
969 // Number of character count bits for three different version ranges.
970 private readonly numBitsCharCount: [int, int, int],
971 ) {}
965 private constructor(
966 // The mode indicator bits, which is a uint4 value (range 0 to 15).
967 modeBits: int,
968 // Number of character count bits for three different version ranges.
969 numBitsCharCount: [int, int, int],
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 in
976 // 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 {
978 return this.numBitsCharCount[Math.floor((ver + 7) / 17)];
979 }
977 // (Package-private) Returns the bit width of the character count field for a segment in
978 // this mode in a QR Code at the given version number. The result is in the range [0, 16].
979 public numCharCountBits(ver: int): int {
980 return this.numBitsCharCount[Math.floor((ver + 7) / 17)]!;
980981 }
981982}
982983
983export const QrCode = qrcodegen.QrCode;
984export const QrSegment = qrcodegen.QrSegment;
984QrCode.Ecc = QrCodeEcc;
985QrSegment.Mode = QrSegmentMode;
packages/shared/src/searchRanking.test.ts+1-1
......@@ -6,7 +6,7 @@ import {
66 normalizeSearchQuery,
77 scoreQueryMatch,
88 scoreSubsequenceMatch,
9} from "./searchRanking";
9} from "./searchRanking.ts";
1010
1111describe("normalizeSearchQuery", () => {
1212 it("trims and lowercases queries", () => {
packages/shared/src/serverSettings.test.ts+1-1
......@@ -5,7 +5,7 @@ import {
55 extractPersistedServerObservabilitySettings,
66 normalizePersistedServerSettingString,
77 parsePersistedServerObservabilitySettings,
8} from "./serverSettings";
8} from "./serverSettings.ts";
99
1010describe("serverSettings helpers", () => {
1111 it("normalizes optional persisted strings", () => {
packages/shared/src/serverSettings.ts+2-2
......@@ -1,7 +1,7 @@
11import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";
22import { Schema } from "effect";
3import { deepMerge } from "./Struct";
4import { fromLenientJson } from "./schemaJson";
3import { deepMerge } from "./Struct.ts";
4import { fromLenientJson } from "./schemaJson.ts";
55
66const ServerSettingsJson = fromLenientJson(ServerSettings);
77
packages/shared/src/shell.test.ts+1-1
......@@ -7,7 +7,7 @@ import {
77 readEnvironmentFromLoginShell,
88 readPathFromLaunchctl,
99 readPathFromLoginShell,
10} from "./shell";
10} from "./shell.ts";
1111
1212describe("extractPathFromShellOutput", () => {
1313 it("extracts the path between capture markers", () => {
scripts/build-desktop-artifact.ts+83-28
......@@ -1,9 +1,5 @@
11#!/usr/bin/env node
22
3import { spawnSync } from "node:child_process";
4import { existsSync } from "node:fs";
5import { join } from "node:path";
6
73import rootPackageJson from "../package.json" with { type: "json" };
84import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
95import serverPackageJson from "../apps/server/package.json" with { type: "json" };
......@@ -14,7 +10,18 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
1410
1511import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1612import * 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";
1825import { Command, Flag } from "effect/unstable/cli";
1926import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2027
......@@ -91,12 +98,49 @@ class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
9198 readonly cause?: unknown;
9299}> {}
93100
94function resolveGitCommitHash(repoRoot: string): string {
95 const result = spawnSync("git", ["rev-parse", "--short=12", "HEAD"], {
96 cwd: repoRoot,
97 encoding: "utf8",
98 });
99 if (result.status !== 0) {
101const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
102 stream.pipe(
103 Stream.decodeText(),
104 Stream.runFold(
105 () => "",
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) {
100144 return "unknown";
101145 }
102146 const hash = result.stdout.trim();
......@@ -104,11 +148,13 @@ function resolveGitCommitHash(repoRoot: string): string {
104148 return "unknown";
105149 }
106150 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;
110156 const configured = process.env.npm_config_python ?? process.env.PYTHON;
111 if (configured && existsSync(configured)) {
157 if (configured && (yield* fs.exists(configured))) {
112158 return configured;
113159 }
114160
......@@ -116,28 +162,37 @@ function resolvePythonForNodeGyp(): string | undefined {
116162 const localAppData = process.env.LOCALAPPDATA;
117163 if (localAppData) {
118164 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {
119 const candidate = join(localAppData, "Programs", "Python", version, "python.exe");
120 if (existsSync(candidate)) {
165 const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe");
166 if (yield* fs.exists(candidate)) {
121167 return candidate;
122168 }
123169 }
124170 }
125171 }
126172
127 const probe = spawnSync("python", ["-c", "import sys;print(sys.executable)"], {
128 encoding: "utf8",
129 });
130 if (probe.status !== 0) {
173 const probe = yield* spawnAndCollectOutput(
174 ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]),
175 ).pipe(
176 Effect.catch(() =>
177 Effect.succeed({
178 stdout: "",
179 stderr: "",
180 exitCode: 1,
181 }),
182 ),
183 );
184
185 if (probe.exitCode !== 0) {
131186 return undefined;
132187 }
133188
134189 const executable = probe.stdout.trim();
135 if (!executable || !existsSync(executable)) {
190 if (!executable || !(yield* fs.exists(executable))) {
136191 return undefined;
137192 }
138193
139194 return executable;
140}
195});
141196
142197interface ResolvedBuildOptions {
143198 readonly platform: typeof BuildPlatform.Type;
......@@ -654,7 +709,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
654709
655710 const appVersion = options.version ?? serverPackageJson.version;
656711 const iconAssets = resolveDesktopBuildIconAssets(appVersion);
657 const commitHash = resolveGitCommitHash(repoRoot);
712 const commitHash = yield* resolveGitCommitHash(repoRoot);
658713 const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;
659714 const stageRoot = yield* mkdir({
660715 prefix: `t3code-desktop-${options.platform}-stage-`,
......@@ -709,9 +764,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
709764 options.platform,
710765 stageResourcesDir,
711766 {
712 macIconPng: join(repoRoot, iconAssets.macIconPng),
713 linuxIconPng: join(repoRoot, iconAssets.linuxIconPng),
714 windowsIconIco: join(repoRoot, iconAssets.windowsIconIco),
767 macIconPng: path.join(repoRoot, iconAssets.macIconPng),
768 linuxIconPng: path.join(repoRoot, iconAssets.linuxIconPng),
769 windowsIconIco: path.join(repoRoot, iconAssets.windowsIconIco),
715770 },
716771 options.verbose,
717772 );
......@@ -727,7 +782,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
727782 private: true,
728783 description: "T3 Code desktop build",
729784 author: "T3 Tools",
730 main: "apps/desktop/dist-electron/main.js",
785 main: "apps/desktop/dist-electron/main.cjs",
731786 build: yield* createBuildConfig(
732787 options.platform,
733788 options.target,
......@@ -777,7 +832,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
777832 }
778833
779834 if (process.platform === "win32") {
780 const python = resolvePythonForNodeGyp();
835 const python = yield* resolvePythonForNodeGyp();
781836 if (python) {
782837 buildEnv.PYTHON = python;
783838 buildEnv.npm_config_python = python;
scripts/dev-runner.test.ts+10-7
......@@ -1,8 +1,7 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
2import { homedir } from "node:os";
3import { resolve } from "node:path";
2import * as NodeOS from "node:os";
43import { assert, describe, it } from "@effect/vitest";
5import { Effect } from "effect";
4import { Effect, Path } from "effect";
65
76import {
87 checkPortAvailabilityOnHosts,
......@@ -49,6 +48,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
4948 describe("createDevRunnerEnv", () => {
5049 it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () =>
5150 Effect.gen(function* () {
51 const path = yield* Path.Path;
5252 const env = yield* createDevRunnerEnv({
5353 mode: "dev",
5454 baseEnv: {},
......@@ -63,12 +63,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
6363 devUrl: undefined,
6464 });
6565
66 assert.equal(env.T3CODE_HOME, resolve(homedir(), ".t3"));
66 assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".t3"));
6767 }),
6868 );
6969
7070 it.effect("supports explicit typed overrides", () =>
7171 Effect.gen(function* () {
72 const path = yield* Path.Path;
7273 const env = yield* createDevRunnerEnv({
7374 mode: "dev:server",
7475 baseEnv: {},
......@@ -83,7 +84,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
8384 devUrl: new URL("http://localhost:7331"),
8485 });
8586
86 assert.equal(env.T3CODE_HOME, resolve("/tmp/custom-t3"));
87 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/custom-t3"));
8788 assert.equal(env.T3CODE_PORT, "4222");
8889 assert.equal(env.VITE_HTTP_URL, "http://localhost:4222");
8990 assert.equal(env.VITE_WS_URL, "ws://localhost:4222");
......@@ -142,6 +143,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
142143
143144 it.effect("uses custom t3Home when provided", () =>
144145 Effect.gen(function* () {
146 const path = yield* Path.Path;
145147 const env = yield* createDevRunnerEnv({
146148 mode: "dev",
147149 baseEnv: {},
......@@ -156,12 +158,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
156158 devUrl: undefined,
157159 });
158160
159 assert.equal(env.T3CODE_HOME, resolve("/tmp/my-t3"));
161 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3"));
160162 }),
161163 );
162164
163165 it.effect("pins desktop dev to a stable backend port and websocket url", () =>
164166 Effect.gen(function* () {
167 const path = yield* Path.Path;
165168 const env = yield* createDevRunnerEnv({
166169 mode: "dev:desktop",
167170 baseEnv: {
......@@ -182,7 +185,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
182185 devUrl: undefined,
183186 });
184187
185 assert.equal(env.T3CODE_HOME, resolve("/tmp/my-t3"));
188 assert.equal(env.T3CODE_HOME, path.resolve("/tmp/my-t3"));
186189 assert.equal(env.PORT, "5733");
187190 assert.equal(env.VITE_DEV_SERVER_URL, "http://127.0.0.1:5733");
188191 assert.equal(env.HOST, "127.0.0.1");
scripts/dev-runner.ts+7-8
......@@ -1,6 +1,6 @@
11#!/usr/bin/env node
22
3import { homedir } from "node:os";
3import * as NodeOS from "node:os";
44
55import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
66import * as NodeServices from "@effect/platform-node/NodeServices";
......@@ -17,7 +17,7 @@ const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
1717const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const;
1818
1919export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) =>
20 path.join(homedir(), ".t3"),
20 path.join(NodeOS.homedir(), ".t3"),
2121);
2222
2323const MODE_ARGS = {
......@@ -523,11 +523,10 @@ const cliRuntimeLayer = Layer.mergeAll(
523523 NetService.layer,
524524);
525525
526const runtimeProgram = Command.run(devRunnerCli, { version: "0.0.0" }).pipe(
527 Effect.scoped,
528 Effect.provide(cliRuntimeLayer),
529);
530
531526if (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 );
533532}
scripts/merge-update-manifests.test.ts+116
......@@ -1,11 +1,17 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
12import { assert, describe, it } from "@effect/vitest";
3import { Effect, FileSystem, Path } from "effect";
4import { Command, CliError } from "effect/unstable/cli";
25
36import {
47 mergePlatformUpdateManifests,
8 mergeUpdateManifestsCommand,
59 parsePlatformUpdateManifest,
610 serializePlatformUpdateManifest,
711} from "./merge-update-manifests.ts";
812
13const runCli = Command.runWith(mergeUpdateManifestsCommand, { version: "0.0.0" });
14
915describe("merge-update-manifests", () => {
1016 it("merges arm64 and x64 macOS update manifests into one multi-arch manifest", () => {
1117 const arm64 = parsePlatformUpdateManifest(
......@@ -188,3 +194,113 @@ releaseDate: '2026-03-07T10:36:07.540Z'
188194 assert.equal(reparsed.version, "1.0");
189195 });
190196});
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 @@
1import { readFileSync, writeFileSync } from "node:fs";
2import { resolve } from "node:path";
3import { fileURLToPath } from "node:url";
1#!/usr/bin/env node
2
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
58import {
69 mergeUpdateManifests,
......@@ -9,7 +12,8 @@ import {
912 type UpdateManifest,
1013} from "./lib/update-manifest.ts";
1114
12export type UpdateManifestPlatform = "mac" | "win";
15const UpdateManifestPlatform = Schema.Literals(["mac", "win"]);
16export type UpdateManifestPlatform = typeof UpdateManifestPlatform.Type;
1317
1418function getPlatformLabel(platform: UpdateManifestPlatform): string {
1519 return platform === "mac" ? "macOS" : "Windows";
......@@ -40,52 +44,65 @@ export function serializePlatformUpdateManifest(
4044 });
4145}
4246
43function parseArgs(args: ReadonlyArray<string>): {
44 platform: UpdateManifestPlatform;
45 primaryPath: string;
46 secondaryPath: string;
47 outputPath: string;
48} {
49 const [platformFlag, platformValue, primaryPathArg, secondaryPathArg, outputPathArg] = args;
50 if (platformFlag !== "--platform" || (platformValue !== "mac" && platformValue !== "win")) {
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);
47export const mergeUpdateManifestFiles = Effect.fn("mergeUpdateManifestFiles")(function* (
48 platform: UpdateManifestPlatform,
49 primaryPathArg: string,
50 secondaryPathArg: string,
51 outputPathArg: string | undefined,
52) {
53 const fs = yield* FileSystem.FileSystem;
54 const path = yield* Path.Path;
6455
65 return {
66 platform: platformValue,
67 primaryPath,
68 secondaryPath,
69 outputPath,
70 };
71}
56 const primaryPath = path.resolve(primaryPathArg);
57 const secondaryPath = path.resolve(secondaryPathArg);
58 const outputPath = path.resolve(outputPathArg ?? primaryPathArg);
7259
73function main(args: ReadonlyArray<string>): void {
74 const { platform, primaryPath, secondaryPath, outputPath } = parseArgs(args);
7560 const primaryManifest = parsePlatformUpdateManifest(
7661 platform,
77 readFileSync(primaryPath, "utf8"),
62 yield* fs.readFileString(primaryPath),
7863 primaryPath,
7964 );
8065 const secondaryManifest = parsePlatformUpdateManifest(
8166 platform,
82 readFileSync(secondaryPath, "utf8"),
67 yield* fs.readFileString(secondaryPath),
8368 secondaryPath,
8469 );
8570 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)) {
90 main(process.argv.slice(2));
72 yield* fs.writeFileString(outputPath, serializePlatformUpdateManifest(platform, merged));
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 );
91108}
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 @@
1import { resolve, relative } from "node:path";
2import { realpathSync } from "node:fs";
1import * as NodeHttp from "node:http";
32
4const port = Number(process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000);
5const root =
6 process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT ??
7 resolve(import.meta.dirname, "..", "release-mock");
3import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Effect, FileSystem, Layer, Path } from "effect";
6import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
87
9const mockServerLog = (level: "info" | "warn" | "error" = "info", message: string) => {
10 console[level](`[mock-update-server] ${message}`);
11};
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 }
8interface MockUpdateServerConfig {
9 readonly port: number;
10 readonly rootRealPath: string;
2011}
2112
22Bun.serve({
23 port,
24 hostname: "localhost",
25 fetch: async (request) => {
26 const url = new URL(request.url);
27 const path = url.pathname;
28 mockServerLog("info", `Request received for path: ${path}`);
29 const filePath = resolve(root, `.${path}`);
30 if (!isWithinRoot(filePath)) {
31 mockServerLog("warn", `Attempted to access file outside of root: ${filePath}`);
32 return new Response("Not Found", { status: 404 });
13const resolveMockUpdateServerConfig = Effect.gen(function* () {
14 const fileSystem = yield* FileSystem.FileSystem;
15 const path = yield* Path.Path;
16 const config = yield* Config.all({
17 port: Config.port("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.withDefault(3000)),
18 root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe(
19 Config.withDefault("../release-mock"),
20 ),
21 }).asEffect();
22
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;
3371 }
34 const file = Bun.file(filePath);
35 if (!(await file.exists())) {
36 mockServerLog("warn", `Attempted to access non-existent file: ${filePath}`);
37 return new Response("Not Found", { status: 404 });
72
73 if (decodedPath.includes("\0")) {
74 return undefined;
3875 }
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(
196196 ),
197197).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series."));
198198
199Command.run(command, { version: "0.0.0" }).pipe(
200 Effect.scoped,
201 Effect.provide(NodeServices.layer),
202 NodeRuntime.runMain,
203);
199if (import.meta.main) {
200 Command.run(command, { version: "0.0.0" }).pipe(
201 Effect.scoped,
202 Effect.provide(NodeServices.layer),
203 NodeRuntime.runMain,
204 );
205}
scripts/tsconfig.json+2-4
......@@ -2,10 +2,8 @@
22 "extends": "../tsconfig.base.json",
33 "compilerOptions": {
44 "composite": true,
5 "types": ["node", "bun"],
6 "lib": ["ES2023", "esnext.disposable"],
7 "noEmit": true,
8 "allowImportingTsExtensions": true,
5 "types": ["node"],
6 "lib": ["ESNext", "esnext.disposable"],
97 "plugins": [
108 {
119 "name": "@effect/language-service"
scripts/update-release-package-versions.test.ts+199-57
......@@ -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", () => {
6 it("parses version only", () => {
7 expect(parseArgs(["1.2.3"])).toEqual({
8 version: "1.2.3",
9 rootDir: undefined,
10 writeGithubOutput: false,
11 });
12 });
13const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);
14const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });
15const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
16const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
17 SchemaGetter.stringifyJson({ space: 2 }),
18);
19const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
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", () => {
15 expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({
16 version: "1.2.3",
17 rootDir: "/path",
18 writeGithubOutput: false,
19 });
20 });
28const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (
29 rootDir: string,
30 version: string,
31) {
32 const fs = yield* FileSystem.FileSystem;
33 const path = yield* Path.Path;
2134
22 it("parses version with --github-output", () => {
23 expect(parseArgs(["1.2.3", "--github-output"])).toEqual({
24 version: "1.2.3",
25 rootDir: undefined,
26 writeGithubOutput: true,
27 });
28 });
35 for (const relativePath of releasePackageFiles) {
36 const filePath = path.join(rootDir, relativePath);
37 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
38 yield* fs.writeFileString(
39 filePath,
40 `${encodePackageJson({
41 name: relativePath,
42 version,
43 private: true,
44 })}\n`,
45 );
46 }
47});
2948
30 it("parses version with --root and --github-output", () => {
31 expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
32 version: "1.2.3",
33 rootDir: "/path",
34 writeGithubOutput: true,
35 });
36 });
49const readReleaseVersions = Effect.fn("readReleaseVersions")(function* (rootDir: string) {
50 const fs = yield* FileSystem.FileSystem;
51 const path = yield* Path.Path;
52 const versions = new Map<string, string>();
3753
38 it("accepts flags before the version positional", () => {
39 expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({
40 version: "1.2.3",
41 rootDir: "/path",
42 writeGithubOutput: true,
43 });
44 });
54 for (const relativePath of releasePackageFiles) {
55 const filePath = path.join(rootDir, relativePath);
56 const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson));
57 versions.set(relativePath, String(packageJson.version));
58 }
4559
46 it("throws on missing version", () => {
47 expect(() => parseArgs([])).toThrow("Usage:");
48 });
60 return versions;
61});
4962
50 it("throws on duplicate version", () => {
51 expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow(
52 "Only one release version can be provided.",
63const captureLogs = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
64 Effect.gen(function* () {
65 const result = yield* effect;
66 const logs = (yield* TestConsole.logLines).filter(
67 (line): line is string => typeof line === "string",
5368 );
69 return { result, logs };
5470 });
5571
56 it("throws on unknown flag", () => {
57 expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown");
58 });
72it.layer(ScriptTestLayer)("update-release-package-versions", (it) => {
73 it.effect("updates all release package versions under the provided root", () =>
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", () => {
61 expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root.");
62 });
80 yield* writePackageJsonFixtures(baseDir, "0.0.1");
6381
64 it("does not consume version as --github-output value", () => {
65 expect(parseArgs(["--github-output", "1.2.3"])).toEqual({
66 version: "1.2.3",
67 rootDir: undefined,
68 writeGithubOutput: true,
69 });
70 });
82 const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir });
83 const versions = yield* readReleaseVersions(baseDir);
84
85 assert.deepStrictEqual(result, { changed: true });
86 assert.deepStrictEqual(
87 Array.from(versions.entries()),
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 );
71213});
scripts/update-release-package-versions.ts+67-72
......@@ -1,8 +1,9 @@
1import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2import { resolve } from "node:path";
3import { fileURLToPath } from "node:url";
1#!/usr/bin/env node
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
78export const releasePackageFiles = [
89 "apps/server/package.json",
......@@ -12,88 +13,82 @@ export const releasePackageFiles = [
1213] as const;
1314
1415interface UpdateReleasePackageVersionsOptions {
15 readonly rootDir?: string;
16 readonly rootDir?: string | undefined;
1617}
1718
18interface MutablePackageJson {
19 version?: string;
20 [key: string]: unknown;
21}
22
23export function updateReleasePackageVersions(
19const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
20const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
21 SchemaGetter.stringifyJson({ space: 2 }),
22);
23const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
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* (
2433 version: string,
2534 options: UpdateReleasePackageVersionsOptions = {},
26): { changed: boolean } {
27 const rootDir = resolve(options.rootDir ?? process.cwd());
35) {
36 const fs = yield* FileSystem.FileSystem;
37 const path = yield* Path.Path;
38 const rootDir = path.resolve(options.rootDir ?? process.cwd());
2839 let changed = false;
2940
3041 for (const relativePath of releasePackageFiles) {
31 const filePath = resolve(rootDir, relativePath);
32 const packageJson = JSON.parse(readFileSync(filePath, "utf8")) as MutablePackageJson;
42 const filePath = path.join(rootDir, relativePath);
43 const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson));
3344 if (packageJson.version === version) {
3445 continue;
3546 }
3647
37 packageJson.version = version;
38 writeFileSync(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
48 yield* fs.writeFileString(filePath, `${encodePackageJson({ ...packageJson, version })}\n`);
3949 changed = true;
4050 }
4151
4252 return { changed };
43}
44
45export function parseArgs(argv: ReadonlyArray<string>): {
46 version: string;
47 rootDir: string | undefined;
48 writeGithubOutput: boolean;
49} {
50 const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] });
51
52 const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root");
53 if (unknownFlags.length > 0) {
54 throw new Error(`Unknown argument: --${unknownFlags[0]}`);
55 }
56
57 if ("root" in flags && flags.root === null) {
58 throw new Error("Missing value for --root.");
59 }
60
61 if (positionals.length > 1) {
62 throw new Error("Only one release version can be provided.");
63 }
64
65 if (positionals.length !== 1 || !positionals[0]) {
66 throw new Error(
67 "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",
68 );
69 }
70
71 return {
72 version: positionals[0],
73 rootDir: flags.root ?? undefined,
74 writeGithubOutput: "github-output" in flags,
75 };
76}
77
78const isMain =
79 process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
80
81if (isMain) {
82 const { version, rootDir, writeGithubOutput } = parseArgs(process.argv.slice(2));
83 const { changed } = updateReleasePackageVersions(
84 version,
85 rootDir === undefined ? {} : { rootDir },
53});
54
55const writeGithubOutput = Effect.fn("writeGithubOutput")(function* (changed: boolean) {
56 const fs = yield* FileSystem.FileSystem;
57 const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT");
58 yield* fs.writeFileString(githubOutputPath, `changed=${changed}\n`, { flag: "a" });
59});
60
61export const updateReleasePackageVersionsCommand = Command.make(
62 "update-release-package-versions",
63 {
64 version: Argument.string("version").pipe(
65 Argument.withDescription("Release version to write into each releasable package.json."),
66 ),
67 root: Flag.string("root").pipe(
68 Flag.withDescription("Workspace root used to resolve the release package manifests."),
69 Flag.optional,
70 ),
71 githubOutput: Flag.boolean("github-output").pipe(
72 Flag.withDescription("Append changed=<boolean> to GITHUB_OUTPUT."),
73 Flag.withDefault(false),
74 ),
75 },
76 ({ version, root, githubOutput }) =>
77 updateReleasePackageVersions(version, {
78 rootDir: Option.getOrUndefined(root),
79 }).pipe(
80 Effect.tap(({ changed }) =>
81 changed
82 ? Effect.void
83 : Console.log("All package.json versions already match release version."),
84 ),
85 Effect.tap(({ changed }) => (githubOutput ? writeGithubOutput(changed) : Effect.void)),
86 ),
87).pipe(Command.withDescription("Update release package versions across the workspace."));
88
89if (import.meta.main) {
90 Command.run(updateReleasePackageVersionsCommand, { version: "0.0.0" }).pipe(
91 Effect.provide(NodeServices.layer),
92 NodeRuntime.runMain,
8693 );
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 }
9994}
tsconfig.base.json+8-3
......@@ -1,8 +1,13 @@
11{
22 "compilerOptions": {
3 "target": "ES2023",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
3 "target": "ESNext",
4 "module": "NodeNext",
5 "moduleResolution": "NodeNext",
6 "noEmit": true,
7 "allowImportingTsExtensions": true,
8 "rewriteRelativeImportExtensions": true,
9 "erasableSyntaxOnly": true,
10 "verbatimModuleSyntax": true,
611 "strict": true,
712 "noUncheckedIndexedAccess": true,
813 "exactOptionalPropertyTypes": true,