UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

219 lines (184 loc) 7.92 kB
import { $inject, $state, AlephaError, z } from "alepha"; import { PackageManagerUtils } from "alepha/cli"; import { $command } from "alepha/command"; import { $logger, ConsoleColorProvider } from "alepha/logger"; import { vendorOptions } from "../atoms/vendorOptions.ts"; import type { VendorDiffResult, VendorPackageDiff, VendorSyncResult, } from "../services/VendorService.ts"; import { VendorService } from "../services/VendorService.ts"; /** * Default remote when none is configured. The HTTPS URL is used so anyone * (CI runners, AI agents, contributors without SSH keys) can clone without * extra setup. */ const DEFAULT_REMOTE = "https://github.com/feunard/alepha"; export class VendorCommand { protected readonly log = $logger(); protected readonly options = $state(vendorOptions); protected readonly vendorService = $inject(VendorService); protected readonly color = $inject(ConsoleColorProvider); protected readonly pm = $inject(PackageManagerUtils); /** * Ensure vendor config is present and return resolved options. */ protected resolveOptions() { if (!this.options) { throw new AlephaError( 'Missing vendor configuration. Add a "vendor" section to alepha.config.ts.', ); } return { remote: this.options.remote ?? DEFAULT_REMOTE, branch: this.options.branch ?? "main", dir: this.options.dir ?? ".vendor", packages: this.options.packages, }; } // ───────────────────────────────────────────────────────────────────────── // alepha vendor sync // ───────────────────────────────────────────────────────────────────────── protected readonly syncFlags = z.object({ force: z .boolean() .meta({ aliases: ["f"] }) .describe("Skip local modification check") .optional(), remote: z .text({ description: "Override the configured remote for this invocation. Accepts any git-clone URL, including local paths (`file:///abs/path/to/alepha`). Useful for CI canaries that need to sync against a local checkout instead of the published repo.", }) .optional(), }); protected readonly sync = $command({ name: "sync", description: "Replace local packages with remote source", flags: this.syncFlags, handler: async ({ flags, root, run }) => { const opts = this.resolveOptions(); const remote = flags.remote ?? opts.remote; const c = this.color; let result: VendorSyncResult = { synced: [], errors: [] }; await run({ name: `Syncing from ${opts.branch}`, handler: async () => { result = await this.vendorService.sync({ root, remote, branch: opts.branch, dir: opts.dir, packages: opts.packages, force: flags.force, }); }, }); if (result.aborted) { run.end(); process.stdout.write( `\nLocal modifications detected. Use ${c.set("CYAN", "--force")} to overwrite.\n`, ); for (const pkg of result.aborted.packages) { this.printPackageDiff(pkg); } process.stdout.write("\n"); return; } if (result.synced.length > 0) { const pmName = await this.pm.getPackageManager(root); await run(`${pmName} install`, { root }); } run.end(); if (result.errors.length > 0) { for (const error of result.errors) { process.stdout.write(`${c.set("RED", " error")} ${error}\n`); } } if (result.synced.length > 0) { process.stdout.write( `\nSynced ${c.set("CYAN", String(result.synced.length))} ${result.synced.length === 1 ? "package" : "packages"} from ${c.set("CYAN", opts.branch)}\n`, ); for (const pkg of result.synced) { process.stdout.write(` ${c.set("GREEN", "\u2713")} ${pkg}\n`); } } process.stdout.write("\n"); }, }); // ───────────────────────────────────────────────────────────────────────── // alepha vendor diff // ───────────────────────────────────────────────────────────────────────── protected readonly diff = $command({ name: "diff", description: "Compare local packages against remote", handler: async ({ root, run }) => { const opts = this.resolveOptions(); let result: VendorDiffResult = { packages: [], totalChanges: 0 }; await run({ name: `Cloning ${opts.remote} at ${opts.branch}`, handler: async () => { result = await this.vendorService.diff({ root, remote: opts.remote, branch: opts.branch, dir: opts.dir, packages: opts.packages, }); }, }); run.end(); if (result.totalChanges === 0) { process.stdout.write("\nNo changes\n\n"); return; } for (const pkg of result.packages) { this.printPackageDiff(pkg); } process.stdout.write("\n"); }, }); // ───────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────── protected printPackageDiff(pkg: VendorPackageDiff) { const c = this.color; const count = pkg.added.length + pkg.modified.length + pkg.removed.length; if (count === 0) { process.stdout.write(`\n${c.set("CYAN", pkg.name)}: no changes\n`); return; } process.stdout.write( `\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`, ); for (const file of pkg.added) { process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`); } for (const fileDiff of pkg.modified) { process.stdout.write(` ${c.set("ORANGE", "M")} ${fileDiff.file}\n`); for (const change of fileDiff.changes) { const prefix = change.type === "removed" ? "-" : "+"; const color = change.type === "removed" ? "RED" : "GREEN"; const lineNum = `L${change.line}`; process.stdout.write( ` ${c.set("DIM", lineNum.padEnd(5))} ${c.set(color, `${prefix} ${change.text}`)}\n`, ); } } for (const file of pkg.removed) { process.stdout.write(` ${c.set("RED", "D")} ${file}\n`); } } // ───────────────────────────────────────────────────────────────────────── // Parent command // ───────────────────────────────────────────────────────────────────────── public readonly vendor = $command({ name: "vendor", description: "Vendor Alepha packages into the project", children: [this.sync, this.diff], handler: async ({ help }) => { help(); }, }); }