1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3import * as Command from "effect/cli/Command";
4import * as Flag from "effect/cli/Flag";
5
6import * as Drift from "../../Alchemist/routes/drift.ts";
7import { Cli } from "../../Report.ts";
8import * as CliKit from "../CliKit/index.ts";
9import { planDecisionScreen } from "../components/view/PlanDecision.tsx";
10
11import { config, envFile, profile, resolveStage, stage } from "./flags.ts";
12import { instrumentCommand } from "./instrument.ts";
13import { renderApply, renderPlanning } from "./render.ts";
14
15const repairFlag = Flag.Boolean("repair").pipe(
16 Flag.withDescription("Repair detected drift without prompting"),
17 Flag.withDefault(false),
18);
19
20interface SyncArgs {
21 main: string;
22 stage: string;
23 envFile: Option.Option<string>;
24 profile?: string;
25 repair?: boolean;
26}
27
28const routeDrift = Effect.fn(function* ({
29 main,
30 stage,
31 envFile,
32 profile,
33 repair = false,
34}: SyncArgs) {
35 const cli = yield* Cli;
36 const snapshot = yield* Drift.inspect({
37 entrypoint: main,
38 stage,
39 profile,
40 envFile: Option.getOrUndefined(envFile),
41 }).pipe(
42 renderPlanning({
43 operation: "Drift",
44 stage,
45 computingLabel: "Checking drift",
46 readyLabel: "Drift check complete",
47 }),
48 );
49 if (!Drift.hasDrift(snapshot)) {
50 return yield* cli.displayPlan(snapshot.repairPlan.native);
51 }
52
53 if (!repair) {
54 const terminal = yield* CliKit.CliKit;
55 if (!terminal.terminal.input) {
56 return yield* cli.displayPlan(snapshot.repairPlan.native);
57 }
58 const decision = yield* terminal.prompt
59 .custom(
60 planDecisionScreen({
61 plan: snapshot.repairPlan.native,
62 message: "Drift detected",
63 choices: [
64 {
65 value: "repair" as const,
66 label: "Repair",
67 },
68 {
69 value: "cancel" as const,
70 label: "Cancel",
71 },
72 ],
73 initialValue: "cancel" as const,
74 }),
75 )
76 .pipe(
77 Effect.catchTag("TerminalCancelled", () =>
78 Effect.succeed("cancel" as const),
79 ),
80 );
81 if (decision === "cancel") return;
82 }
83
84 yield* Drift.repair(snapshot).pipe(renderApply(snapshot.repairPlan.native));
85});
86
87export const driftCommand = Command.make(
88 "drift",
89 {
90 repair: repairFlag,
91 main: config,
92 envFile,
93 stage,
94 profile,
95 },
96 (args) =>
97 resolveStage("live", args.stage, args.envFile).pipe(
98 Effect.flatMap((stage) =>
99 instrumentCommand("drift", (a: SyncArgs & { repair: boolean }) => ({
100 "alchemy.stage": a.stage,
101 "alchemy.profile": a.profile,
102 "alchemy.main": a.main,
103 "alchemy.repair": a.repair,
104 }))((resolved) => routeDrift(resolved))({ ...args, stage }),
105 ),
106 ),
107).pipe(Command.withDescription("Detect infrastructure drift"));