alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
219 lines (184 loc) • 7.92 kB
text/typescript
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();
},
});
}