mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 03:41:17 +02:00
329 lines
9.9 KiB
JavaScript
329 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
import * as Console from "effect/Console";
|
|
import * as Effect from "effect/Effect";
|
|
import * as FileSystem from "effect/FileSystem";
|
|
import * as Option from "effect/Option";
|
|
import * as Path from "effect/Path";
|
|
import * as Schema from "effect/Schema";
|
|
import * as Stream from "effect/Stream";
|
|
import { Command, Flag } from "effect/cli";
|
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
import { fromYaml } from "@t3tools/shared/schemaYaml";
|
|
|
|
import { referenceRepos, type ReferenceRepo } from "./lib/reference-repos.ts";
|
|
|
|
export type ReferenceRepoSyncAction = "add" | "pull";
|
|
|
|
export interface ReferenceRepoSyncOptions {
|
|
readonly rootDir?: string | undefined;
|
|
readonly repoId?: string | undefined;
|
|
readonly latest?: boolean | undefined;
|
|
readonly dryRun?: boolean | undefined;
|
|
}
|
|
|
|
export interface ReferenceRepoSyncPlan {
|
|
readonly repo: ReferenceRepo;
|
|
readonly action: ReferenceRepoSyncAction;
|
|
readonly ref: string;
|
|
readonly args: ReadonlyArray<string>;
|
|
}
|
|
|
|
export class ReferenceRepoSelectionError extends Schema.TaggedError<ReferenceRepoSelectionError>()(
|
|
"ReferenceRepoSelectionError",
|
|
{
|
|
repoId: Schema.String,
|
|
expectedRepoIds: Schema.Array(Schema.String),
|
|
},
|
|
) {
|
|
override get message(): string {
|
|
return `Unknown reference repo "${this.repoId}". Expected one of: ${this.expectedRepoIds.join(", ")}.`;
|
|
}
|
|
}
|
|
|
|
export class ReferenceRepoVersionSourceError extends Schema.TaggedError<ReferenceRepoVersionSourceError>()(
|
|
"ReferenceRepoVersionSourceError",
|
|
{
|
|
operation: Schema.Literals(["read", "parse"]),
|
|
repoId: Schema.String,
|
|
sourcePath: Schema.String,
|
|
cause: Schema.Defect(),
|
|
},
|
|
) {
|
|
override get message(): string {
|
|
return `Reference repo "${this.repoId}" version source operation "${this.operation}" failed for ${this.sourcePath}.`;
|
|
}
|
|
}
|
|
|
|
export class ReferenceRepoVersionResolutionError extends Schema.TaggedError<ReferenceRepoVersionResolutionError>()(
|
|
"ReferenceRepoVersionResolutionError",
|
|
{
|
|
repoId: Schema.String,
|
|
sourcePath: Schema.String,
|
|
packageVersionPath: Schema.Array(Schema.String),
|
|
},
|
|
) {
|
|
override get message(): string {
|
|
return `No version was found for reference repo "${this.repoId}" at ${this.sourcePath}:${this.packageVersionPath.join(".")}.`;
|
|
}
|
|
}
|
|
|
|
export class ReferenceRepoGitSubtreeError extends Schema.TaggedError<ReferenceRepoGitSubtreeError>()(
|
|
"ReferenceRepoGitSubtreeError",
|
|
{
|
|
operation: Schema.Literals(["spawn", "communicate", "exit"]),
|
|
repoId: Schema.String,
|
|
action: Schema.Literals(["add", "pull"]),
|
|
repository: Schema.String,
|
|
ref: Schema.String,
|
|
rootDir: Schema.String,
|
|
argumentCount: Schema.Number,
|
|
exitCode: Schema.optional(Schema.Number),
|
|
stdoutLength: Schema.optional(Schema.Number),
|
|
stderrLength: Schema.optional(Schema.Number),
|
|
cause: Schema.optional(Schema.Defect()),
|
|
},
|
|
) {
|
|
override get message(): string {
|
|
return `Git subtree ${this.action} for reference repo "${this.repoId}" failed during "${this.operation}".`;
|
|
}
|
|
}
|
|
|
|
export type ReferenceRepoSyncError =
|
|
| ReferenceRepoSelectionError
|
|
| ReferenceRepoVersionSourceError
|
|
| ReferenceRepoVersionResolutionError
|
|
| ReferenceRepoGitSubtreeError;
|
|
|
|
const decodeJsonSource = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
|
|
const decodeYamlSource = Schema.decodeEffect(fromYaml(Schema.Unknown));
|
|
|
|
const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
|
|
stream.pipe(
|
|
Stream.decodeText(),
|
|
Stream.runFold(
|
|
() => "",
|
|
(acc, chunk) => acc + chunk,
|
|
),
|
|
);
|
|
|
|
function readNestedString(input: unknown, keys: ReadonlyArray<string>): string | undefined {
|
|
let value = input;
|
|
for (const key of keys) {
|
|
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
return undefined;
|
|
}
|
|
value = (value as Record<string, unknown>)[key];
|
|
}
|
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
}
|
|
|
|
function decodeVersionSource(
|
|
repo: ReferenceRepo,
|
|
sourcePath: string,
|
|
content: string,
|
|
): Effect.Effect<unknown, ReferenceRepoSyncError> {
|
|
const decode =
|
|
repo.versionSourcePath.endsWith(".yaml") || repo.versionSourcePath.endsWith(".yml")
|
|
? decodeYamlSource
|
|
: decodeJsonSource;
|
|
return decode(content).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new ReferenceRepoVersionSourceError({
|
|
operation: "parse",
|
|
repoId: repo.id,
|
|
sourcePath,
|
|
cause,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function getSelectedRepos(
|
|
repoId: string | undefined,
|
|
): Effect.Effect<ReadonlyArray<ReferenceRepo>, ReferenceRepoSyncError> {
|
|
if (!repoId) {
|
|
return Effect.succeed(referenceRepos);
|
|
}
|
|
|
|
const repo = referenceRepos.find((candidate) => candidate.id === repoId);
|
|
return repo
|
|
? Effect.succeed([repo])
|
|
: Effect.fail(
|
|
new ReferenceRepoSelectionError({
|
|
repoId,
|
|
expectedRepoIds: referenceRepos.map((candidate) => candidate.id),
|
|
}),
|
|
);
|
|
}
|
|
|
|
export const resolveReferenceRepoRef = Effect.fn("resolveReferenceRepoRef")(function* (
|
|
repo: ReferenceRepo,
|
|
rootDir: string,
|
|
latest: boolean,
|
|
) {
|
|
if (latest) {
|
|
return repo.latestRef;
|
|
}
|
|
|
|
const fs = yield* FileSystem.FileSystem;
|
|
const path = yield* Path.Path;
|
|
const versionSourcePath = path.join(rootDir, repo.versionSourcePath);
|
|
const versionSourceContent = yield* fs.readFileString(versionSourcePath).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new ReferenceRepoVersionSourceError({
|
|
operation: "read",
|
|
repoId: repo.id,
|
|
sourcePath: versionSourcePath,
|
|
cause,
|
|
}),
|
|
),
|
|
);
|
|
const versionSource = yield* decodeVersionSource(repo, versionSourcePath, versionSourceContent);
|
|
const version = readNestedString(versionSource, repo.packageVersionPath);
|
|
|
|
if (!version) {
|
|
return yield* new ReferenceRepoVersionResolutionError({
|
|
repoId: repo.id,
|
|
sourcePath: versionSourcePath,
|
|
packageVersionPath: repo.packageVersionPath,
|
|
});
|
|
}
|
|
|
|
return `${repo.versionTagPrefix}${version}`;
|
|
});
|
|
|
|
export const planReferenceRepoSync = Effect.fn("planReferenceRepoSync")(function* (
|
|
repo: ReferenceRepo,
|
|
rootDir: string,
|
|
latest: boolean,
|
|
) {
|
|
const fs = yield* FileSystem.FileSystem;
|
|
const path = yield* Path.Path;
|
|
const action: ReferenceRepoSyncAction = (yield* fs.exists(path.join(rootDir, repo.prefix)))
|
|
? "pull"
|
|
: "add";
|
|
const ref = yield* resolveReferenceRepoRef(repo, rootDir, latest);
|
|
|
|
return {
|
|
repo,
|
|
action,
|
|
ref,
|
|
args: ["subtree", action, `--prefix=${repo.prefix}`, repo.repository, ref, "--squash"],
|
|
} satisfies ReferenceRepoSyncPlan;
|
|
});
|
|
|
|
const runGit = Effect.fn("runGit")(function* (rootDir: string, plan: ReferenceRepoSyncPlan) {
|
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
const errorContext = {
|
|
repoId: plan.repo.id,
|
|
action: plan.action,
|
|
repository: plan.repo.repository,
|
|
ref: plan.ref,
|
|
rootDir,
|
|
argumentCount: plan.args.length,
|
|
} as const;
|
|
const child = yield* spawner.spawn(ChildProcess.make("git", plan.args, { cwd: rootDir })).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new ReferenceRepoGitSubtreeError({
|
|
...errorContext,
|
|
operation: "spawn",
|
|
cause,
|
|
}),
|
|
),
|
|
);
|
|
const [stdout, stderr, exitCode] = yield* Effect.all(
|
|
[
|
|
collectStreamAsString(child.stdout),
|
|
collectStreamAsString(child.stderr),
|
|
child.exitCode.pipe(Effect.map(Number)),
|
|
],
|
|
{ concurrency: "unbounded" },
|
|
).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new ReferenceRepoGitSubtreeError({
|
|
...errorContext,
|
|
operation: "communicate",
|
|
cause,
|
|
}),
|
|
),
|
|
);
|
|
|
|
if (exitCode !== 0) {
|
|
return yield* new ReferenceRepoGitSubtreeError({
|
|
...errorContext,
|
|
operation: "exit",
|
|
exitCode,
|
|
stdoutLength: stdout.length,
|
|
stderrLength: stderr.length,
|
|
});
|
|
}
|
|
|
|
if (stdout.trim().length > 0) {
|
|
yield* Console.log(stdout.trim());
|
|
}
|
|
});
|
|
|
|
export const syncReferenceRepos = Effect.fn("syncReferenceRepos")(function* (
|
|
options: ReferenceRepoSyncOptions = {},
|
|
) {
|
|
const path = yield* Path.Path;
|
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
const repos = yield* getSelectedRepos(options.repoId);
|
|
const plans: Array<ReferenceRepoSyncPlan> = [];
|
|
|
|
for (const repo of repos) {
|
|
const plan = yield* planReferenceRepoSync(repo, rootDir, options.latest ?? false);
|
|
plans.push(plan);
|
|
yield* Console.log(`Syncing ${repo.id} from ${plan.ref} with git subtree ${plan.action}.`);
|
|
if (!(options.dryRun ?? false)) {
|
|
yield* runGit(rootDir, plan).pipe(Effect.scoped);
|
|
}
|
|
}
|
|
|
|
return plans;
|
|
});
|
|
|
|
export const syncReferenceReposCommand = Command.make(
|
|
"sync-reference-repos",
|
|
{
|
|
repo: Flag.String("repo").pipe(
|
|
Flag.withDescription("Sync only the named reference repo. Defaults to all configured repos."),
|
|
Flag.optional,
|
|
),
|
|
latest: Flag.Boolean("latest").pipe(
|
|
Flag.withDescription(
|
|
"Sync each repo from its latest branch instead of the installed version.",
|
|
),
|
|
Flag.withDefault(false),
|
|
),
|
|
root: Flag.String("root").pipe(
|
|
Flag.withDescription("Workspace root used to resolve versions and subtree prefixes."),
|
|
Flag.optional,
|
|
),
|
|
dryRun: Flag.Boolean("dry-run").pipe(
|
|
Flag.withDescription("Print planned subtree operations without running git."),
|
|
Flag.withDefault(false),
|
|
),
|
|
},
|
|
({ repo, latest, root, dryRun }) =>
|
|
syncReferenceRepos({
|
|
repoId: Option.getOrUndefined(repo),
|
|
rootDir: Option.getOrUndefined(root),
|
|
latest,
|
|
dryRun,
|
|
}),
|
|
).pipe(Command.withDescription("Sync vendored reference repositories under .repos/."));
|
|
|
|
if (import.meta.main) {
|
|
Command.run(syncReferenceReposCommand, { version: "0.0.0" }).pipe(
|
|
Effect.provide(NodeServices.layer),
|
|
NodeRuntime.runMain,
|
|
);
|
|
}
|