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