fleetfizz 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/fleetfizz.mjs +278 -45
  2. package/package.json +1 -1
@@ -7433,6 +7433,8 @@ const onExitFilter = /*#__PURE__*/ dual(3, (self, filter, f) => onExit$1(self, (
7433
7433
  }));
7434
7434
  /** @internal */
7435
7435
  const onError$1 = /*#__PURE__*/ dual(2, (self, f) => onExitFilter(self, exitFilterCause, f));
7436
+ /** @internal */
7437
+ const acquireUseRelease$1 = (acquire, use, release) => uninterruptibleMask$1((restore) => flatMap$2(acquire, (a) => onExitPrimitive$1(suspend$3(() => restore(use(a))), (exit) => release(a, exit), true)));
7436
7438
  const infiniteTTL = /*#__PURE__*/ constant(Infinity);
7437
7439
  const makeCachedUnsafe = (self, ttlMillis) => {
7438
7440
  let expiresAt = 0;
@@ -12690,6 +12692,82 @@ const scopedWith = scopedWith$1;
12690
12692
  */
12691
12693
  const acquireRelease = acquireRelease$1;
12692
12694
  /**
12695
+ * Runs resource acquisition, usage, and release as one bracketed effect.
12696
+ *
12697
+ * **When to use**
12698
+ *
12699
+ * Use to bracket acquire, use, and release logic in one effect.
12700
+ *
12701
+ * **Details**
12702
+ *
12703
+ * `acquireUseRelease` does the following:
12704
+ *
12705
+ * 1. Ensures that the `Effect` value that acquires the resource will not be
12706
+ * interrupted. Note that acquisition may still fail due to internal
12707
+ * reasons (such as an uncaught exception).
12708
+ * 2. Ensures that the `release` `Effect` value will not be interrupted,
12709
+ * and will be executed as long as the acquisition `Effect` value
12710
+ * successfully acquires the resource.
12711
+ *
12712
+ * During the time period between the acquisition and release of the resource,
12713
+ * the `use` `Effect` value will be executed.
12714
+ *
12715
+ * If the `release` `Effect` value fails, then the entire `Effect` value will
12716
+ * fail, even if the `use` `Effect` value succeeds. If this fail-fast behavior
12717
+ * is not desired, errors produced by the `release` `Effect` value can be caught
12718
+ * and ignored.
12719
+ *
12720
+ * **Example** (Acquiring resources with cleanup)
12721
+ *
12722
+ * ```ts import.meta.vitest
12723
+ * import { Effect, Exit } from "effect"
12724
+ * const output: Array<unknown> = []
12725
+ *
12726
+ * interface Database {
12727
+ * readonly connection: string
12728
+ * readonly query: (sql: string) => Effect.Effect<string>
12729
+ * }
12730
+ *
12731
+ * const program = Effect.acquireUseRelease(
12732
+ * // Acquire - connect to database
12733
+ * Effect.gen(function*() {
12734
+ * yield* Effect.sync(() => { output.push("Connecting to database...") })
12735
+ * return {
12736
+ * connection: "db://localhost:5432",
12737
+ * query: (sql: string) => Effect.succeed(`Result for: ${sql}`)
12738
+ * }
12739
+ * }),
12740
+ * // Use - perform database operations
12741
+ * (db) =>
12742
+ * Effect.gen(function*() {
12743
+ * yield* Effect.sync(() => { output.push(`Connected to ${db.connection}`) })
12744
+ * const result = yield* db.query("SELECT * FROM users")
12745
+ * yield* Effect.sync(() => { output.push(`Query result: ${result}`) })
12746
+ * return result
12747
+ * }),
12748
+ * // Release - close database connection
12749
+ * (db, exit) =>
12750
+ * Effect.gen(function*() {
12751
+ * if (Exit.isSuccess(exit)) {
12752
+ * yield* Effect.sync(() => { output.push(`Closing connection to ${db.connection} (success)`) })
12753
+ * } else {
12754
+ * yield* Effect.sync(() => { output.push(`Closing connection to ${db.connection} (failure)`) })
12755
+ * }
12756
+ * })
12757
+ * )
12758
+ *
12759
+ * await Effect.runPromise(program)
12760
+ * output // => ["Connecting to database...", "Connected to db://localhost:5432", "Query result: Result for: SELECT * FROM users", "Closing connection to db://localhost:5432 (success)"]
12761
+ * ```
12762
+ *
12763
+ * @see {@link acquireRelease} for scoped resources whose use happens later
12764
+ *
12765
+ * @stability stable
12766
+ * @category resource management
12767
+ * @since 2.0.0
12768
+ */
12769
+ const acquireUseRelease = acquireUseRelease$1;
12770
+ /**
12693
12771
  * Adds a finalizer to the current scope.
12694
12772
  *
12695
12773
  * **When to use**
@@ -33264,7 +33342,7 @@ const runWith = (command, config) => {
33264
33342
  };
33265
33343
  //#endregion
33266
33344
  //#region package.json
33267
- var version = "0.10.0";
33345
+ var version = "0.11.0";
33268
33346
  //#endregion
33269
33347
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Logger.js
33270
33348
  const TypeId = "~effect/Logger";
@@ -61120,7 +61198,7 @@ if (process$1.env["DEV"] === "true") {
61120
61198
  } catch {}
61121
61199
  if (isDevtoolsInstalled) await init_devtools().then(() => devtools_exports);
61122
61200
  }
61123
- const diff = (before, after) => {
61201
+ const diff$1 = (before, after) => {
61124
61202
  if (before === after) return;
61125
61203
  if (!before) return after;
61126
61204
  const changed = {};
@@ -61295,8 +61373,8 @@ const hostConfig = {
61295
61373
  const rootNode = findRootNode(node);
61296
61374
  if (rootNode) rootNode.isStaticDirty = true;
61297
61375
  }
61298
- const props = diff(oldProps, newProps);
61299
- const style = diff(oldProps["style"], newProps["style"]);
61376
+ const props = diff$1(oldProps, newProps);
61377
+ const style = diff$1(oldProps["style"], newProps["style"]);
61300
61378
  if (!props && !style) return;
61301
61379
  if (props) for (const [key, value] of Object.entries(props)) {
61302
61380
  if (key === "style") {
@@ -65011,6 +65089,14 @@ const useInput = (inputHandler, options = {}) => {
65011
65089
  }, [options.isActive, internal_eventEmitter]);
65012
65090
  };
65013
65091
  //#endregion
65092
+ //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/hooks/use-app.js
65093
+ /**
65094
+ A React hook that returns app lifecycle methods like `exit()` and `waitUntilRenderFlush()`.
65095
+ */
65096
+ function useApp() {
65097
+ return (0, import_react.use)(AppContext);
65098
+ }
65099
+ //#endregion
65014
65100
  //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/hooks/use-stdout.js
65015
65101
  /**
65016
65102
  A React hook that returns the stdout stream where Ink renders your app.
@@ -67680,10 +67766,10 @@ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
67680
67766
  const value = (i) => i.to === void 0 ? i.from ?? "" : i.from === void 0 ? `+ ${i.to}` : i.from === i.to ? i.to : `${i.from} → ${i.to}`;
67681
67767
  const SPINNER = cli_spinners_default.dots;
67682
67768
  const runGlyph = (live, now) => (live ? SPINNER.frames[Math.floor(now / SPINNER.interval) % SPINNER.frames.length] : void 0) ?? "▸";
67683
- const unchanged = (i) => ({
67769
+ const UNCHANGED = {
67684
67770
  text: "",
67685
- detail: i.kind === "record" || i.kind === "config" ? "up to date" : void 0
67686
- });
67771
+ quiet: true
67772
+ };
67687
67773
  const statusOf = (i, dryRun, glyph) => {
67688
67774
  switch (i.status) {
67689
67775
  case "running": return {
@@ -67701,7 +67787,7 @@ const statusOf = (i, dryRun, glyph) => {
67701
67787
  detail: i.detail
67702
67788
  };
67703
67789
  case "ok":
67704
- if (!i.changes) return unchanged(i);
67790
+ if (!i.changes) return UNCHANGED;
67705
67791
  if (i.kind === "config") return {
67706
67792
  text: "✓ copied",
67707
67793
  tone: "ok"
@@ -67723,9 +67809,10 @@ const statusOf = (i, dryRun, glyph) => {
67723
67809
  if (i.changes) return {
67724
67810
  text: dryRun ? "· planned" : "· queued",
67725
67811
  tone: "dim",
67726
- detail: i.reason
67812
+ detail: i.reason,
67813
+ quiet: true
67727
67814
  };
67728
- return unchanged(i);
67815
+ return UNCHANGED;
67729
67816
  }
67730
67817
  };
67731
67818
  const countsOf = (m) => {
@@ -67875,7 +67962,7 @@ const MachineLine = (props) => {
67875
67962
  const ItemLine = (props) => {
67876
67963
  const { i, w } = props;
67877
67964
  const status = statusOf(i, props.dryRun, props.glyph);
67878
- const quiet = i.status === "idle" ? "dim" : void 0;
67965
+ const quiet = status.quiet === true ? "dim" : void 0;
67879
67966
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
67880
67967
  columnGap: GAP,
67881
67968
  children: [
@@ -68446,6 +68533,7 @@ const copiesOf = (checked, from) => {
68446
68533
  return copies;
68447
68534
  };
68448
68535
  const PULL = "pull:";
68536
+ const DIFF = "diff:";
68449
68537
  const askWhy = (current, record, source, from) => {
68450
68538
  if (record === void 0) return "differs, and no Ship wrote it";
68451
68539
  const recorded = recordEntries(record);
@@ -68471,7 +68559,11 @@ const fileQuestion = (subject, found, problem, pull, from) => ({
68471
68559
  title: "skip on these Machines (leave as is, shown as skipped)"
68472
68560
  }
68473
68561
  ],
68474
- default: "skip"
68562
+ default: "skip",
68563
+ ...pull === void 0 || !pull.openssl ? {} : { diffs: [{
68564
+ value: `${DIFF}${pull.machine}`,
68565
+ title: `view the diff from ${pull.machine}'s copy to this Machine's`
68566
+ }] }
68475
68567
  });
68476
68568
  const missingQuestion = (subject, found, copies, from) => ({
68477
68569
  kind: "file",
@@ -68670,16 +68762,12 @@ const copyStep = (change) => {
68670
68762
  input
68671
68763
  };
68672
68764
  };
68673
- const pull = (name, machine, resolved) => gen(function* () {
68674
- const source = resolved.sources.get(name);
68675
- const copy = resolved.copies.get(name)?.find((c) => c.machine === machine);
68676
- if (source === void 0 || copy === void 0 || source.kind === "unreadable") return { error: `cannot pull ${machine}'s copy here` };
68765
+ const fetchFiles = (copy, rels) => gen(function* () {
68677
68766
  const key = [...globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("");
68678
- const rels = [...copy.entries].flatMap(([rel, entry]) => entry.startsWith("f ") ? [rel] : []);
68679
68767
  const contents = /* @__PURE__ */ new Map();
68680
68768
  let finished = false;
68681
68769
  const code = yield* runScript({
68682
- name: machine,
68770
+ name: copy.machine,
68683
68771
  local: false
68684
68772
  }, [
68685
68773
  prelude,
@@ -68693,24 +68781,114 @@ const pull = (name, machine, resolved) => gen(function* () {
68693
68781
  if (match !== null) contents.set(Number(match[1]), new Hidden(match[2] ?? ""));
68694
68782
  else if (line === "ff-pulled") finished = true;
68695
68783
  }));
68696
- if (!finished || contents.size !== rels.length) return { error: code === 255 ? `could not reach ${machine}` : `could not read its copy on ${machine}` };
68784
+ return !finished || contents.size !== rels.length ? { error: code === 255 ? `could not reach ${copy.machine}` : `could not read its copy on ${copy.machine}` } : {
68785
+ key: new Hidden(key),
68786
+ contents: new Map(rels.map((rel, n) => [rel, contents.get(n) ?? new Hidden("")]))
68787
+ };
68788
+ });
68789
+ const runHere = (from, step) => gen(function* () {
68790
+ let ok = false;
68791
+ yield* runScript({
68792
+ name: from,
68793
+ local: true
68794
+ }, stepScript(step), stepStdin(step), (line) => sync(() => {
68795
+ const parsed = parseLine(line);
68796
+ if (parsed.kind === "done" && parsed.done.id === step.id) ok = parsed.done.ok;
68797
+ }));
68798
+ return ok;
68799
+ });
68800
+ const pull = (name, machine, resolved) => gen(function* () {
68801
+ const source = resolved.sources.get(name);
68802
+ const copy = resolved.copies.get(name)?.find((c) => c.machine === machine);
68803
+ if (source === void 0 || copy === void 0 || source.kind === "unreadable") return { error: `cannot pull ${machine}'s copy here` };
68804
+ const fetched = yield* fetchFiles(copy, [...copy.entries].flatMap(([rel, entry]) => entry.startsWith("f ") ? [rel] : []));
68805
+ if ("error" in fetched) return fetched;
68697
68806
  const step = copyStep({
68698
68807
  item: source.item,
68699
68808
  entries: copy.entries,
68700
68809
  ...source.kind === "present" ? { current: source.entries } : {},
68701
- contents: new Map(rels.map((rel, n) => [rel, contents.get(n) ?? new Hidden("")])),
68810
+ contents: fetched.contents,
68702
68811
  at: copy.record?.at ?? resolved.at,
68703
- key: new Hidden(key)
68812
+ key: fetched.key
68704
68813
  });
68705
- let written = false;
68706
- yield* runScript({
68707
- name: resolved.from,
68708
- local: true
68709
- }, stepScript(step), stepStdin(step), (line) => sync(() => {
68710
- const parsed = parseLine(line);
68711
- if (parsed.kind === "done" && parsed.done.id === step.id) written = parsed.done.ok;
68712
- }));
68713
- return written ? { done: `pulled ${short$1(copy.entries)} from ${machine}` } : { error: `could not write ${machine}'s copy on ${resolved.from}` };
68814
+ return (yield* runHere(resolved.from, step)) ? { done: `pulled ${short$1(copy.entries)} from ${machine}` } : { error: `could not write ${machine}'s copy on ${resolved.from}` };
68815
+ }).pipe(catch_((e) => succeed$3({ error: e.message })));
68816
+ const differing = (a, b) => [.../* @__PURE__ */ new Set([...a.keys(), ...b.keys()])].filter((rel) => a.get(rel) !== b.get(rel)).toSorted();
68817
+ const notShownByGit = (name, old, now) => differing(old.entries, now.entries).flatMap((rel) => {
68818
+ const a = old.entries.get(rel);
68819
+ const b = now.entries.get(rel);
68820
+ const path = rel === "." ? name : `${name}/${rel}`;
68821
+ const mode = (entry) => entry.split(" ")[1] ?? "";
68822
+ const executable = (entry) => (Number.parseInt(mode(entry), 8) & 64) !== 0;
68823
+ if (a?.startsWith("d ") === true && b?.startsWith("d ") === true) return [`${path}/ mode ${mode(a)} → ${mode(b)}`];
68824
+ if (a?.startsWith("d ") === true) return [`${path}/ only on ${old.machine}`];
68825
+ if (b?.startsWith("d ") === true) return [`${path}/ only on ${now.machine}`];
68826
+ if (a?.startsWith("f ") === true && b?.startsWith("f ") === true && mode(a) !== mode(b) && executable(a) === executable(b)) return [`${path} mode ${mode(a)} → ${mode(b)}`];
68827
+ return [];
68828
+ });
68829
+ const diffStep = (input) => {
68830
+ const rels = differing(input.old.entries, input.new.entries);
68831
+ const inputs = [];
68832
+ const side = (s) => {
68833
+ const decode = s.key === void 0 ? "base64 -d" : `FF_KEY="$ff_input_${inputs.push(s.key) - 1}" openssl enc -d ${CIPHER} -pass env:FF_KEY`;
68834
+ return [
68835
+ `ff_p=${quote(`${input.dir}/${s.machine}/${input.name}`)}`,
68836
+ kindOf(s.entries) === "a directory" ? `mkdir -p "$ff_p"` : `mkdir -p "$(dirname "$ff_p")"`,
68837
+ ...rels.flatMap((rel) => {
68838
+ const entry = s.entries.get(rel);
68839
+ if (entry?.startsWith("l ")) return [`mkdir -p "$(dirname ${at(rel)})"`, `ln -s -- ${quote(entry.slice(2))} ${at(rel)}`];
68840
+ if (!entry?.startsWith("f ")) return [];
68841
+ const n = inputs.push(s.contents.get(rel) ?? new Hidden("")) - 1;
68842
+ return [
68843
+ `mkdir -p "$(dirname ${at(rel)})"`,
68844
+ `printf '%s' "$ff_input_${n}" | ${decode} > ${at(rel)}`,
68845
+ `chmod ${entry.split(" ")[1] ?? ""} ${at(rel)}`
68846
+ ];
68847
+ })
68848
+ ];
68849
+ };
68850
+ const run = [...side(input.old), ...side(input.new)];
68851
+ return {
68852
+ id: `files:${input.name}:diff`,
68853
+ items: [input.name],
68854
+ doing: "diffing",
68855
+ run: [
68856
+ "(",
68857
+ " set -e",
68858
+ ...run.map((l) => ` ${l}`),
68859
+ ")"
68860
+ ],
68861
+ input: inputs
68862
+ };
68863
+ };
68864
+ const diff = (name, machine, resolved, dir) => gen(function* () {
68865
+ const source = resolved.sources.get(name);
68866
+ const copy = resolved.copies.get(name)?.find((c) => c.machine === machine);
68867
+ if (source?.kind !== "present" || copy === void 0) return { error: `cannot diff ${machine}'s copy here` };
68868
+ const fetched = yield* fetchFiles(copy, differing(copy.entries, source.entries).filter((rel) => copy.entries.get(rel)?.startsWith("f ") === true));
68869
+ if ("error" in fetched) return fetched;
68870
+ const sides = {
68871
+ old: {
68872
+ machine,
68873
+ entries: copy.entries,
68874
+ contents: fetched.contents,
68875
+ key: fetched.key
68876
+ },
68877
+ new: {
68878
+ machine: resolved.from,
68879
+ entries: source.entries,
68880
+ contents: source.contents
68881
+ }
68882
+ };
68883
+ return (yield* runHere(resolved.from, diffStep({
68884
+ name,
68885
+ dir,
68886
+ ...sides
68887
+ }))) ? {
68888
+ old: machine,
68889
+ new: resolved.from,
68890
+ notShown: notShownByGit(name, sides.old, sides.new)
68891
+ } : { error: `could not write the diff on ${resolved.from}` };
68714
68892
  }).pipe(catch_((e) => succeed$3({ error: e.message })));
68715
68893
  const recordStep = (change) => ({
68716
68894
  id: `files:${change.item.name}:record`,
@@ -68762,7 +68940,8 @@ const files = {
68762
68940
  }),
68763
68941
  decide: ({ items, observed, resolved, ctx }) => items.map((item) => decideFile(item, observed, resolved, ctx)),
68764
68942
  act: (decisions) => decisions.flatMap((d) => d.kind === "change" ? [copyStep(d.change)] : d.kind === "leave" && d.upkeep !== void 0 ? [recordStep(d.upkeep)] : []),
68765
- adopt: (item, answer, resolved) => answer.startsWith(PULL) ? pull(item, answer.slice(5), resolved) : void 0
68943
+ adopt: (item, answer, resolved) => answer.startsWith(PULL) ? pull(item, answer.slice(5), resolved) : void 0,
68944
+ diff: (item, choice, resolved, dir) => choice.startsWith(DIFF) ? diff(item, choice.slice(5), resolved, dir) : void 0
68766
68945
  };
68767
68946
  const decodeFact$1 = decodeUnknownOption(Union([
68768
68947
  Struct({
@@ -69367,6 +69546,25 @@ const PROVIDERS = [
69367
69546
  repos
69368
69547
  ];
69369
69548
  //#endregion
69549
+ //#region src/diff.ts
69550
+ const showDiff = fn("showDiff")(function* (dir, sides) {
69551
+ const spawner = yield* ChildProcessSpawner;
69552
+ return yield* scoped(gen(function* () {
69553
+ return yield* (yield* spawner.spawn(make$13("git", [
69554
+ "diff",
69555
+ "--no-index",
69556
+ "--",
69557
+ sides.old,
69558
+ sides.new
69559
+ ], {
69560
+ cwd: dir,
69561
+ stdin: "inherit",
69562
+ stdout: "inherit",
69563
+ stderr: "inherit"
69564
+ }))).exitCode;
69565
+ }));
69566
+ });
69567
+ //#endregion
69370
69568
  //#region src/live.tsx
69371
69569
  const cell = (initial) => {
69372
69570
  let value = initial;
@@ -69384,6 +69582,10 @@ const cell = (initial) => {
69384
69582
  };
69385
69583
  };
69386
69584
  const LiveLedger = (props) => {
69585
+ const { suspendTerminal } = useApp();
69586
+ (0, import_react.useEffect)(() => {
69587
+ props.suspend.current = suspendTerminal;
69588
+ }, [props.suspend, suspendTerminal]);
69387
69589
  const { state, now } = (0, import_react.useSyncExternalStore)(props.store.subscribe, props.store.get);
69388
69590
  const asking = (0, import_react.useSyncExternalStore)(props.asking.subscribe, props.asking.get);
69389
69591
  const [full, setFull] = (0, import_react.useState)(false);
@@ -69423,9 +69625,11 @@ const LiveLedger = (props) => {
69423
69625
  };
69424
69626
  const startLive = fn("startLive")(function* (store, color) {
69425
69627
  const asking = cell(void 0);
69628
+ const suspend = {};
69426
69629
  const instance = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LiveLedger, {
69427
69630
  store,
69428
69631
  asking,
69632
+ suspend,
69429
69633
  color
69430
69634
  }), { exitOnCtrlC: false });
69431
69635
  yield* addFinalizer(() => promise(async () => {
@@ -69434,20 +69638,29 @@ const startLive = fn("startLive")(function* (store, color) {
69434
69638
  await instance.waitUntilExit();
69435
69639
  }));
69436
69640
  yield* forkScoped(repeat$1(store.tick, spaced(`${SPINNER.interval} millis`)));
69437
- return { ask: process.stdin.isTTY ? (choices) => callback$1((resume) => {
69438
- asking.set({
69439
- titles: choices.map((c) => c.title),
69440
- answer: (index) => {
69441
- const choice = choices[index];
69442
- if (choice === void 0) return;
69443
- asking.set(void 0);
69444
- resume(succeed$3(choice.value));
69445
- }
69446
- });
69447
- return sync(() => asking.set(void 0));
69448
- }) : void 0 };
69641
+ return {
69642
+ ask: process.stdin.isTTY ? (choices) => callback$1((resume) => {
69643
+ asking.set({
69644
+ titles: choices.map((c) => c.title),
69645
+ answer: (index) => {
69646
+ const choice = choices[index];
69647
+ if (choice === void 0) return;
69648
+ asking.set(void 0);
69649
+ resume(succeed$3(choice.value));
69650
+ }
69651
+ });
69652
+ return sync(() => asking.set(void 0));
69653
+ }) : void 0,
69654
+ aside: (effect) => {
69655
+ const suspendTerminal = suspend.current;
69656
+ return suspendTerminal === void 0 ? effect : acquireUseRelease(promise(() => suspendTerminal()), () => effect, (suspension) => promise(() => suspension.resume()));
69657
+ }
69658
+ };
69449
69659
  });
69450
- const noLive = { ask: void 0 };
69660
+ const noLive = {
69661
+ ask: void 0,
69662
+ aside: (effect) => effect
69663
+ };
69451
69664
  const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
69452
69665
  //#endregion
69453
69666
  //#region src/machine.ts
@@ -69606,7 +69819,14 @@ const askQuestions = fn("askQuestions")(function* (input) {
69606
69819
  value: c.value,
69607
69820
  title: c.value === "bump" && bumps.length > 0 ? `bump the config to ${bumps.map((b) => bumpedRange(b.range, b.newest)).join(" and ")}` : c.title
69608
69821
  }));
69609
- const answer = input.answer(question.kind, question.subject) ?? (input.ask === void 0 ? question.default : yield* input.ask(choices));
69822
+ const diffs = [...new Map(group.flatMap((a) => a.question.diffs ?? []).map((c) => [c.value, c])).values()];
69823
+ const isDiff = (value) => diffs.some((d) => d.value === value);
69824
+ let answer = input.answer(question.kind, question.subject);
69825
+ while (answer === void 0) {
69826
+ const picked = input.ask === void 0 ? question.default : yield* input.ask([...choices, ...diffs]);
69827
+ if (isDiff(picked)) yield* input.diff(question, picked);
69828
+ else answer = picked;
69829
+ }
69610
69830
  yield* input.ledger.note("answer", choices.find((c) => c.value === answer)?.title ?? answer);
69611
69831
  answered.push({
69612
69832
  question,
@@ -69885,6 +70105,18 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
69885
70105
  let cfg = config;
69886
70106
  const decided = /* @__PURE__ */ new Map();
69887
70107
  const answer = (kind, subject) => decided.get(`${kind}:${subject}`) ?? given(kind, subject);
70108
+ const diff = (question, choice) => scoped(gen(function* () {
70109
+ const at = parseAddress(question.subject);
70110
+ const provider = providers.find((p) => p.name === at?.provider);
70111
+ if (at === void 0 || provider === void 0) return;
70112
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "fleetfizz-diff-" });
70113
+ const sides = yield* provider.diff?.(at.item, choice, resolved[provider.name], dir) ?? succeed$3(void 0);
70114
+ if (sides === void 0) return;
70115
+ if ("error" in sides) return yield* ledger.note("info", `${question.subject}: ${sides.error}`);
70116
+ const code = yield* live.aside(showDiff(dir, sides));
70117
+ if (code > 1) yield* ledger.note("info", `${question.subject}: git diff exited with ${code}`);
70118
+ if (sides.notShown.length > 0) yield* ledger.note("info", `${question.subject}: also differs, not shown by git: ${sides.notShown.join(", ")}`);
70119
+ })).pipe(catch_((e) => ledger.note("info", `${question.subject}: could not show the diff: ${e.message}`)));
69888
70120
  let planned = planAll(cfg, answer);
69889
70121
  for (;;) {
69890
70122
  const asked = planned.flatMap((g) => g.plan.providers.flatMap((pp) => pp.decisions.flatMap((d) => d.kind === "ask" ? [{
@@ -69896,6 +70128,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
69896
70128
  answer,
69897
70129
  ledger,
69898
70130
  ask: live.ask,
70131
+ diff,
69899
70132
  dryRun: flags.dryRun
69900
70133
  });
69901
70134
  let progressed = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetfizz",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Keep every Machine on your tailnet in line with one config.",
5
5
  "license": "MIT",
6
6
  "repository": {