mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
251 lines
9.6 KiB
JavaScript
251 lines
9.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
import * as Effect from "effect/Effect";
|
|
import * as FileSystem from "effect/FileSystem";
|
|
import * as Logger from "effect/Logger";
|
|
import * as Option from "effect/Option";
|
|
import * as Path from "effect/Path";
|
|
import { Command, Flag } from "effect/cli";
|
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
|
|
import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";
|
|
import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-executable-imports.ts";
|
|
import { resolveSpawnCommand } from "@t3tools/shared/shell";
|
|
import {
|
|
ServerCliBuildAssetMissingError,
|
|
ServerCliCommandExitError,
|
|
ServerCliDevelopmentIconSourceMissingError,
|
|
ServerCliDevelopmentIconTargetMissingError,
|
|
ServerCliExecutableImportError,
|
|
} from "./cliErrors.ts";
|
|
import { publishPlatformsThenLauncher } from "./publishOrder.ts";
|
|
|
|
const RepoRoot = Effect.service(Path.Path).pipe(
|
|
Effect.flatMap((path) => path.fromFileUrl(new URL("../../..", import.meta.url))),
|
|
);
|
|
|
|
const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.StandardCommand) {
|
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
const child = yield* spawner.spawn(command);
|
|
const exitCode = yield* child.exitCode;
|
|
|
|
if (exitCode !== 0) {
|
|
return yield* new ServerCliCommandExitError({
|
|
command: command.command,
|
|
args: command.args,
|
|
cwd: command.options.cwd,
|
|
exitCode,
|
|
});
|
|
}
|
|
});
|
|
|
|
const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* (
|
|
repoRoot: string,
|
|
serverDir: string,
|
|
) {
|
|
const path = yield* Path.Path;
|
|
const fs = yield* FileSystem.FileSystem;
|
|
|
|
for (const override of DEVELOPMENT_ICON_OVERRIDES) {
|
|
const sourcePath = path.join(repoRoot, override.sourceRelativePath);
|
|
const targetPath = path.join(serverDir, override.targetRelativePath);
|
|
|
|
if (!(yield* fs.exists(sourcePath))) {
|
|
return yield* new ServerCliDevelopmentIconSourceMissingError({ sourcePath });
|
|
}
|
|
if (!(yield* fs.exists(targetPath))) {
|
|
return yield* new ServerCliDevelopmentIconTargetMissingError({ targetPath });
|
|
}
|
|
|
|
yield* fs.copyFile(sourcePath, targetPath);
|
|
}
|
|
|
|
yield* Effect.log("[cli] Applied development icon overrides to dist/client");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// build subcommand
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const buildCmd = Command.make(
|
|
"build",
|
|
{
|
|
verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)),
|
|
},
|
|
(config) =>
|
|
Effect.gen(function* () {
|
|
const path = yield* Path.Path;
|
|
const fs = yield* FileSystem.FileSystem;
|
|
const repoRoot = yield* RepoRoot;
|
|
const serverDir = path.join(repoRoot, "apps/server");
|
|
|
|
yield* Effect.log("[cli] Running tsdown...");
|
|
yield* runCommand(
|
|
ChildProcess.make(process.execPath, ["--run", "build:bundle"], {
|
|
cwd: serverDir,
|
|
stdout: config.verbose ? "inherit" : "ignore",
|
|
stderr: "inherit",
|
|
shell: false,
|
|
}),
|
|
);
|
|
|
|
const webDist = path.join(repoRoot, "apps/web/dist");
|
|
const clientTarget = path.join(serverDir, "dist/client");
|
|
|
|
if (yield* fs.exists(webDist)) {
|
|
yield* fs.copy(webDist, clientTarget);
|
|
yield* applyDevelopmentIconOverrides(repoRoot, serverDir);
|
|
yield* Effect.log("[cli] Bundled web app into dist/client");
|
|
} else {
|
|
yield* Effect.logWarning("[cli] Web dist not found — skipping client bundle.");
|
|
}
|
|
}),
|
|
).pipe(Command.withDescription("Build the server package (tsdown + bundle web client)."));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// build-exe subcommand
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const buildExeCmd = Command.make(
|
|
"build-exe",
|
|
{
|
|
verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)),
|
|
target: Flag.String("target").pipe(
|
|
Flag.withDescription(
|
|
"Cross-build for <platform>-<arch> in nodejs.org naming (for example darwin-x64); defaults to the host.",
|
|
),
|
|
Flag.optional,
|
|
),
|
|
},
|
|
(config) =>
|
|
Effect.gen(function* () {
|
|
const path = yield* Path.Path;
|
|
const fs = yield* FileSystem.FileSystem;
|
|
const repoRoot = yield* RepoRoot;
|
|
const serverDir = path.join(repoRoot, "apps/server");
|
|
|
|
yield* Effect.log("[cli] Building single-executable...");
|
|
const spawnCommand = yield* resolveSpawnCommand("vp", ["pack"]);
|
|
yield* runCommand(
|
|
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
|
|
cwd: serverDir,
|
|
env: {
|
|
...process.env,
|
|
T3CODE_PACK_EXE: "1",
|
|
...Option.match(config.target, {
|
|
onNone: () => ({}),
|
|
onSome: (target) => ({ T3CODE_PACK_EXE_TARGET: target }),
|
|
}),
|
|
},
|
|
stdout: config.verbose ? "inherit" : "ignore",
|
|
stderr: "inherit",
|
|
shell: spawnCommand.shell,
|
|
}),
|
|
);
|
|
|
|
// The executable can only `import` built-ins. A file-backed import
|
|
// passes the bundler and `node dist/bin.mjs`, then throws inside the
|
|
// binary, so read the emitted module graph rather than trusting config.
|
|
const bundlePath = path.join(serverDir, "dist-exe/bin.mjs");
|
|
const specifiers = findEsmImportsOfExternalPackages(yield* fs.readFileString(bundlePath));
|
|
if (specifiers.length > 0) {
|
|
return yield* new ServerCliExecutableImportError({ bundlePath, specifiers });
|
|
}
|
|
yield* Effect.log(
|
|
"[cli] Built dist-exe/t3 (expects client/, resource-monitor/, and the runtime-external node_modules beside it; scripts/build-cli-archive.ts assembles that tree)",
|
|
);
|
|
}),
|
|
).pipe(
|
|
Command.withDescription(
|
|
"Build the server as a Node single-executable (needs a Node 25.7+ host for --build-sea). The binary still resolves native packages from a node_modules tree beside it.",
|
|
),
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// publish subcommand
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Publishes the tarballs scripts/build-npm-platform-packages.ts produced:
|
|
* every `@t3code/t3-<platform>.tgz` first, `t3.tgz` (the launcher) last, so
|
|
* the launcher is never installable before the executables it depends on.
|
|
* Tarballs rather than directories because `npm publish <dir>` strips the
|
|
* `node_modules/` the executable loads its native addons from.
|
|
*/
|
|
const publishCmd = Command.make(
|
|
"publish",
|
|
{
|
|
packagesDir: Flag.String("packages-dir").pipe(
|
|
Flag.withDescription("Output dir of scripts/build-npm-platform-packages.ts."),
|
|
),
|
|
tag: Flag.String("tag").pipe(Flag.withDefault("latest")),
|
|
access: Flag.String("access").pipe(Flag.withDefault("public")),
|
|
provenance: Flag.Boolean("provenance").pipe(Flag.withDefault(false)),
|
|
dryRun: Flag.Boolean("dry-run").pipe(Flag.withDefault(false)),
|
|
verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)),
|
|
},
|
|
(config) =>
|
|
Effect.gen(function* () {
|
|
const path = yield* Path.Path;
|
|
const fs = yield* FileSystem.FileSystem;
|
|
// npm runs with cwd set to the packages dir below, so tarball paths are
|
|
// resolved once here rather than joined twice.
|
|
const packagesDir = path.resolve(config.packagesDir);
|
|
const scopeDir = path.join(packagesDir, "@t3code");
|
|
const launcherTarball = path.join(packagesDir, "t3.tgz");
|
|
const platformTarballs = (yield* fs
|
|
.readDirectory(scopeDir)
|
|
.pipe(Effect.orElseSucceed((): ReadonlyArray<string> => [])))
|
|
.filter((entry) => entry.startsWith("t3-") && entry.endsWith(".tgz"))
|
|
.sort()
|
|
.map((entry) => path.join(scopeDir, entry));
|
|
if (platformTarballs.length === 0) {
|
|
return yield* new ServerCliBuildAssetMissingError({
|
|
assetPath: path.join(scopeDir, "t3-<platform>.tgz"),
|
|
});
|
|
}
|
|
if (!(yield* fs.exists(launcherTarball))) {
|
|
return yield* new ServerCliBuildAssetMissingError({ assetPath: launcherTarball });
|
|
}
|
|
|
|
const args = ["publish", "--access", config.access, "--tag", config.tag];
|
|
if (config.provenance) args.push("--provenance");
|
|
if (config.dryRun) args.push("--dry-run");
|
|
|
|
const publish = Effect.fn("publish")(function* (tarball: string) {
|
|
const spawnCommand = yield* resolveSpawnCommand("npm", [...args, tarball]);
|
|
yield* Effect.log(`[cli] npm ${args.join(" ")} ${path.basename(tarball)}`);
|
|
yield* runCommand(
|
|
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
|
|
cwd: packagesDir,
|
|
stdout: config.verbose ? "inherit" : "ignore",
|
|
stderr: "inherit",
|
|
shell: spawnCommand.shell,
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Each publish takes about 17s, so the platform packages go at once.
|
|
yield* publishPlatformsThenLauncher({ platformTarballs, launcherTarball, publish });
|
|
}),
|
|
).pipe(
|
|
Command.withDescription(
|
|
"Publish the @t3code/t3-<platform> tarballs and then the t3 launcher to npm.",
|
|
),
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// root command
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const cli = Command.make("cli").pipe(
|
|
Command.withDescription("T3 server build & publish CLI."),
|
|
Command.withSubcommands([buildCmd, buildExeCmd, publishCmd]),
|
|
);
|
|
|
|
Command.run(cli, { version: "0.0.0" }).pipe(
|
|
Effect.scoped,
|
|
Effect.provide([Logger.layer([Logger.consolePretty()]), NodeServices.layer]),
|
|
NodeRuntime.runMain,
|
|
);
|