From 66738b4f0323a3f867829e7ad1e351300d3acc90 Mon Sep 17 00:00:00 2001 From: Victor Vogel <263261067+only21mil@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:11:30 -0500 Subject: [PATCH] fix(server): preserve resource monitor executable modes Signed-off-by: Victor Vogel <263261067+only21mil@users.noreply.github.com> --- apps/server/package.json | 8 ++ apps/server/scripts/cli.ts | 41 ++------ apps/server/scripts/cliManifest.test.ts | 126 ++++++++++++++++++++++++ apps/server/scripts/cliManifest.ts | 50 ++++++++++ 4 files changed, 190 insertions(+), 35 deletions(-) create mode 100644 apps/server/scripts/cliManifest.test.ts create mode 100644 apps/server/scripts/cliManifest.ts diff --git a/apps/server/package.json b/apps/server/package.json index 4d17229cd3af..664357b87282 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -14,6 +14,14 @@ "dist" ], "type": "module", + "publishConfig": { + "executableFiles": [ + "dist/resource-monitor/darwin-arm64/t3-resource-monitor", + "dist/resource-monitor/darwin-x64/t3-resource-monitor", + "dist/resource-monitor/linux-x64/t3-resource-monitor", + "dist/resource-monitor/win32-x64/t3-resource-monitor.exe" + ] + }, "scripts": { "dev": "node --watch src/bin.ts", "build:bundle": "vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean", diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index 2de5b702a286..fa6b9188809a 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -15,7 +15,6 @@ import { resolveWebAssetBrandForPackageVersion, resolveWebIconOverrides, } from "../../../scripts/lib/brand-assets.ts"; -import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; import { fromYaml } from "@t3tools/shared/schemaYaml"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; @@ -28,22 +27,7 @@ import { ServerCliPublishIconSourceMissingError, ServerCliPublishIconTargetMissingError, } from "./cliErrors.ts"; - -interface PackageJson { - name: string; - repository: { - type: string; - url: string; - directory: string; - }; - bin: Record; - type: string; - version: string; - engines: Record; - files: string[]; - dependencies: Record; - overrides: Record; -} +import { createServerCliPublishPackageJson } from "./cliManifest.ts"; const PackageJsonPrettyJson = fromJsonStringPretty(Schema.Unknown); const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson); @@ -241,25 +225,12 @@ const publishCmd = Command.make( const workspaceConfig = yield* readWorkspaceConfig(); const workspaceCatalog = workspaceConfig.catalog ?? {}; const workspaceOverrides = workspaceConfig.overrides ?? {}; - const pkg: PackageJson = { - name: serverPackageJson.name, - repository: serverPackageJson.repository, - bin: serverPackageJson.bin, - type: serverPackageJson.type, + const pkg = createServerCliPublishPackageJson({ + source: serverPackageJson, version, - engines: serverPackageJson.engines, - files: serverPackageJson.files, - dependencies: resolveCatalogDependencies( - serverPackageJson.dependencies, - workspaceCatalog, - "apps/server", - ), - overrides: resolveCatalogDependencies( - workspaceOverrides, - workspaceCatalog, - "apps/server", - ), - }; + workspaceCatalog, + workspaceOverrides, + }); return { packageJsonString: yield* encodePackageJson(pkg), diff --git a/apps/server/scripts/cliManifest.test.ts b/apps/server/scripts/cliManifest.test.ts new file mode 100644 index 000000000000..1404a3e0efe4 --- /dev/null +++ b/apps/server/scripts/cliManifest.test.ts @@ -0,0 +1,126 @@ +import * as NodeProcess from "node:process"; +import * as NodeZlib from "node:zlib"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; + +import serverPackageJson from "../package.json" with { type: "json" }; +import { createServerCliPublishPackageJson } from "./cliManifest.ts"; + +const RESOURCE_MONITOR_EXECUTABLES = [ + "dist/resource-monitor/darwin-arm64/t3-resource-monitor", + "dist/resource-monitor/darwin-x64/t3-resource-monitor", + "dist/resource-monitor/linux-x64/t3-resource-monitor", + "dist/resource-monitor/win32-x64/t3-resource-monitor.exe", +]; +const encodeUnknownJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); + +function readTarField(header: Uint8Array, start: number, end: number): string { + const field = header.subarray(start, end); + const nullOffset = field.indexOf(0); + return field + .subarray(0, nullOffset === -1 ? field.length : nullOffset) + .toString() + .trim(); +} + +function readTarModes(tarball: Uint8Array): ReadonlyMap { + const archive = NodeZlib.gunzipSync(tarball); + const modes = new Map(); + + for (let offset = 0; offset + 512 <= archive.length; ) { + const header = archive.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) break; + + const name = readTarField(header, 0, 100); + const modeText = readTarField(header, 100, 108); + const sizeText = readTarField(header, 124, 136); + const mode = Number.parseInt(modeText, 8); + const size = Number.parseInt(sizeText, 8); + + modes.set(name, mode & 0o777); + offset += 512 + Math.ceil(size / 512) * 512; + } + + return modes; +} + +describe("CLI publish manifest", () => { + it("marks every packaged resource monitor as executable", () => { + assert.deepStrictEqual( + serverPackageJson.publishConfig.executableFiles, + RESOURCE_MONITOR_EXECUTABLES, + ); + }); + + it("retains executable file metadata in the generated publish manifest", () => { + const manifest = createServerCliPublishPackageJson({ + source: { ...serverPackageJson, dependencies: {} }, + version: "1.2.3", + workspaceCatalog: {}, + workspaceOverrides: {}, + }); + + assert.deepStrictEqual(manifest.publishConfig.executableFiles, RESOURCE_MONITOR_EXECUTABLES); + }); + + it.effect("packs every resource monitor with executable mode", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-resource-monitor-pack-", + }); + const outputDir = path.join(fixtureDir, "packed"); + + yield* fileSystem.makeDirectory(outputDir); + yield* fileSystem.writeFileString( + path.join(fixtureDir, "package.json"), + encodeUnknownJson({ + name: "t3-resource-monitor-pack-fixture", + version: "1.0.0", + packageManager: "pnpm@11.10.0", + files: ["dist"], + publishConfig: serverPackageJson.publishConfig, + }), + ); + + for (const executable of RESOURCE_MONITOR_EXECUTABLES) { + const executablePath = path.join(fixtureDir, executable); + yield* fileSystem.makeDirectory(path.dirname(executablePath), { recursive: true }); + yield* fileSystem.writeFileString(executablePath, "fixture\n"); + yield* fileSystem.chmod(executablePath, 0o644); + } + + const vpExecutable = path.resolve( + import.meta.dirname, + "../../../node_modules/vite-plus/bin/vp", + ); + const child = yield* spawner.spawn( + ChildProcess.make( + NodeProcess.execPath, + [vpExecutable, "pm", "pack", "--pack-destination", outputDir], + { + cwd: fixtureDir, + stdout: "inherit", + stderr: "inherit", + }, + ), + ); + assert.equal(yield* child.exitCode, 0); + + const [tarballName] = yield* fileSystem.readDirectory(outputDir); + assert.ok(tarballName); + const modes = readTarModes(yield* fileSystem.readFile(path.join(outputDir, tarballName))); + for (const executable of RESOURCE_MONITOR_EXECUTABLES) { + assert.equal(modes.get(`package/${executable}`), 0o755, executable); + } + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); +}); diff --git a/apps/server/scripts/cliManifest.ts b/apps/server/scripts/cliManifest.ts new file mode 100644 index 000000000000..60464039479a --- /dev/null +++ b/apps/server/scripts/cliManifest.ts @@ -0,0 +1,50 @@ +import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; + +export interface ServerCliPublishPackageJson { + readonly name: string; + readonly repository: { + readonly type: string; + readonly url: string; + readonly directory: string; + }; + readonly bin: Readonly>; + readonly type: string; + readonly version: string; + readonly engines: Readonly>; + readonly files: ReadonlyArray; + readonly dependencies: Readonly>; + readonly overrides: Readonly>; + readonly publishConfig: { + readonly executableFiles: ReadonlyArray; + }; +} + +type ServerCliPackageSource = Omit; + +export function createServerCliPublishPackageJson(input: { + readonly source: ServerCliPackageSource; + readonly version: string; + readonly workspaceCatalog: Record; + readonly workspaceOverrides: Record; +}): ServerCliPublishPackageJson { + return { + name: input.source.name, + repository: input.source.repository, + bin: input.source.bin, + type: input.source.type, + version: input.version, + engines: input.source.engines, + files: input.source.files, + dependencies: resolveCatalogDependencies( + input.source.dependencies, + input.workspaceCatalog, + "apps/server", + ), + overrides: resolveCatalogDependencies( + input.workspaceOverrides, + input.workspaceCatalog, + "apps/server", + ), + publishConfig: input.source.publishConfig, + }; +}