fleetfizz 0.9.1 → 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 (3) hide show
  1. package/dist/fleetfizz.mjs +1303 -161
  2. package/package.json +3 -2
  3. package/schema.json +321 -21
@@ -1849,7 +1849,7 @@ const emptyContext$1 = {
1849
1849
  * @category formatting
1850
1850
  * @since 2.0.0
1851
1851
  */
1852
- function format$2(input, options) {
1852
+ function format$3(input, options) {
1853
1853
  const space = options?.space ?? 0;
1854
1854
  const ancestors = /* @__PURE__ */ new WeakSet();
1855
1855
  const gap = !space ? "" : typeof space === "number" ? " ".repeat(space) : space;
@@ -2012,7 +2012,7 @@ function formatJson(input, options) {
2012
2012
  return JSON.stringify(input, function(key, value) {
2013
2013
  const original = Object.getOwnPropertyDescriptor(this, key)?.value;
2014
2014
  const redacted = hasProperty(original, symbolRedactable) ? redact(original) : redact(value);
2015
- if (typeof redacted === "bigint") return format$2(redacted);
2015
+ if (typeof redacted === "bigint") return format$3(redacted);
2016
2016
  if (typeof redacted !== "object" || redacted === null) return redacted;
2017
2017
  const current = redacted instanceof Error && !hasProperty(redacted, "toJSON") ? {
2018
2018
  ...redacted,
@@ -2148,7 +2148,7 @@ const toJson = (input) => {
2148
2148
  const toStringUnknown = (u, whitespace = 2) => {
2149
2149
  if (typeof u === "string") return u;
2150
2150
  try {
2151
- return typeof u === "object" ? formatJson(u, { space: whitespace }) : format$2(u, { space: whitespace });
2151
+ return typeof u === "object" ? formatJson(u, { space: whitespace }) : format$3(u, { space: whitespace });
2152
2152
  } catch {
2153
2153
  return String(u);
2154
2154
  }
@@ -2198,7 +2198,7 @@ const BaseProto = {
2198
2198
  return this.toJSON();
2199
2199
  },
2200
2200
  toString() {
2201
- return format$2(this.toJSON());
2201
+ return format$3(this.toJSON());
2202
2202
  }
2203
2203
  };
2204
2204
  //#endregion
@@ -2361,7 +2361,7 @@ const PipeInspectableProto = {
2361
2361
  return { ...this };
2362
2362
  },
2363
2363
  toString() {
2364
- return format$2(this.toJSON(), {
2364
+ return format$3(this.toJSON(), {
2365
2365
  ignoreToString: true,
2366
2366
  space: 2
2367
2367
  });
@@ -2411,7 +2411,7 @@ var CauseImpl = class {
2411
2411
  };
2412
2412
  }
2413
2413
  toString() {
2414
- return `Cause(${format$2(this.reasons)})`;
2414
+ return `Cause(${format$3(this.reasons)})`;
2415
2415
  }
2416
2416
  [NodeInspectSymbol]() {
2417
2417
  return this.toJSON();
@@ -2454,7 +2454,7 @@ var ReasonBase = class {
2454
2454
  return pipeArguments(this, arguments);
2455
2455
  }
2456
2456
  toString() {
2457
- return format$2(this);
2457
+ return format$3(this);
2458
2458
  }
2459
2459
  [NodeInspectSymbol]() {
2460
2460
  return this.toString();
@@ -2469,7 +2469,7 @@ var Fail = class extends ReasonBase {
2469
2469
  this.error = error;
2470
2470
  }
2471
2471
  toString() {
2472
- return `Fail(${format$2(this.error)})`;
2472
+ return `Fail(${format$3(this.error)})`;
2473
2473
  }
2474
2474
  toJSON() {
2475
2475
  return {
@@ -2517,7 +2517,7 @@ var Die = class extends ReasonBase {
2517
2517
  this.defect = defect;
2518
2518
  }
2519
2519
  toString() {
2520
- return `Die(${format$2(this.defect)})`;
2520
+ return `Die(${format$3(this.defect)})`;
2521
2521
  }
2522
2522
  toJSON() {
2523
2523
  return {
@@ -2578,7 +2578,7 @@ const makeExit = (options) => {
2578
2578
  },
2579
2579
  ...makePrimitiveProto(options),
2580
2580
  toString() {
2581
- return `${options.op}(${format$2(this[args])})`;
2581
+ return `${options.op}(${format$3(this[args])})`;
2582
2582
  },
2583
2583
  toJSON() {
2584
2584
  return {
@@ -2768,7 +2768,7 @@ const SomeProto = /*#__PURE__*/ Object.defineProperty(/*#__PURE__*/ Object.assig
2768
2768
  return combine$1(hash(this._tag))(hash(this.value));
2769
2769
  },
2770
2770
  toString() {
2771
- return `some(${format$2(this.value)})`;
2771
+ return `some(${format$3(this.value)})`;
2772
2772
  },
2773
2773
  toJSON() {
2774
2774
  return {
@@ -2840,7 +2840,7 @@ const SuccessProto = /*#__PURE__*/ Object.assign(/*#__PURE__*/ Object.create(Com
2840
2840
  return combine$1(hash(this._tag))(hash(this.success));
2841
2841
  },
2842
2842
  toString() {
2843
- return `success(${format$2(this.success)})`;
2843
+ return `success(${format$3(this.success)})`;
2844
2844
  },
2845
2845
  toJSON() {
2846
2846
  return {
@@ -2860,7 +2860,7 @@ const FailureProto = /*#__PURE__*/ Object.assign(/*#__PURE__*/ Object.create(Com
2860
2860
  return combine$1(hash(this._tag))(hash(this.failure));
2861
2861
  },
2862
2862
  toString() {
2863
- return `failure(${format$2(this.failure)})`;
2863
+ return `failure(${format$3(this.failure)})`;
2864
2864
  },
2865
2865
  toJSON() {
2866
2866
  return {
@@ -4720,7 +4720,10 @@ const bigint0$3 = /*#__PURE__*/ BigInt(0);
4720
4720
  const bigint1$2 = /*#__PURE__*/ BigInt(1);
4721
4721
  const bigint2 = /*#__PURE__*/ BigInt(2);
4722
4722
  const bigint10$1 = /*#__PURE__*/ BigInt(10);
4723
+ const bigint24 = /*#__PURE__*/ BigInt(24);
4724
+ const bigint60 = /*#__PURE__*/ BigInt(60);
4723
4725
  const bigint1e3 = /*#__PURE__*/ BigInt(1e3);
4726
+ const bigint1e6 = /*#__PURE__*/ BigInt(1e6);
4724
4727
  const roundTiesAwayFromZero = (input) => BigInt(input < 0 ? Math.ceil(input - .5) : Math.floor(input + .5));
4725
4728
  const roundMillisToNanos = (millis) => roundTiesAwayFromZero(millis * 1e6);
4726
4729
  const parseNanos = (input, scale) => {
@@ -4930,6 +4933,79 @@ const isDuration = (u) => hasProperty(u, TypeId$32);
4930
4933
  */
4931
4934
  const isFinite$1 = (self) => self.value._tag !== "Infinity" && self.value._tag !== "NegativeInfinity";
4932
4935
  /**
4936
+ * Checks whether a Duration is zero.
4937
+ *
4938
+ * **Example** (Checking for zero durations)
4939
+ *
4940
+ * ```ts import.meta.vitest
4941
+ * import { Duration } from "effect"
4942
+ *
4943
+ * Duration.isZero(Duration.zero) // => true
4944
+ * Duration.isZero(Duration.seconds(1)) // => false
4945
+ * ```
4946
+ *
4947
+ * @stability stable
4948
+ * @category predicates
4949
+ * @since 3.5.0
4950
+ */
4951
+ const isZero$1 = (self) => {
4952
+ switch (self.value._tag) {
4953
+ case "Millis": return self.value.millis === 0;
4954
+ case "Nanos": return self.value.nanos === bigint0$3;
4955
+ case "Infinity":
4956
+ case "NegativeInfinity": return false;
4957
+ }
4958
+ };
4959
+ /**
4960
+ * Returns `true` if the duration is negative (strictly less than zero).
4961
+ *
4962
+ * **Example** (Checking for negative durations)
4963
+ *
4964
+ * ```ts import.meta.vitest
4965
+ * import { Duration } from "effect"
4966
+ *
4967
+ * Duration.isNegative(Duration.seconds(-5)) // => true
4968
+ * Duration.isNegative(Duration.zero) // => false
4969
+ * Duration.isNegative(Duration.negativeInfinity) // => true
4970
+ * ```
4971
+ *
4972
+ * @stability stable
4973
+ * @category predicates
4974
+ * @since 4.0.0
4975
+ */
4976
+ const isNegative = (self) => {
4977
+ switch (self.value._tag) {
4978
+ case "Millis": return self.value.millis < 0;
4979
+ case "Nanos": return self.value.nanos < bigint0$3;
4980
+ case "NegativeInfinity": return true;
4981
+ case "Infinity": return false;
4982
+ }
4983
+ };
4984
+ /**
4985
+ * Returns the absolute value of the duration.
4986
+ *
4987
+ * **Example** (Taking absolute duration values)
4988
+ *
4989
+ * ```ts import.meta.vitest
4990
+ * import { Duration } from "effect"
4991
+ *
4992
+ * Duration.abs(Duration.seconds(-5)) // => Duration.seconds(5)
4993
+ * Duration.abs(Duration.negativeInfinity) // => Duration.infinity
4994
+ * ```
4995
+ *
4996
+ * @stability stable
4997
+ * @category math
4998
+ * @since 4.0.0
4999
+ */
5000
+ const abs = (self) => {
5001
+ switch (self.value._tag) {
5002
+ case "Infinity":
5003
+ case "NegativeInfinity": return infinity;
5004
+ case "Millis": return self.value.millis < 0 ? make$27(-self.value.millis) : self;
5005
+ case "Nanos": return self.value.nanos < bigint0$3 ? make$27(-self.value.nanos) : self;
5006
+ }
5007
+ };
5008
+ /**
4933
5009
  * A Duration representing zero time.
4934
5010
  *
4935
5011
  * **Example** (Referencing the zero duration)
@@ -5245,6 +5321,104 @@ const Equivalence$1 = (self, that) => matchPair(self, that, {
5245
5321
  * @since 2.0.0
5246
5322
  */
5247
5323
  const equals = /*#__PURE__*/ dual(2, (self, that) => Equivalence$1(self, that));
5324
+ /**
5325
+ * Decomposes a `Duration` into normalized signed components.
5326
+ *
5327
+ * **Details**
5328
+ *
5329
+ * Finite durations are returned as `{ days, hours, minutes, seconds, millis,
5330
+ * nanos }`. Infinite durations return every component as `Infinity` or
5331
+ * `-Infinity`.
5332
+ *
5333
+ * **Example** (Decomposing durations into parts)
5334
+ *
5335
+ * ```ts import.meta.vitest
5336
+ * import { Duration } from "effect"
5337
+ *
5338
+ * // Create a complex duration by adding multiple parts
5339
+ * const duration = Duration.sum(
5340
+ * Duration.sum(
5341
+ * Duration.sum(Duration.days(1), Duration.hours(2)),
5342
+ * Duration.sum(Duration.minutes(30), Duration.seconds(45))
5343
+ * ),
5344
+ * Duration.millis(123)
5345
+ * )
5346
+ * Duration.parts(duration) // => ({ days: 1, hours: 2, minutes: 30, seconds: 45, millis: 123, nanos: 0 })
5347
+ *
5348
+ * const complex = Duration.sum(Duration.hours(25), Duration.minutes(90))
5349
+ * Duration.parts(complex) // => ({ days: 1, hours: 2, minutes: 30, seconds: 0, millis: 0, nanos: 0 })
5350
+ * ```
5351
+ *
5352
+ * @stability stable
5353
+ * @category converting
5354
+ * @since 3.8.0
5355
+ */
5356
+ const parts = (self) => {
5357
+ if (self.value._tag === "Infinity") return {
5358
+ days: Infinity,
5359
+ hours: Infinity,
5360
+ minutes: Infinity,
5361
+ seconds: Infinity,
5362
+ millis: Infinity,
5363
+ nanos: Infinity
5364
+ };
5365
+ if (self.value._tag === "NegativeInfinity") return {
5366
+ days: -Infinity,
5367
+ hours: -Infinity,
5368
+ minutes: -Infinity,
5369
+ seconds: -Infinity,
5370
+ millis: -Infinity,
5371
+ nanos: -Infinity
5372
+ };
5373
+ const n = toNanosUnsafe(self);
5374
+ const neg = n < bigint0$3;
5375
+ const a = neg ? -n : n;
5376
+ const ms = a / bigint1e6;
5377
+ const sec = ms / bigint1e3;
5378
+ const min = sec / bigint60;
5379
+ const hr = min / bigint60;
5380
+ const d = hr / bigint24;
5381
+ const sign = neg ? -1 : 1;
5382
+ return {
5383
+ days: sign * Number(d),
5384
+ hours: sign * Number(hr % bigint24),
5385
+ minutes: sign * Number(min % bigint60),
5386
+ seconds: sign * Number(sec % bigint60),
5387
+ millis: sign * Number(ms % bigint1e3),
5388
+ nanos: sign * Number(a % bigint1e6)
5389
+ };
5390
+ };
5391
+ /**
5392
+ * Converts a `Duration` to a human readable string.
5393
+ *
5394
+ * **Example** (Formatting durations)
5395
+ *
5396
+ * ```ts import.meta.vitest
5397
+ * import { Duration } from "effect"
5398
+ *
5399
+ * Duration.format(Duration.millis(1000)) // => "1s"
5400
+ * Duration.format(Duration.millis(1001)) // => "1s 1ms"
5401
+ * ```
5402
+ *
5403
+ * @stability stable
5404
+ * @category converting
5405
+ * @since 2.0.0
5406
+ */
5407
+ const format$2 = (self) => {
5408
+ if (self.value._tag === "Infinity") return "Infinity";
5409
+ if (self.value._tag === "NegativeInfinity") return "-Infinity";
5410
+ if (isZero$1(self)) return "0";
5411
+ if (isNegative(self)) return "-" + format$2(abs(self));
5412
+ const fragments = parts(self);
5413
+ const pieces = [];
5414
+ if (fragments.days !== 0) pieces.push(`${fragments.days}d`);
5415
+ if (fragments.hours !== 0) pieces.push(`${fragments.hours}h`);
5416
+ if (fragments.minutes !== 0) pieces.push(`${fragments.minutes}m`);
5417
+ if (fragments.seconds !== 0) pieces.push(`${fragments.seconds}s`);
5418
+ if (fragments.millis !== 0) pieces.push(`${fragments.millis}ms`);
5419
+ if (fragments.nanos !== 0) pieces.push(`${fragments.nanos}ns`);
5420
+ return pieces.join(" ");
5421
+ };
5248
5422
  //#endregion
5249
5423
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Scheduler.js
5250
5424
  /**
@@ -7057,6 +7231,16 @@ const exitPrimitive = /*#__PURE__*/ makePrimitive$1({
7057
7231
  /** @internal */
7058
7232
  const timeoutOrElse$1 = /*#__PURE__*/ dual(2, (self, options) => flatMap$2(timeoutOption(self, options.duration), (option) => isNone(option) ? options.orElse() : succeed$6(option.value)));
7059
7233
  /** @internal */
7234
+ const timeoutErrorFromDuration = (duration) => new TimeoutError(`Operation timed out after '${format$2(duration)}'`);
7235
+ /** @internal */
7236
+ const timeout$1 = /*#__PURE__*/ dual(2, (self, duration) => {
7237
+ const decoded = fromInputUnsafe$1(duration);
7238
+ return timeoutOrElse$1(self, {
7239
+ duration: decoded,
7240
+ orElse: () => fail$4(timeoutErrorFromDuration(decoded))
7241
+ });
7242
+ });
7243
+ /** @internal */
7060
7244
  const timeoutOption = /*#__PURE__*/ dual(2, (self, duration) => raceFirst$1(asSome(self), as$1(sleep$1(duration), none())));
7061
7245
  /** @internal */
7062
7246
  const ScopeTypeId = "~effect/Scope";
@@ -7249,6 +7433,8 @@ const onExitFilter = /*#__PURE__*/ dual(3, (self, filter, f) => onExit$1(self, (
7249
7433
  }));
7250
7434
  /** @internal */
7251
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)));
7252
7438
  const infiniteTTL = /*#__PURE__*/ constant(Infinity);
7253
7439
  const makeCachedUnsafe = (self, ttlMillis) => {
7254
7440
  let expiresAt = 0;
@@ -7809,6 +7995,15 @@ const sleep$1 = (duration) => clockWith$1((clock) => clock.sleep(fromInputUnsafe
7809
7995
  /** @internal */
7810
7996
  const currentTimeMillis$1 = /*#__PURE__*/ clockWith$1((clock) => clock.currentTimeMillis);
7811
7997
  /** @internal */
7998
+ const TimeoutErrorTypeId = "~effect/Cause/TimeoutError";
7999
+ /** @internal */
8000
+ var TimeoutError = class extends (/*#__PURE__*/ TaggedError$2("TimeoutError")) {
8001
+ [TimeoutErrorTypeId] = TimeoutErrorTypeId;
8002
+ constructor(message) {
8003
+ super({ message });
8004
+ }
8005
+ };
8006
+ /** @internal */
7812
8007
  const IllegalArgumentErrorTypeId = "~effect/Cause/IllegalArgumentError";
7813
8008
  /** @internal */
7814
8009
  var IllegalArgumentError$1 = class extends (/*#__PURE__*/ TaggedError$2("IllegalArgumentError")) {
@@ -11846,6 +12041,45 @@ const ignore = ignore$1;
11846
12041
  */
11847
12042
  const orElseSucceed = orElseSucceed$1;
11848
12043
  /**
12044
+ * Adds a time limit to an effect, triggering a timeout if the effect exceeds
12045
+ * the duration.
12046
+ *
12047
+ * **When to use**
12048
+ *
12049
+ * Use when you need a timeout of an `Effect` to be represented as a typed
12050
+ * failure.
12051
+ *
12052
+ * **Details**
12053
+ *
12054
+ * The `timeout` function allows you to specify a time limit for an
12055
+ * effect's execution. If the effect does not complete within the given time, a
12056
+ * `TimeoutError` is raised. This can be useful for controlling how long your
12057
+ * program waits for a task to finish, ensuring that it doesn't hang
12058
+ * indefinitely if the task takes too long.
12059
+ *
12060
+ * **Gotchas**
12061
+ *
12062
+ * If the timeout wins, the source effect is interrupted.
12063
+ *
12064
+ * **Example** (Failing when work takes too long)
12065
+ *
12066
+ * ```ts import.meta.vitest
12067
+ * import { Effect } from "effect"
12068
+ *
12069
+ * const timedEffect = Effect.never.pipe(Effect.timeout(0))
12070
+ * const error = await Effect.runPromise(Effect.flip(timedEffect))
12071
+ * error._tag // => "TimeoutError"
12072
+ * ```
12073
+ *
12074
+ * @see {@link timeoutOption} for returning `Option.none` on timeout.
12075
+ * @see {@link timeoutOrElse} for a version that allows specifying both success and timeout handlers.
12076
+ *
12077
+ * @stability stable
12078
+ * @category delays & timeouts
12079
+ * @since 2.0.0
12080
+ */
12081
+ const timeout = timeout$1;
12082
+ /**
11849
12083
  * Applies a timeout to an effect, lazily evaluating `orElse` after interrupting
11850
12084
  * the source if the timeout is reached.
11851
12085
  *
@@ -12458,6 +12692,82 @@ const scopedWith = scopedWith$1;
12458
12692
  */
12459
12693
  const acquireRelease = acquireRelease$1;
12460
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
+ /**
12461
12771
  * Adds a finalizer to the current scope.
12462
12772
  *
12463
12773
  * **When to use**
@@ -18066,7 +18376,7 @@ const defaultLeafHook = (issue) => {
18066
18376
  */
18067
18377
  const defaultCheckHook = (issue) => findMessage(issue.issue) ?? findMessage(issue);
18068
18378
  function formatInput(issue) {
18069
- return hasInput(issue) ? format$2(issue.input) : void 0;
18379
+ return hasInput(issue) ? format$3(issue.input) : void 0;
18070
18380
  }
18071
18381
  function findExpected(issue) {
18072
18382
  const expected = issue.annotations?.expected;
@@ -18978,9 +19288,9 @@ const decodeJsonError = (input) => {
18978
19288
  const encodeUnknownAsJson = (input) => {
18979
19289
  try {
18980
19290
  const json = formatJson(input);
18981
- return json === void 0 ? format$2(input) : JSON.parse(json);
19291
+ return json === void 0 ? format$3(input) : JSON.parse(json);
18982
19292
  } catch {
18983
- return format$2(input);
19293
+ return format$3(input);
18984
19294
  }
18985
19295
  };
18986
19296
  const encodeJsonError = (input, options, encodeDefect) => {
@@ -19346,7 +19656,7 @@ const Literal$1 = class extends ASTNodeImpl {
19346
19656
  literal;
19347
19657
  constructor(literal, annotations, checks, encoding, context) {
19348
19658
  super(annotations, checks, encoding, context);
19349
- if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) throw new Error(`A numeric literal must be finite, got ${format$2(literal)}`);
19659
+ if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) throw new Error(`A numeric literal must be finite, got ${format$3(literal)}`);
19350
19660
  this.literal = literal;
19351
19661
  }
19352
19662
  /** @internal */
@@ -25686,7 +25996,7 @@ function isPattern(regExp, annotations) {
25686
25996
  const source = regExp.source;
25687
25997
  const flags = regExp.flags;
25688
25998
  const canExport = /^[dg]*uy?$/.test(flags);
25689
- const runtimeRegExp = flags === "" ? `new RegExp(${format$2(source)})` : `new RegExp(${format$2(source)}, ${format$2(flags)})`;
25999
+ const runtimeRegExp = flags === "" ? `new RegExp(${format$3(source)})` : `new RegExp(${format$3(source)}, ${format$3(flags)})`;
25690
26000
  return isPattern$1(regExp, {
25691
26001
  toJsonSchema: () => canExport ? { pattern: flags.endsWith("y") ? `^(?:${source})` : source } : [{}, true],
25692
26002
  toCode: () => ({ runtime: `Schema.isPattern(${runtimeRegExp})` }),
@@ -27118,7 +27428,7 @@ const String$5 = /*#__PURE__*/ makePrimitive("String", (value) => succeed$3(valu
27118
27428
  */
27119
27429
  const Choice = (choices) => {
27120
27430
  const choiceMap = new Map(choices);
27121
- const validChoices = choices.map(([key]) => format$2(key)).join(" | ");
27431
+ const validChoices = choices.map(([key]) => format$3(key)).join(" | ");
27122
27432
  const primitive = makePrimitive("Choice", (value) => {
27123
27433
  if (choiceMap.has(value)) return succeed$3(choiceMap.get(value));
27124
27434
  return fail$1(validChoices);
@@ -33032,7 +33342,7 @@ const runWith = (command, config) => {
33032
33342
  };
33033
33343
  //#endregion
33034
33344
  //#region package.json
33035
- var version = "0.9.1";
33345
+ var version = "0.11.0";
33036
33346
  //#endregion
33037
33347
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Logger.js
33038
33348
  const TypeId = "~effect/Logger";
@@ -33877,7 +34187,7 @@ var ParseOptions;
33877
34187
  * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
33878
34188
  * Therefore always check the errors list to find out if the input was valid.
33879
34189
  */
33880
- function parse$1(text, errors = [], options = ParseOptions.DEFAULT) {
34190
+ function parse$2(text, errors = [], options = ParseOptions.DEFAULT) {
33881
34191
  let currentProperty = null;
33882
34192
  let currentParent = [];
33883
34193
  const previousParents = [];
@@ -34453,7 +34763,7 @@ var SyntaxKind;
34453
34763
  * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
34454
34764
  * Therefore, always check the errors list to find out if the input was valid.
34455
34765
  */
34456
- const parse = parse$1;
34766
+ const parse$1 = parse$2;
34457
34767
  /**
34458
34768
  * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result.
34459
34769
  */
@@ -35989,6 +36299,7 @@ const Origin = String$6.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35989
36299
  const Ref = NonEmptyString;
35990
36300
  const isPin = (ref) => /^[0-9a-f]{40}$/.test(ref);
35991
36301
  const ProviderName = Literals$2([
36302
+ "files",
35992
36303
  "vp",
35993
36304
  "brew",
35994
36305
  "cask",
@@ -36072,12 +36383,41 @@ const ReposSection = section({ cloneRoot: optionalKey(variants(withShorthand(Str
36072
36383
  decode: (path) => ({ path }),
36073
36384
  encode: (root) => root.path
36074
36385
  }))).annotateKey({ description: `Where a Machine keeps its Managed Clones: a path, or a list of \`{ path, platforms?, machines? }\` where a Machine gets the first that targets it. \`~\` expands on that Machine. Default: \`${DEFAULT_CLONE_ROOT}\`.` }) }, Record(NonEmptyString, variants(TrackedRepo)), `Tracked Repos, keyed by name, which names the Managed Clone and the \`repos:<name>\` Item. Each is \`{ origin, ref, install, ... }\`, ${VARIANTS}.`);
36386
+ const HomePath = NonEmptyString.check(isPattern(/^(\/|~\/)./, {
36387
+ title: "path",
36388
+ description: "an absolute path, or one under ~"
36389
+ }));
36390
+ const OWN_DIRS = ["~/.config/fleetfizz", "~/.local/state/fleetfizz"];
36391
+ const SharedFilePath = HomePath.check(makeFilter((path) => {
36392
+ const p = path.replace(/\/+$/, "");
36393
+ const dir = OWN_DIRS.find((d) => p === "~" || d === p || d.startsWith(`${p}/`) || p.startsWith(`${d}/`));
36394
+ return dir === void 0 || `overlaps ${dir}, which a Ship writes itself`;
36395
+ }, {
36396
+ title: "Shared File path",
36397
+ description: "a path outside fleetfizz's own directories"
36398
+ }));
36399
+ const SharedFile = Struct({
36400
+ ...Targeting,
36401
+ path: SharedFilePath.annotateKey({ description: "A file or directory, absolute or under `~`, which expands on each Machine." }),
36402
+ exclude: optionalKey(ArraySchema(NonEmptyString)).annotateKey({ description: "Globs inside a directory that are neither copied nor deleted. One without a `/` matches a name at any depth. `.DS_Store` and `._*` are always excluded. Every Variant of one Shared File has the same exclude." })
36403
+ });
36404
+ const sameExclude = (a, b) => [...new Set(a.exclude ?? [])].toSorted().join("\n") === [...new Set(b.exclude ?? [])].toSorted().join("\n");
36405
+ const FilesSection = section({}, Record(NonEmptyString, variants(withShorthand(SharedFile, SharedFilePath, {
36406
+ decode: (path) => ({ path }),
36407
+ encode: (file) => file.path
36408
+ }))).check(makeFilter((items) => {
36409
+ const badName = Object.keys(items).find((n) => !/^[A-Za-z0-9._-]+$/.test(n) || n === "." || n === "..");
36410
+ if (badName !== void 0) return `"${badName}": a Shared File's name uses only letters, digits, ., _ and -, and is not . or ..`;
36411
+ const name = Object.entries(items).find(([, item]) => Array.isArray(item) && item.some((v) => !sameExclude(v, item[0])))?.[0];
36412
+ return name === void 0 || `"${name}": every Variant needs the same exclude`;
36413
+ }, {
36414
+ title: "Shared Files",
36415
+ description: "names of letters, digits, ., _ and -, and one exclude across a Shared File's Variants"
36416
+ })), `Shared Files, keyed by name, which names the \`files:<name>\` Item. Each is a path, \`{ path, exclude?, platforms?, machines? }\`, ${VARIANTS}.`);
36075
36417
  const FleetConfig = Struct({
36076
36418
  $schema: optionalKey(String$6),
36077
- shippedAt: optionalKey(String$6),
36078
- shippedFrom: optionalKey(NonEmptyString),
36079
- shippedTo: optionalKey(Record(NonEmptyString, String$6)),
36080
36419
  machines: ArraySchema(NonEmptyString).check(isMinLength(1, { message: "list at least one Machine" }), isUnique({ message: "list each Machine once" })).annotateKey({ description: "The Fleet: each Machine's tailnet name. A tailnet member that is not listed is not part of the Fleet." }),
36420
+ files: optionalKey(FilesSection).annotateKey({ description: "Files and directories copied from the Starting Machine to every targeted Machine. A directory is mirrored." }),
36081
36421
  repos: optionalKey(ReposSection).annotateKey({ description: "Each Tracked Repo is cloned on every targeted Machine and installed from its origin ref." }),
36082
36422
  vp: optionalKey(VpSection).annotateKey({ description: "The vite-plus toolchain." }),
36083
36423
  brew: optionalKey(BrewSection).annotateKey({ description: "Homebrew Formulae. Needs Homebrew on the Machine; a Ship never installs it." }),
@@ -36133,10 +36473,17 @@ const repeatedKeys = (text) => {
36133
36473
  if (tree !== void 0) visit(tree, []);
36134
36474
  return problems;
36135
36475
  };
36476
+ const STAMPS = [
36477
+ "shippedAt",
36478
+ "shippedFrom",
36479
+ "shippedTo"
36480
+ ];
36136
36481
  const movedKeys = (parsed) => {
36137
36482
  if (typeof parsed !== "object" || parsed === null) return [];
36138
36483
  const config = parsed;
36484
+ const stamps = STAMPS.filter((key) => key in parsed);
36139
36485
  return [
36486
+ ...stamps.length > 0 ? [`${stamps.join(", ")}: no longer written; a Ship keeps them in its Install Record now, so delete them`] : [],
36140
36487
  ...typeof config.machines === "object" && config.machines !== null && !Array.isArray(config.machines) ? ["machines is now a list of names, and machines.<name>.clones is repos.cloneRoot"] : [],
36141
36488
  ...Array.isArray(config.repos) ? ["repos is now { items: { <name>: { origin, ref, install } } }, keyed by each repo's name"] : [],
36142
36489
  ...typeof config.vp === "object" && config.vp !== null && "globals" in config.vp ? ["vp.globals is now vp.items"] : []
@@ -36144,7 +36491,7 @@ const movedKeys = (parsed) => {
36144
36491
  };
36145
36492
  const parseConfig = fn("parseConfig")(function* (path, text) {
36146
36493
  const parseErrors = [];
36147
- const parsed = parse(text, parseErrors, { allowTrailingComma: true });
36494
+ const parsed = parse$1(text, parseErrors, { allowTrailingComma: true });
36148
36495
  if (parseErrors.length > 0) return yield* new ConfigError({
36149
36496
  path,
36150
36497
  problems: parseErrors.map((e) => `line ${lineOf(text, e.offset)}: ${printParseErrorCode(e.error)}`)
@@ -36171,49 +36518,26 @@ const loadConfig = fn("loadConfig")(function* (path) {
36171
36518
  const text = yield* fs.readFileString(path);
36172
36519
  return yield* parseConfig(path, text);
36173
36520
  });
36174
- //#endregion
36175
- //#region ../../packages/config/src/stamp.ts
36176
- const insertAfter = (before) => (properties) => {
36177
- return before.map((key) => properties.indexOf(key)).reduce((a, b) => Math.max(a, b), -1) + 1;
36178
- };
36179
- const STAMP_KEYS = [
36180
- "$schema",
36181
- "shippedAt",
36182
- "shippedFrom",
36183
- "shippedTo"
36184
- ];
36185
- const stampText = (text, stamp) => {
36186
- const options = { formattingOptions: {
36187
- insertSpaces: true,
36188
- tabSize: 2
36189
- } };
36190
- const set = (current, key, value) => applyEdits(current, modify(current, [key], value, {
36191
- ...options,
36192
- getInsertionIndex: insertAfter(STAMP_KEYS.slice(0, STAMP_KEYS.indexOf(key)))
36193
- }));
36194
- const previous = parse(text, void 0, { allowTrailingComma: true });
36195
- const fleet = machinesOf(previous);
36196
- const shippedTo = {
36197
- ...Object.fromEntries(Object.entries(shippedToOf(previous)).filter(([name]) => fleet.includes(name))),
36198
- ...Object.fromEntries([...stamp.reached, ...stamp.own === void 0 ? [] : [stamp.own]].map((name) => [name, stamp.shippedAt]))
36199
- };
36200
- let out = set(text, "shippedAt", stamp.shippedAt);
36201
- out = set(out, "shippedFrom", stamp.shippedFrom);
36202
- return set(out, "shippedTo", shippedTo);
36203
- };
36204
- const machinesOf = (config) => {
36205
- if (typeof config !== "object" || config === null || !("machines" in config)) return [];
36206
- const value = config.machines;
36207
- return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
36208
- };
36209
- const shippedToOf = (config) => {
36210
- if (typeof config !== "object" || config === null || !("shippedTo" in config)) return {};
36211
- const value = config.shippedTo;
36212
- if (typeof value !== "object" || value === null) return {};
36213
- return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
36521
+ const ConfigRecord = Struct({
36522
+ ship: NonEmptyString,
36523
+ sha: String$6,
36524
+ reached: Record(NonEmptyString, String$6)
36525
+ });
36526
+ const decodeConfigRecord = decodeUnknownOption(ConfigRecord);
36527
+ const decodeConfigRecordJson = decodeUnknownOption(fromJsonString(ConfigRecord));
36528
+ const encodeConfigRecord = encodeSync(fromJsonString(ConfigRecord));
36529
+ const shaOf = (text) => promise(() => globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text))).pipe(map$5((digest) => [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("")));
36530
+ const reachedAfter = (previous, at, machines, fleet) => ({
36531
+ ...Object.fromEntries(Object.entries(previous).filter(([name]) => fleet.includes(name))),
36532
+ ...Object.fromEntries(machines.map((name) => [name, at]))
36533
+ });
36534
+ const mergeReached = (all) => {
36535
+ const merged = {};
36536
+ for (const reached of all) for (const [name, at] of Object.entries(reached)) if (isNewer(at, merged[name])) merged[name] = at;
36537
+ return merged;
36214
36538
  };
36215
36539
  const isNewer = (remote, local) => remote !== void 0 && (local === void 0 || remote > local);
36216
- const staleCopies = (config, copies, own) => copies.filter((c) => c.machine !== own && isNewer(c.config.shippedTo?.[c.machine], config.shippedTo?.[c.machine]));
36540
+ const staleWhy = (copy, base) => copy.record === void 0 ? "unrecorded" : copy.record.sha !== copy.sha ? "edited" : isNewer(copy.record.reached[copy.machine], base[copy.machine]) ? "newer" : void 0;
36217
36541
  //#endregion
36218
36542
  //#region ../../packages/remote/src/shell.ts
36219
36543
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
@@ -36232,6 +36556,7 @@ const prelude = [
36232
36556
  `export PATH="$PATH:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin"`,
36233
36557
  `ff_json() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' | tr -d '\\n'; }`,
36234
36558
  `ff_state() { printf 'ff-state %s\\n' "$1"; }`,
36559
+ `ff_sha() { if command -v shasum >/dev/null 2>&1; then shasum -a 256; else sha256sum; fi | cut -d" " -f1; }`,
36235
36560
  `ff_at() { ff_state "{\\"provider\\":\\"$(ff_json "$1")\\",\\"fact\\":\\"at\\",\\"item\\":\\"$(ff_json "$2")\\",\\"at\\":\\"$(ff_json "$3")\\"}"; }`
36236
36561
  ].join("\n");
36237
36562
  const fingerprintFn = [
@@ -36250,6 +36575,7 @@ const LOGS_DIR = `${STATE_DIR}/logs`;
36250
36575
  const CONFIG_DIR_SH = "\"${XDG_CONFIG_HOME:-$HOME/.config}/fleetfizz\"";
36251
36576
  const recordDir = (provider) => `${quotePath(RECORDS_DIR)}/${quote(provider)}`;
36252
36577
  const recordFile = (provider, item) => `${recordDir(provider)}/${quote(`${item}.json`)}`;
36578
+ const CONFIG_RECORD = `${RECORDS_DIR}/config.json`;
36253
36579
  const LEGACY_RECORD_PROVIDER = "repos";
36254
36580
  const recordFact = (provider, item) => [
36255
36581
  `ff_r=${recordFile(provider, item)}`,
@@ -36266,7 +36592,7 @@ const checkScript = (input) => {
36266
36592
  lines.push(provider.observe(items, input.scope));
36267
36593
  if (provider.rule === "record") for (const item of items) lines.push(...recordFact(provider.name, item.name));
36268
36594
  }
36269
- lines.push(`ff_c=${CONFIG_DIR_SH}/${quote(CONFIG_FILE)}`, `if [ -f "$ff_c" ]; then ff_state "{\\"fact\\":\\"config\\",\\"present\\":true,\\"base64\\":\\"$(base64 < "$ff_c" | tr -d '\\n')\\"}"; else ff_state '{"fact":"config","present":false}'; fi`);
36595
+ lines.push(`ff_c=${CONFIG_DIR_SH}/${quote(CONFIG_FILE)}`, `if [ -f "$ff_c" ]; then ff_b=$(base64 < "$ff_c" | tr -d '\\n'); ff_state "{\\"fact\\":\\"config\\",\\"present\\":true,\\"sha\\":\\"$(printf '%s' "$ff_b" | base64 -d | ff_sha)\\",\\"base64\\":\\"$ff_b\\"}"; else ff_state '{"fact":"config","present":false}'; fi`, `ff_r=${quotePath(CONFIG_RECORD)}`, `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"configRecord\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`);
36270
36596
  lines.push(`ff_state '{"fact":"end"}'`);
36271
36597
  return lines.join("\n");
36272
36598
  };
@@ -36283,7 +36609,7 @@ const StepSkip = Struct({
36283
36609
  id: String$6,
36284
36610
  reason: String$6
36285
36611
  });
36286
- const decodeFact$3 = decodeUnknownResult(fromJsonString(Fact));
36612
+ const decodeFact$4 = decodeUnknownResult(fromJsonString(Fact));
36287
36613
  const decodeStart = decodeUnknownResult(fromJsonString(StepStart));
36288
36614
  const decodeDone = decodeUnknownResult(fromJsonString(StepDone));
36289
36615
  const decodeSkip = decodeUnknownResult(fromJsonString(StepSkip));
@@ -36293,7 +36619,7 @@ const DONE = "ff-done ";
36293
36619
  const SKIP = "ff-skip ";
36294
36620
  const parseLine = (line) => {
36295
36621
  if (line.startsWith(STATE)) {
36296
- const result = decodeFact$3(line.slice(9));
36622
+ const result = decodeFact$4(line.slice(9));
36297
36623
  if (result._tag === "Success") return {
36298
36624
  kind: "state",
36299
36625
  fact: result.success
@@ -36339,12 +36665,14 @@ const marked = (id, run, failed = []) => [
36339
36665
  doneLine(id, true),
36340
36666
  "fi"
36341
36667
  ];
36342
- const configStep = () => {
36668
+ const configStep = (record) => {
36343
36669
  const dir = CONFIG_DIR_SH;
36344
36670
  const write = (variable, file) => `printf '%s' "$${variable}" | base64 -d > ${dir}/${quote(`${file}.tmp`)} && mv -f ${dir}/${quote(`${file}.tmp`)} ${dir}/${quote(file)}`;
36345
- return marked(CONFIG_STEP, [`( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`]);
36671
+ const recordFile = quotePath(CONFIG_RECORD);
36672
+ const recordTmp = quotePath(`${CONFIG_RECORD}.tmp`);
36673
+ return marked(CONFIG_STEP, [`( mkdir -p ${dir} ${quotePath(RECORDS_DIR)} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} && printf '%s\\n' ${quote(record)} > ${recordTmp} && mv -f ${recordTmp} ${recordFile} )`]);
36346
36674
  };
36347
- const providerSteps = (plan) => {
36675
+ const providerSteps = (plan, offsets) => {
36348
36676
  if (plan.decisions.length === 0 && plan.steps.length === 0) return [];
36349
36677
  const held = `ff_held_${plan.name}`;
36350
36678
  const items = [...new Set(plan.steps.flatMap((step) => step.items))];
@@ -36353,6 +36681,7 @@ const providerSteps = (plan) => {
36353
36681
  `if [ -n "${s.items.map((item) => `\${${flag(item)}:-}`).join("")}" ]; then`,
36354
36682
  skipLine(s.id, "'an earlier step failed'"),
36355
36683
  "else",
36684
+ ...(s.input ?? []).map((_, i) => `ff_input_${i}="$ff_in_${(offsets.get(s) ?? 0) + i}"`),
36356
36685
  ...marked(s.id, s.run, [...s.items.map((item) => `${flag(item)}=1`), `${held}=1`]),
36357
36686
  "fi"
36358
36687
  ];
@@ -36368,10 +36697,21 @@ const providerSteps = (plan) => {
36368
36697
  "fi"
36369
36698
  ];
36370
36699
  };
36371
- const applyStdin = (token, config, schema) => [
36700
+ const inputsOf = (plan) => plan.providers.flatMap((p) => p.steps.flatMap((step) => step.input ?? []));
36701
+ const inputOffsets = (plan) => {
36702
+ const offsets = /* @__PURE__ */ new Map();
36703
+ let next = 0;
36704
+ for (const step of plan.providers.flatMap((p) => p.steps)) {
36705
+ offsets.set(step, next);
36706
+ next += step.input?.length ?? 0;
36707
+ }
36708
+ return offsets;
36709
+ };
36710
+ const applyStdin = (token, config, schema, plan) => [
36372
36711
  token ?? "",
36373
36712
  encodeBase64(config),
36374
- encodeBase64(schema)
36713
+ encodeBase64(schema),
36714
+ ...inputsOf(plan).map((input) => input.reveal())
36375
36715
  ];
36376
36716
  const encodeBase64 = (text) => {
36377
36717
  const bytes = new TextEncoder().encode(text);
@@ -36379,18 +36719,30 @@ const encodeBase64 = (text) => {
36379
36719
  for (const byte of bytes) binary += String.fromCharCode(byte);
36380
36720
  return btoa(binary);
36381
36721
  };
36382
- const applyScript = (plan) => [
36722
+ const applyScript = (plan, configRecord) => {
36723
+ const offsets = inputOffsets(plan);
36724
+ return [
36725
+ prelude,
36726
+ fingerprintFn,
36727
+ `ff_lit() { printf '%s' "$1"; }`,
36728
+ `IFS= read -r ff_token || ff_token=""`,
36729
+ `IFS= read -r ff_config || ff_config=""`,
36730
+ `IFS= read -r ff_schema || ff_schema=""`,
36731
+ ...inputsOf(plan).map((_, i) => `IFS= read -r ff_in_${i} || exit 1`),
36732
+ `ff_git() { if [ -n "$ff_token" ]; then FF_TOKEN="$ff_token" git -c credential.helper= -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$FF_TOKEN"; }; f' "$@"; else git "$@"; fi; }`,
36733
+ ...configStep(configRecord),
36734
+ ...plan.providers.flatMap((p) => providerSteps(p, offsets)),
36735
+ doneLine("end", true)
36736
+ ].join("\n");
36737
+ };
36738
+ const stepScript = (step) => [
36383
36739
  prelude,
36384
36740
  fingerprintFn,
36385
36741
  `ff_lit() { printf '%s' "$1"; }`,
36386
- `IFS= read -r ff_token || ff_token=""`,
36387
- `IFS= read -r ff_config || ff_config=""`,
36388
- `IFS= read -r ff_schema || ff_schema=""`,
36389
- `ff_git() { if [ -n "$ff_token" ]; then FF_TOKEN="$ff_token" git -c credential.helper= -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$FF_TOKEN"; }; f' "$@"; else git "$@"; fi; }`,
36390
- ...configStep(),
36391
- ...plan.providers.flatMap(providerSteps),
36392
- doneLine("end", true)
36742
+ ...(step.input ?? []).flatMap((_, i) => [`IFS= read -r ff_in_${i} || ff_in_${i}=""`, `ff_input_${i}="$ff_in_${i}"`]),
36743
+ ...marked(step.id, step.run)
36393
36744
  ].join("\n");
36745
+ const stepStdin = (step) => (step.input ?? []).map((i) => i.reveal());
36394
36746
  //#endregion
36395
36747
  //#region ../../packages/remote/src/tailnet.ts
36396
36748
  const StatusNode = Struct({
@@ -36544,6 +36896,20 @@ const resolveVersions = fn("resolveVersions")(function* (packages) {
36544
36896
  const entries = yield* forEach$1(unique, (pkg) => resolveOne(pkg).pipe(map$5((v) => [pkg, v])), { concurrency: 8 });
36545
36897
  return Object.fromEntries(entries);
36546
36898
  });
36899
+ //#endregion
36900
+ //#region src/configRecord.ts
36901
+ const recordPath = () => CONFIG_RECORD.replace(/^~/, process.env.HOME ?? "~");
36902
+ const readConfigRecord = gen(function* () {
36903
+ const text = yield* (yield* FileSystem).readFileString(recordPath()).pipe(option);
36904
+ return getOrUndefined$1(flatMap$3(text, decodeConfigRecordJson));
36905
+ });
36906
+ const writeConfigRecord = fn("writeConfigRecord")(function* (json) {
36907
+ const fs = yield* FileSystem;
36908
+ const path = recordPath();
36909
+ yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
36910
+ yield* fs.writeFileString(`${path}.tmp`, `${json}\n`);
36911
+ yield* fs.rename(`${path}.tmp`, path);
36912
+ });
36547
36913
  var LogsError = class extends Error {
36548
36914
  name = "LogsError";
36549
36915
  };
@@ -36616,7 +36982,7 @@ const logs = fn("logs")(function* (flags) {
36616
36982
  for (const id of local.toReversed()) yield* log(id);
36617
36983
  return;
36618
36984
  }
36619
- const id = flags.id ?? (yield* latestShip(local, self, targetOf));
36985
+ const id = flags.id ?? (yield* latestShip(local, self));
36620
36986
  const ship = parseShipId(id);
36621
36987
  if (ship === void 0) return yield* fail$1(new LogsError(`not a Ship id: ${id} (fizz logs --list shows them)`));
36622
36988
  let missing = false;
@@ -36627,17 +36993,13 @@ const logs = fn("logs")(function* (flags) {
36627
36993
  });
36628
36994
  if (missing) return yield* fail$1(new LogsError(`no log for ${id} on ${ship.from}; a Machine keeps its last 20`));
36629
36995
  });
36630
- const latestShip = fn("latestShip")(function* (local, self, targetOf) {
36996
+ const latestShip = fn("latestShip")(function* (local, self) {
36631
36997
  const newest = local.at(-1);
36632
- const config = getOrUndefined$1(yield* loadConfig(configPath(process.env)).pipe(option));
36633
- const from = config?.shippedFrom;
36634
- const at = Date.parse(config?.shippedAt ?? "");
36998
+ const record = yield* readConfigRecord;
36999
+ const from = parseShipId(record?.ship ?? "")?.from;
37000
+ const at = self === void 0 ? Infinity : Date.parse(record?.reached[self] ?? "");
36635
37001
  const newestStarted = parseShipId(newest ?? "")?.started ?? -Infinity;
36636
- if (from !== void 0 && from !== self && at > newestStarted) {
36637
- const there = (yield* shipIds(yield* targetOf(from))).filter((id) => (parseShipId(id)?.started ?? Infinity) <= at).at(-1);
36638
- if (there === void 0) return yield* fail$1(new LogsError(`the last Ship to reach this Machine (from ${from} at ${config?.shippedAt}) has no log there`));
36639
- return there;
36640
- }
37002
+ if (record !== void 0 && from !== void 0 && from !== self && at > newestStarted) return record.ship;
36641
37003
  if (newest === void 0) return yield* fail$1(new LogsError("no Ship Log on this Machine yet"));
36642
37004
  return newest;
36643
37005
  });
@@ -60836,7 +61198,7 @@ if (process$1.env["DEV"] === "true") {
60836
61198
  } catch {}
60837
61199
  if (isDevtoolsInstalled) await init_devtools().then(() => devtools_exports);
60838
61200
  }
60839
- const diff = (before, after) => {
61201
+ const diff$1 = (before, after) => {
60840
61202
  if (before === after) return;
60841
61203
  if (!before) return after;
60842
61204
  const changed = {};
@@ -61011,8 +61373,8 @@ const hostConfig = {
61011
61373
  const rootNode = findRootNode(node);
61012
61374
  if (rootNode) rootNode.isStaticDirty = true;
61013
61375
  }
61014
- const props = diff(oldProps, newProps);
61015
- const style = diff(oldProps["style"], newProps["style"]);
61376
+ const props = diff$1(oldProps, newProps);
61377
+ const style = diff$1(oldProps["style"], newProps["style"]);
61016
61378
  if (!props && !style) return;
61017
61379
  if (props) for (const [key, value] of Object.entries(props)) {
61018
61380
  if (key === "style") {
@@ -64727,6 +65089,14 @@ const useInput = (inputHandler, options = {}) => {
64727
65089
  }, [options.isActive, internal_eventEmitter]);
64728
65090
  };
64729
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
64730
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
64731
65101
  /**
64732
65102
  A React hook that returns the stdout stream where Ink renders your app.
@@ -64886,9 +65256,25 @@ const QUESTION_CHOICES = {
64886
65256
  "downgrade"
64887
65257
  ],
64888
65258
  config: ["overwrite", "stop"],
64889
- mode: ["managed", "skip"]
65259
+ mode: ["managed", "skip"],
65260
+ file: ["overwrite", "skip"]
64890
65261
  };
64891
65262
  const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
65263
+ var Hidden = class {
65264
+ #value;
65265
+ constructor(value) {
65266
+ this.#value = value;
65267
+ }
65268
+ reveal() {
65269
+ return this.#value;
65270
+ }
65271
+ toJSON() {
65272
+ return "[hidden]";
65273
+ }
65274
+ toString() {
65275
+ return "[hidden]";
65276
+ }
65277
+ };
64892
65278
  const rangeQuestion = (subject, installed, range, bumpPath) => ({
64893
65279
  kind: "range",
64894
65280
  subject,
@@ -65022,8 +65408,13 @@ const EngineFact = Union([
65022
65408
  Struct({
65023
65409
  fact: Literal("config"),
65024
65410
  present: Boolean$4,
65411
+ sha: optionalKey(String$6),
65025
65412
  base64: optionalKey(String$6)
65026
65413
  }),
65414
+ Struct({
65415
+ fact: Literal("configRecord"),
65416
+ record: Unknown
65417
+ }),
65027
65418
  Struct({
65028
65419
  fact: Literal("record"),
65029
65420
  provider: String$6,
@@ -65066,9 +65457,18 @@ const assembleState = (facts) => {
65066
65457
  };
65067
65458
  break;
65068
65459
  case "config":
65069
- if (f.present && f.base64 !== void 0) state = {
65460
+ if (f.present && f.base64 !== void 0 && f.sha !== void 0) state = {
65070
65461
  ...state,
65071
- config: decodeBase64(f.base64)
65462
+ config: {
65463
+ text: decodeBase64(f.base64),
65464
+ sha: f.sha
65465
+ }
65466
+ };
65467
+ break;
65468
+ case "configRecord":
65469
+ state = {
65470
+ ...state,
65471
+ configRecord: f.record
65072
65472
  };
65073
65473
  break;
65074
65474
  case "record": state = {
@@ -65167,7 +65567,7 @@ const LedgerState = Struct({
65167
65567
  started: Number$2,
65168
65568
  ended: optionalKey(Number$2)
65169
65569
  });
65170
- const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65570
+ const itemsOf = (p, config) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65171
65571
  const base = {
65172
65572
  name: address(pp.name, d.item),
65173
65573
  kind: pp.rule,
@@ -65210,7 +65610,8 @@ const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65210
65610
  })), {
65211
65611
  name: CONFIG_STEP,
65212
65612
  kind: "config",
65213
- changes: false,
65613
+ ...config,
65614
+ changes: config.from !== config.to,
65214
65615
  status: "idle",
65215
65616
  steps: [{
65216
65617
  id: CONFIG_STEP,
@@ -65306,10 +65707,10 @@ const makeLedger = fn("makeLedger")(function* (input) {
65306
65707
  }
65307
65708
  })).pipe(andThen(logInfo(`resolving ${n} items`))),
65308
65709
  resolved: update(({ resolving: _, ...state }) => state).pipe(andThen(logInfo("resolved"))),
65309
- plan: (name, p) => machine(name, (m) => ({
65710
+ plan: (name, p, config) => machine(name, (m) => ({
65310
65711
  ...m,
65311
65712
  status: "planned",
65312
- items: itemsOf(p)
65713
+ items: itemsOf(p, config)
65313
65714
  })).pipe(andThen(forEach$1(p.providers.flatMap((pp) => pp.decisions.map((d) => ({
65314
65715
  pp,
65315
65716
  d
@@ -67365,10 +67766,10 @@ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
67365
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}`;
67366
67767
  const SPINNER = cli_spinners_default.dots;
67367
67768
  const runGlyph = (live, now) => (live ? SPINNER.frames[Math.floor(now / SPINNER.interval) % SPINNER.frames.length] : void 0) ?? "▸";
67368
- const unchanged = (i) => ({
67769
+ const UNCHANGED = {
67369
67770
  text: "",
67370
- detail: i.kind === "record" ? "up to date" : void 0
67371
- });
67771
+ quiet: true
67772
+ };
67372
67773
  const statusOf = (i, dryRun, glyph) => {
67373
67774
  switch (i.status) {
67374
67775
  case "running": return {
@@ -67386,11 +67787,11 @@ const statusOf = (i, dryRun, glyph) => {
67386
67787
  detail: i.detail
67387
67788
  };
67388
67789
  case "ok":
67790
+ if (!i.changes) return UNCHANGED;
67389
67791
  if (i.kind === "config") return {
67390
67792
  text: "✓ copied",
67391
- tone: "dim"
67793
+ tone: "ok"
67392
67794
  };
67393
- if (!i.changes) return unchanged(i);
67394
67795
  if (i.kind === "record") return i.from !== void 0 && i.from === i.to ? {
67395
67796
  text: "✓ rebuilt",
67396
67797
  tone: "ok",
@@ -67408,9 +67809,10 @@ const statusOf = (i, dryRun, glyph) => {
67408
67809
  if (i.changes) return {
67409
67810
  text: dryRun ? "· planned" : "· queued",
67410
67811
  tone: "dim",
67411
- detail: i.reason
67812
+ detail: i.reason,
67813
+ quiet: true
67412
67814
  };
67413
- return unchanged(i);
67815
+ return UNCHANGED;
67414
67816
  }
67415
67817
  };
67416
67818
  const countsOf = (m) => {
@@ -67560,7 +67962,7 @@ const MachineLine = (props) => {
67560
67962
  const ItemLine = (props) => {
67561
67963
  const { i, w } = props;
67562
67964
  const status = statusOf(i, props.dryRun, props.glyph);
67563
- const quiet = i.status === "idle" ? "dim" : void 0;
67965
+ const quiet = status.quiet === true ? "dim" : void 0;
67564
67966
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
67565
67967
  columnGap: GAP,
67566
67968
  children: [
@@ -67794,7 +68196,7 @@ const report = fn("report")(function* () {
67794
68196
  ]
67795
68197
  })));
67796
68198
  });
67797
- const decodeFact$2 = decodeUnknownOption(Union([Struct({
68199
+ const decodeFact$3 = decodeUnknownOption(Union([Struct({
67798
68200
  fact: Literal("brew"),
67799
68201
  version: NullOr(String$6)
67800
68202
  }), AtFact]));
@@ -67846,7 +68248,7 @@ function homebrew(name) {
67846
68248
  let brew;
67847
68249
  const at = /* @__PURE__ */ new Map();
67848
68250
  for (const raw of facts) {
67849
- const fact = decodeFact$2(raw);
68251
+ const fact = decodeFact$3(raw);
67850
68252
  if (fact._tag === "None") continue;
67851
68253
  const f = fact.value;
67852
68254
  if (f.fact === "brew") brew = f.version ?? void 0;
@@ -67923,6 +68325,624 @@ function homebrew(name) {
67923
68325
  }
67924
68326
  const brew = homebrew("brew");
67925
68327
  const cask = homebrew("cask");
68328
+ //#endregion
68329
+ //#region ../../packages/providers/src/files/index.ts
68330
+ const ALWAYS_EXCLUDED = [".DS_Store", "._*"];
68331
+ const SIZE_LIMIT = 10485760;
68332
+ const decodeFact$2 = decodeUnknownOption(Union([Struct({
68333
+ fact: Literal("entry"),
68334
+ item: String$6,
68335
+ path: String$6,
68336
+ entry: String$6,
68337
+ size: Number$2
68338
+ }), Struct({
68339
+ fact: Literal("openssl"),
68340
+ present: Boolean$4
68341
+ })]));
68342
+ const FileRecord = Struct({
68343
+ at: String$6,
68344
+ entries: Record(String$6, String$6)
68345
+ });
68346
+ const decodeRecord$1 = decodeUnknownOption(FileRecord);
68347
+ const jsonRecord = encodeSync(fromJsonString(FileRecord));
68348
+ const fingerprint = (entries) => [...entries].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([rel, entry]) => `${rel}\t${entry}\n`).join("");
68349
+ const short$1 = (entries) => (string$1(fingerprint(entries)) >>> 0).toString(16).padStart(8, "0").slice(0, 7);
68350
+ const recordEntries = (record) => new Map(Object.entries(record.entries));
68351
+ const kindOf = (entries) => ({
68352
+ f: "a file",
68353
+ d: "a directory",
68354
+ l: "a symlink",
68355
+ "?": "something it cannot copy"
68356
+ })[entries.get(".")?.[0] ?? ""] ?? "missing";
68357
+ const depth = (rel) => rel === "." ? 0 : rel.split("/").length;
68358
+ const changedPaths = (a, b) => {
68359
+ const paths = [.../* @__PURE__ */ new Set([...a.keys(), ...b.keys()])].filter((rel) => a.get(rel) !== b.get(rel)).toSorted();
68360
+ const shown = paths.slice(0, 3).join(", ");
68361
+ return paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown;
68362
+ };
68363
+ const SHELL_FNS = [
68364
+ `ff_mode() { stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1"; }`,
68365
+ `ff_entry() {`,
68366
+ ` if [ -L "$3" ]; then ff_e="l $(readlink "$3")"; ff_s=0`,
68367
+ ` elif [ -d "$3" ]; then if [ -r "$3" ] && [ -x "$3" ]; then ff_e="d $(ff_mode "$3")"; else ff_e="?"; fi; ff_s=0`,
68368
+ ` elif [ -f "$3" ]; then if [ -r "$3" ]; then ff_e="f $(ff_mode "$3") $(ff_sha < "$3")"; ff_s=$(wc -c < "$3" | tr -d " "); else ff_e="?"; ff_s=0; fi`,
68369
+ ` else ff_e="?"; ff_s=0; fi`,
68370
+ ` ff_state "{\\"provider\\":\\"files\\",\\"fact\\":\\"entry\\",\\"item\\":\\"$(ff_json "$1")\\",\\"path\\":\\"$(ff_json "$2")\\",\\"entry\\":\\"$(ff_json "$ff_e")\\",\\"size\\":$ff_s}"`,
68371
+ `}`
68372
+ ].join("\n");
68373
+ const pruneExpr = (exclude) => [...ALWAYS_EXCLUDED, ...exclude].map((pattern) => pattern.includes("/") ? `-path ${quote(`./${pattern.replace(/^(\.?\/)+|\/+$/g, "")}`)}` : `-name ${quote(pattern)}`).join(" -o ");
68374
+ const observe$1 = (items) => {
68375
+ if (items.length === 0) return "";
68376
+ return [
68377
+ SHELL_FNS,
68378
+ `if command -v openssl >/dev/null 2>&1; then ff_o=true; else ff_o=false; fi`,
68379
+ `ff_state "{\\"provider\\":\\"files\\",\\"fact\\":\\"openssl\\",\\"present\\":$ff_o}"`,
68380
+ ...items.flatMap((item) => {
68381
+ const name = quote(item.name);
68382
+ return [
68383
+ `ff_p=${quotePath(item.path)}`,
68384
+ `if [ -d "$ff_p" ] && [ ! -L "$ff_p" ]; then`,
68385
+ ` ff_entry ${name} . "$ff_p"`,
68386
+ ` (cd "$ff_p" && find . -mindepth 1 \\( ${pruneExpr(item.exclude)} \\) -prune -o -print) | while IFS= read -r ff_rel; do ff_rel=\${ff_rel#./}; ff_entry ${name} "$ff_rel" "$ff_p/$ff_rel"; done`,
68387
+ `elif [ -e "$ff_p" ] || [ -L "$ff_p" ]; then`,
68388
+ ` ff_entry ${name} . "$ff_p"`,
68389
+ "fi"
68390
+ ];
68391
+ })
68392
+ ].join("\n");
68393
+ };
68394
+ const parse = (facts, records) => {
68395
+ const items = /* @__PURE__ */ new Map();
68396
+ let openssl = false;
68397
+ for (const raw of facts) {
68398
+ const fact = decodeFact$2(raw);
68399
+ if (fact._tag === "None") continue;
68400
+ const f = fact.value;
68401
+ if (f.fact === "openssl") {
68402
+ openssl = f.present;
68403
+ continue;
68404
+ }
68405
+ const item = items.get(f.item) ?? {
68406
+ entries: /* @__PURE__ */ new Map(),
68407
+ size: 0
68408
+ };
68409
+ item.entries.set(f.path, f.entry);
68410
+ item.size += f.size;
68411
+ items.set(f.item, item);
68412
+ }
68413
+ const decoded = /* @__PURE__ */ new Map();
68414
+ for (const [name, raw] of Object.entries(records)) {
68415
+ const record = decodeRecord$1(raw);
68416
+ if (record._tag === "Some") decoded.set(name, record.value);
68417
+ }
68418
+ return {
68419
+ items,
68420
+ records: decoded,
68421
+ openssl
68422
+ };
68423
+ };
68424
+ const at = (rel) => rel === "." ? `"$ff_p"` : `"$ff_p"/${quote(rel)}`;
68425
+ const readContents = (files) => gen(function* () {
68426
+ const contents = /* @__PURE__ */ new Map();
68427
+ const script = files.map(({ abs }, n) => `ff_b=$(base64 < ${abs}) || exit 1; printf 'ff-file ${n} %s\\n' "$(printf '%s' "$ff_b" | tr -d '\\n')"`).join("\n");
68428
+ return (yield* runScript({
68429
+ name: "local",
68430
+ local: true
68431
+ }, script, [], (line) => sync(() => {
68432
+ const match = /^ff-file (\d+) (.*)$/.exec(line);
68433
+ if (match !== null) contents.set(Number(match[1]), new Hidden(match[2] ?? ""));
68434
+ }))) === 0 && contents.size === files.length ? contents : void 0;
68435
+ });
68436
+ const resolveSources = fn("resolveSources")(function* (items, scope) {
68437
+ const facts = [];
68438
+ yield* runScript({
68439
+ name: scope.machine.name,
68440
+ local: true
68441
+ }, checkScript({
68442
+ scope,
68443
+ providers: [{
68444
+ provider: files,
68445
+ items
68446
+ }]
68447
+ }), [], (line) => sync(() => {
68448
+ const parsed = parseLine(line);
68449
+ if (parsed.kind === "state") facts.push(parsed.fact);
68450
+ }));
68451
+ const state = assembleState(facts);
68452
+ const observed = parse(state.facts.files ?? [], state.records.files ?? {});
68453
+ const sources = /* @__PURE__ */ new Map();
68454
+ for (const item of items) {
68455
+ const copy = observed.items.get(item.name);
68456
+ if (!state.complete) {
68457
+ sources.set(item.name, {
68458
+ item,
68459
+ kind: "unreadable",
68460
+ reason: "could not read it here"
68461
+ });
68462
+ continue;
68463
+ }
68464
+ if (copy === void 0) {
68465
+ sources.set(item.name, {
68466
+ item,
68467
+ kind: "missing"
68468
+ });
68469
+ continue;
68470
+ }
68471
+ const unreadable = [...copy.entries].find(([, entry]) => entry === "?")?.[0];
68472
+ if (unreadable !== void 0) {
68473
+ sources.set(item.name, {
68474
+ item,
68475
+ kind: "unreadable",
68476
+ reason: `cannot read or copy ${unreadable === "." ? item.path : unreadable} here`
68477
+ });
68478
+ continue;
68479
+ }
68480
+ if (copy.size > 10485760) {
68481
+ sources.set(item.name, {
68482
+ item,
68483
+ kind: "unreadable",
68484
+ reason: `${(copy.size / 1024 / 1024).toFixed(1)} MB, over the ${SIZE_LIMIT / 1024 / 1024} MB limit`
68485
+ });
68486
+ continue;
68487
+ }
68488
+ const list = [...copy.entries].flatMap(([rel, entry]) => entry.startsWith("f ") ? [{
68489
+ rel,
68490
+ abs: rel === "." ? quotePath(item.path) : `${quotePath(item.path)}/${quote(rel)}`
68491
+ }] : []);
68492
+ const read = yield* readContents(list);
68493
+ if (read === void 0) {
68494
+ sources.set(item.name, {
68495
+ item,
68496
+ kind: "unreadable",
68497
+ reason: "could not read it here"
68498
+ });
68499
+ continue;
68500
+ }
68501
+ const record = observed.records.get(item.name);
68502
+ sources.set(item.name, {
68503
+ item,
68504
+ kind: "present",
68505
+ entries: copy.entries,
68506
+ ...record === void 0 ? {} : { record },
68507
+ contents: new Map(list.map(({ rel }, n) => [rel, read.get(n) ?? new Hidden("")]))
68508
+ });
68509
+ }
68510
+ return {
68511
+ sources,
68512
+ openssl: observed.openssl
68513
+ };
68514
+ });
68515
+ const copiesOf = (checked, from) => {
68516
+ const copies = /* @__PURE__ */ new Map();
68517
+ for (const { machine, items, facts, records } of checked) {
68518
+ if (machine.name === from) continue;
68519
+ const observed = parse(facts, records);
68520
+ for (const item of items) {
68521
+ const copy = observed.items.get(item.name);
68522
+ if (copy === void 0 || copy.size > 10485760 || [...copy.entries.values()].includes("?")) continue;
68523
+ const record = observed.records.get(item.name);
68524
+ copies.set(item.name, [...copies.get(item.name) ?? [], {
68525
+ machine: machine.name,
68526
+ item,
68527
+ entries: copy.entries,
68528
+ ...record === void 0 ? {} : { record },
68529
+ openssl: observed.openssl
68530
+ }]);
68531
+ }
68532
+ }
68533
+ return copies;
68534
+ };
68535
+ const PULL = "pull:";
68536
+ const DIFF = "diff:";
68537
+ const askWhy = (current, record, source, from) => {
68538
+ if (record === void 0) return "differs, and no Ship wrote it";
68539
+ const recorded = recordEntries(record);
68540
+ if (fingerprint(recorded) !== fingerprint(current)) return `was edited there${current.size === 1 && recorded.size === 1 ? "" : ` (${changedPaths(recorded, current)})`}`;
68541
+ if (fingerprint(recorded) !== (source.record === void 0 ? void 0 : fingerprint(recordEntries(source.record))) && record.at > (source.record?.at ?? "")) return `came from a Ship at ${record.at.slice(0, 16).replace("T", " ")} that ${from} missed`;
68542
+ };
68543
+ const fileQuestion = (subject, found, problem, pull, from) => ({
68544
+ kind: "file",
68545
+ subject,
68546
+ found,
68547
+ problem,
68548
+ choices: [
68549
+ {
68550
+ value: "overwrite",
68551
+ title: "overwrite it with this Machine's copy"
68552
+ },
68553
+ ...pull === void 0 || !pull.openssl ? [] : [{
68554
+ value: `${PULL}${pull.machine}`,
68555
+ title: `use ${pull.machine}'s copy (${short$1(pull.entries)}) on ${from} instead`
68556
+ }],
68557
+ {
68558
+ value: "skip",
68559
+ title: "skip on these Machines (leave as is, shown as skipped)"
68560
+ }
68561
+ ],
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
+ }] }
68567
+ });
68568
+ const missingQuestion = (subject, found, copies, from) => ({
68569
+ kind: "file",
68570
+ subject,
68571
+ found,
68572
+ problem: `is not on ${from}`,
68573
+ choices: [...Map.groupBy(copies.filter((c) => c.openssl), (c) => fingerprint(c.entries)).values().flatMap((same) => {
68574
+ const [first] = same;
68575
+ return first === void 0 ? [] : [{
68576
+ value: `${PULL}${first.machine}`,
68577
+ title: `pull ${short$1(first.entries)} from ${same.map((c) => c.machine).join(", ")}`
68578
+ }];
68579
+ }), {
68580
+ value: "skip",
68581
+ title: "skip it everywhere"
68582
+ }],
68583
+ default: "skip"
68584
+ });
68585
+ const decideFile = (item, observed, resolved, ctx) => {
68586
+ const { from } = resolved;
68587
+ const source = resolved.sources.get(item.name);
68588
+ if (source === void 0) return {
68589
+ kind: "skip",
68590
+ item: item.name,
68591
+ reason: `${from} has no Variant for it; ship from a Machine it targets`
68592
+ };
68593
+ const subject = address("files", item.name);
68594
+ const answer = ctx.answer("file", subject);
68595
+ const pulling = answer?.startsWith(PULL) === true;
68596
+ const copies = resolved.copies.get(item.name) ?? [];
68597
+ const mine = copies.find((c) => c.machine === ctx.machine.name);
68598
+ if (source.kind === "missing") {
68599
+ const notHere = {
68600
+ kind: "skip",
68601
+ item: item.name,
68602
+ reason: `not on ${from}`
68603
+ };
68604
+ if (mine === void 0) return notHere;
68605
+ if (pulling) return {
68606
+ ...notHere,
68607
+ at: short$1(mine.entries),
68608
+ reason: `pulling it from ${answer?.slice(5)}`
68609
+ };
68610
+ if (answer === "skip" || !resolved.openssl || !copies.some((c) => c.openssl)) {
68611
+ const without = resolved.openssl ? copies.filter((c) => !c.openssl).map((c) => c.machine) : [from];
68612
+ return {
68613
+ ...notHere,
68614
+ at: short$1(mine.entries),
68615
+ ...answer !== "skip" && without.length > 0 ? { reason: `not on ${from}; install openssl on ${without.join(", ")} to pull it` } : {}
68616
+ };
68617
+ }
68618
+ return {
68619
+ kind: "ask",
68620
+ item: item.name,
68621
+ at: short$1(mine.entries),
68622
+ question: missingQuestion(subject, short$1(mine.entries), copies, from)
68623
+ };
68624
+ }
68625
+ if (source.kind === "unreadable") return {
68626
+ kind: "fail",
68627
+ item: item.name,
68628
+ reason: `${from}: ${source.reason}`
68629
+ };
68630
+ const current = observed.items.get(item.name)?.entries;
68631
+ const record = observed.records.get(item.name);
68632
+ const ours = fingerprint(source.entries);
68633
+ const change = {
68634
+ item,
68635
+ entries: source.entries,
68636
+ ...current === void 0 ? {} : { current },
68637
+ contents: source.contents,
68638
+ at: resolved.at
68639
+ };
68640
+ const recorded = record === void 0 ? void 0 : fingerprint(recordEntries(record));
68641
+ if (ctx.machine.name === from || current !== void 0 && fingerprint(current) === ours) return {
68642
+ kind: "leave",
68643
+ item: item.name,
68644
+ at: short$1(source.entries),
68645
+ ...recorded === ours ? {} : { upkeep: change }
68646
+ };
68647
+ const to = short$1(source.entries);
68648
+ if (current === void 0) return {
68649
+ kind: "change",
68650
+ item: item.name,
68651
+ to,
68652
+ change
68653
+ };
68654
+ if (kindOf(current) !== kindOf(source.entries)) return {
68655
+ kind: "fail",
68656
+ item: item.name,
68657
+ at: short$1(current),
68658
+ reason: `${kindOf(current)} here, ${kindOf(source.entries)} on ${from}`
68659
+ };
68660
+ const fromAt = short$1(current);
68661
+ const why = askWhy(current, record, source, from);
68662
+ if (why === void 0) return {
68663
+ kind: "change",
68664
+ item: item.name,
68665
+ from: fromAt,
68666
+ to,
68667
+ change
68668
+ };
68669
+ if (pulling) return {
68670
+ kind: "skip",
68671
+ item: item.name,
68672
+ at: fromAt,
68673
+ reason: `pulling it from ${answer?.slice(5)}`
68674
+ };
68675
+ if (answer === "skip") return {
68676
+ kind: "skip",
68677
+ item: item.name,
68678
+ at: fromAt,
68679
+ reason: why
68680
+ };
68681
+ if (answer === "overwrite") return {
68682
+ kind: "change",
68683
+ item: item.name,
68684
+ from: fromAt,
68685
+ to,
68686
+ reason: why,
68687
+ change
68688
+ };
68689
+ return {
68690
+ kind: "ask",
68691
+ item: item.name,
68692
+ at: fromAt,
68693
+ question: fileQuestion(subject, fromAt, why, resolved.openssl ? mine : void 0, from)
68694
+ };
68695
+ };
68696
+ const CIPHER = "-aes-256-cbc -pbkdf2 -a -A";
68697
+ const writeRecord = (name, entries, at) => {
68698
+ const file = recordFile("files", name);
68699
+ const temporary = `${recordDir("files")}/${quote(`${name}.json.tmp`)}`;
68700
+ return [
68701
+ `mkdir -p ${recordDir("files")}`,
68702
+ `ff_lit ${quote(jsonRecord({
68703
+ at,
68704
+ entries: Object.fromEntries(entries)
68705
+ }))} > ${temporary}`,
68706
+ `mv -f ${temporary} ${file}`
68707
+ ];
68708
+ };
68709
+ const copyStep = (change) => {
68710
+ const { item, entries, current, contents } = change;
68711
+ const typeOf = (entry) => entry?.[0];
68712
+ const deletes = [...current ?? []].filter(([rel, entry]) => {
68713
+ const ours = entries.get(rel);
68714
+ return ours === void 0 || typeOf(ours) !== typeOf(entry);
68715
+ }).toSorted(([a], [b]) => depth(b) - depth(a));
68716
+ const writes = [...entries].filter(([rel, entry]) => current?.get(rel) !== entry).toSorted(([a], [b]) => depth(a) - depth(b));
68717
+ const input = change.key === void 0 ? [] : [change.key];
68718
+ const decode = change.key === void 0 ? "base64 -d" : `FF_KEY="$ff_input_0" openssl enc -d ${CIPHER} -pass env:FF_KEY`;
68719
+ const write = ([rel, entry]) => {
68720
+ const [type, mode = ""] = entry.split(" ");
68721
+ if (type === "d") return [`(umask 077 && mkdir -p ${at(rel)})`];
68722
+ if (type === "l") return [`rm -f ${at(rel)}`, `ln -s -- ${quote(entry.slice(2))} ${at(rel)}`];
68723
+ const n = input.push(contents.get(rel) ?? new Hidden("")) - 1;
68724
+ return [
68725
+ `ff_t=$(mktemp "$(dirname ${at(rel)})/.fftmp.XXXXXX")`,
68726
+ `printf '%s' "$ff_input_${n}" | ${decode} > "$ff_t"`,
68727
+ `chmod ${mode} "$ff_t"`,
68728
+ `mv -f "$ff_t" ${at(rel)}`,
68729
+ `ff_t=""`
68730
+ ];
68731
+ };
68732
+ const modes = writes.filter(([, entry]) => entry.startsWith("d ")).toReversed().map(([rel, entry]) => `chmod ${entry.slice(2)} ${at(rel)}`);
68733
+ const links = [...entries].filter(([, entry]) => entry.startsWith("l ")).map(([rel]) => rel);
68734
+ const reached = short$1(entries);
68735
+ return {
68736
+ id: `files:${item.name}:copy`,
68737
+ items: [item.name],
68738
+ doing: "copying",
68739
+ run: [
68740
+ `ff_p=${quotePath(item.path)}`,
68741
+ "(",
68742
+ " set -e",
68743
+ ` ff_t=""`,
68744
+ ` trap '[ -z "$ff_t" ] || rm -f "$ff_t"' EXIT`,
68745
+ ` trap 'exit 1' HUP INT TERM`,
68746
+ ...[
68747
+ `mkdir -p "$(dirname "$ff_p")"`,
68748
+ ...deletes.map(([rel, entry]) => entry.startsWith("d ") ? `rmdir ${at(rel)}` : `rm -f ${at(rel)}`),
68749
+ ...writes.flatMap(write),
68750
+ ...modes,
68751
+ ...writeRecord(item.name, entries, change.at)
68752
+ ].map((l) => ` ${l}`),
68753
+ ")",
68754
+ `ff_rc=$?`,
68755
+ `if [ "$ff_rc" -eq 0 ]; then`,
68756
+ ` ff_dl=""`,
68757
+ ...links.map((rel) => ` [ -e ${at(rel)} ] || ff_dl="$ff_dl, "${quote(rel)}`),
68758
+ ` if [ -n "$ff_dl" ]; then ff_at files ${quote(item.name)} "${reached}, dangling link: \${ff_dl#, }"; else ff_at files ${quote(item.name)} ${reached}; fi`,
68759
+ "fi",
68760
+ `(exit "$ff_rc")`
68761
+ ],
68762
+ input
68763
+ };
68764
+ };
68765
+ const fetchFiles = (copy, rels) => gen(function* () {
68766
+ const key = [...globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("");
68767
+ const contents = /* @__PURE__ */ new Map();
68768
+ let finished = false;
68769
+ const code = yield* runScript({
68770
+ name: copy.machine,
68771
+ local: false
68772
+ }, [
68773
+ prelude,
68774
+ `IFS= read -r FF_KEY || FF_KEY=""`,
68775
+ "export FF_KEY",
68776
+ `ff_p=${quotePath(copy.item.path)}`,
68777
+ ...rels.map((rel, n) => `ff_c=$(openssl enc ${CIPHER} -pass env:FF_KEY -in ${at(rel)}) || exit 1; printf 'ff-file ${n} %s\\n' "$ff_c"`),
68778
+ `printf 'ff-pulled\\n'`
68779
+ ].join("\n"), [key], (line) => sync(() => {
68780
+ const match = /^ff-file (\d+) (.*)$/.exec(line);
68781
+ if (match !== null) contents.set(Number(match[1]), new Hidden(match[2] ?? ""));
68782
+ else if (line === "ff-pulled") finished = true;
68783
+ }));
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;
68806
+ const step = copyStep({
68807
+ item: source.item,
68808
+ entries: copy.entries,
68809
+ ...source.kind === "present" ? { current: source.entries } : {},
68810
+ contents: fetched.contents,
68811
+ at: copy.record?.at ?? resolved.at,
68812
+ key: fetched.key
68813
+ });
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}` };
68892
+ }).pipe(catch_((e) => succeed$3({ error: e.message })));
68893
+ const recordStep = (change) => ({
68894
+ id: `files:${change.item.name}:record`,
68895
+ items: [change.item.name],
68896
+ doing: "recording",
68897
+ run: [
68898
+ "(",
68899
+ " set -e",
68900
+ ...writeRecord(change.item.name, change.entries, change.at).map((l) => ` ${l}`),
68901
+ ")"
68902
+ ]
68903
+ });
68904
+ const files = {
68905
+ name: "files",
68906
+ rule: "record",
68907
+ dependsOn: [],
68908
+ items: (_section, listed) => listed.map(({ name, def }) => ({
68909
+ name,
68910
+ path: def.path,
68911
+ exclude: def.exclude ?? []
68912
+ })),
68913
+ observe: (items) => observe$1(items),
68914
+ parse,
68915
+ resolve: (items, { starting: all, checked }) => gen(function* () {
68916
+ const at = formatIso(yield* now);
68917
+ const needed = new Set(items.map((item) => item.name));
68918
+ const starting = {
68919
+ ...all,
68920
+ items: all.items.filter((item) => needed.has(item.name))
68921
+ };
68922
+ const { sources, openssl } = starting.items.length === 0 ? {
68923
+ sources: /* @__PURE__ */ new Map(),
68924
+ openssl: false
68925
+ } : yield* resolveSources(starting.items, starting).pipe(timeout("2 minutes"), orElseSucceed(() => ({
68926
+ sources: new Map(starting.items.map((item) => [item.name, {
68927
+ item,
68928
+ kind: "unreadable",
68929
+ reason: "could not read it here"
68930
+ }])),
68931
+ openssl: false
68932
+ })));
68933
+ return {
68934
+ from: starting.machine.name,
68935
+ at,
68936
+ sources,
68937
+ copies: copiesOf(checked, starting.machine.name),
68938
+ openssl
68939
+ };
68940
+ }),
68941
+ decide: ({ items, observed, resolved, ctx }) => items.map((item) => decideFile(item, observed, resolved, ctx)),
68942
+ act: (decisions) => decisions.flatMap((d) => d.kind === "change" ? [copyStep(d.change)] : d.kind === "leave" && d.upkeep !== void 0 ? [recordStep(d.upkeep)] : []),
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
68945
+ };
67926
68946
  const decodeFact$1 = decodeUnknownOption(Union([
67927
68947
  Struct({
67928
68948
  fact: Literal("cloneRoot"),
@@ -68353,6 +69373,7 @@ const decideRanged = (item, observed, resolved, ctx) => {
68353
69373
  //#endregion
68354
69374
  //#region ../../packages/providers/src/index.ts
68355
69375
  const PROVIDERS = [
69376
+ files,
68356
69377
  {
68357
69378
  name: "vp",
68358
69379
  rule: "range",
@@ -68525,6 +69546,25 @@ const PROVIDERS = [
68525
69546
  repos
68526
69547
  ];
68527
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
68528
69568
  //#region src/live.tsx
68529
69569
  const cell = (initial) => {
68530
69570
  let value = initial;
@@ -68542,6 +69582,10 @@ const cell = (initial) => {
68542
69582
  };
68543
69583
  };
68544
69584
  const LiveLedger = (props) => {
69585
+ const { suspendTerminal } = useApp();
69586
+ (0, import_react.useEffect)(() => {
69587
+ props.suspend.current = suspendTerminal;
69588
+ }, [props.suspend, suspendTerminal]);
68545
69589
  const { state, now } = (0, import_react.useSyncExternalStore)(props.store.subscribe, props.store.get);
68546
69590
  const asking = (0, import_react.useSyncExternalStore)(props.asking.subscribe, props.asking.get);
68547
69591
  const [full, setFull] = (0, import_react.useState)(false);
@@ -68581,9 +69625,11 @@ const LiveLedger = (props) => {
68581
69625
  };
68582
69626
  const startLive = fn("startLive")(function* (store, color) {
68583
69627
  const asking = cell(void 0);
69628
+ const suspend = {};
68584
69629
  const instance = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LiveLedger, {
68585
69630
  store,
68586
69631
  asking,
69632
+ suspend,
68587
69633
  color
68588
69634
  }), { exitOnCtrlC: false });
68589
69635
  yield* addFinalizer(() => promise(async () => {
@@ -68592,20 +69638,29 @@ const startLive = fn("startLive")(function* (store, color) {
68592
69638
  await instance.waitUntilExit();
68593
69639
  }));
68594
69640
  yield* forkScoped(repeat$1(store.tick, spaced(`${SPINNER.interval} millis`)));
68595
- return { ask: process.stdin.isTTY ? (choices) => callback$1((resume) => {
68596
- asking.set({
68597
- titles: choices.map((c) => c.title),
68598
- answer: (index) => {
68599
- const choice = choices[index];
68600
- if (choice === void 0) return;
68601
- asking.set(void 0);
68602
- resume(succeed$3(choice.value));
68603
- }
68604
- });
68605
- return sync(() => asking.set(void 0));
68606
- }) : 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
+ };
68607
69659
  });
68608
- const noLive = { ask: void 0 };
69660
+ const noLive = {
69661
+ ask: void 0,
69662
+ aside: (effect) => effect
69663
+ };
68609
69664
  const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
68610
69665
  //#endregion
68611
69666
  //#region src/machine.ts
@@ -68760,11 +69815,18 @@ const askQuestions = fn("askQuestions")(function* (input) {
68760
69815
  yield* input.ledger.note("answer", "not asked (dry run)");
68761
69816
  continue;
68762
69817
  }
68763
- const choices = question.choices.map((c) => ({
69818
+ const choices = [...new Map(group.flatMap((a) => a.question.choices).map((c) => [c.value, c])).values()].map((c) => ({
68764
69819
  value: c.value,
68765
69820
  title: c.value === "bump" && bumps.length > 0 ? `bump the config to ${bumps.map((b) => bumpedRange(b.range, b.newest)).join(" and ")}` : c.title
68766
69821
  }));
68767
- 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
+ }
68768
69830
  yield* input.ledger.note("answer", choices.find((c) => c.value === answer)?.title ?? answer);
68769
69831
  answered.push({
68770
69832
  question,
@@ -68797,27 +69859,43 @@ const unshippable = (config, tailnet, only, source) => {
68797
69859
  const outsideFleet = only.filter((name) => !fleet.has(name));
68798
69860
  if (outsideFleet.length > 0) return `not in machines in ${source}: ${outsideFleet.join(", ")}`;
68799
69861
  };
68800
- const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger) {
69862
+ const minute = (iso) => iso?.slice(0, 16).replace("T", " ") ?? "an unknown time";
69863
+ const staleConfigs = fn("staleConfigs")(function* (text, base, own, checked, ledger) {
68801
69864
  const copies = [];
68802
69865
  for (const { machine, state } of checked) {
68803
- if (state.config === void 0) continue;
68804
- const parsed = yield* result(parseConfig(machine.name, state.config));
68805
- if (parsed._tag === "Failure") yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
68806
- else copies.push({
69866
+ if (machine.name === own || state.config === void 0 || state.config.text === text) continue;
69867
+ const parsed = yield* result(parseConfig(machine.name, state.config.text));
69868
+ if (parsed._tag === "Failure") {
69869
+ yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
69870
+ continue;
69871
+ }
69872
+ const record = getOrUndefined$1(decodeConfigRecord(state.configRecord));
69873
+ const why = staleWhy({
69874
+ machine: machine.name,
69875
+ sha: state.config.sha,
69876
+ record
69877
+ }, base);
69878
+ if (why === void 0) continue;
69879
+ copies.push({
68807
69880
  machine: machine.name,
69881
+ text: state.config.text,
69882
+ sha: state.config.sha,
68808
69883
  config: parsed.success,
68809
- text: state.config
69884
+ record,
69885
+ why: why === "unrecorded" ? "differs, and no Ship wrote it" : why === "edited" ? "was edited there" : `got a Ship from ${parseShipId(record?.ship ?? "")?.from ?? "another Machine"} at ${minute(record?.reached[machine.name])} that missed this Machine`
68810
69886
  });
68811
69887
  }
68812
- return [...Map.groupBy(staleCopies(config, copies, own), (c) => c.text)].flatMap(([text, [first, ...rest]]) => {
69888
+ return [...Map.groupBy(copies, (c) => c.text)].flatMap(([text, group]) => {
69889
+ const [first] = group;
68813
69890
  if (first === void 0) return [];
68814
- const { config, machine } = first;
68815
- const machines = [machine, ...rest.map((c) => c.machine)];
69891
+ const machines = group.map((c) => c.machine);
68816
69892
  return [{
68817
69893
  text,
68818
- config,
69894
+ sha: first.sha,
69895
+ config: first.config,
68819
69896
  machines,
68820
- description: `${machines.join(", ")} (shipped ${config.shippedAt} from ${config.shippedFrom ?? "unknown"})`
69897
+ reached: mergeReached(group.flatMap((c) => c.record === void 0 ? [] : [c.record.reached])),
69898
+ description: `the config on ${machines.join(", ")} ${first.why}`
68821
69899
  }];
68822
69900
  });
68823
69901
  });
@@ -68832,10 +69910,10 @@ const ship = fn("ship")(function* (flags) {
68832
69910
  yield* logInfo(`Ship ${id}: fleetfizz ${flags.version}, node ${process.version} on ${process.platform}`);
68833
69911
  yield* logInfo(`fizz ${process.argv.slice(2).join(" ")}`);
68834
69912
  for (const m of tailnet.fleet) yield* logInfo(`tailnet: ${m.name} ${m.platform}${m.online ? "" : " offline"}${m.ssh ? "" : " no-ssh"}${m.name === tailnet.self.name ? " (this Machine)" : ""}`);
68835
- while ((yield* shipPass(flags, tailnet, hidden)) === "adopted");
69913
+ while ((yield* shipPass(flags, tailnet, hidden, id)) === "adopted");
68836
69914
  }).pipe(onExit((exit) => isSuccess(exit) ? logInfo("the Ship ended") : logError("the Ship stopped", exit.cause)), provide(layer$1(logger === void 0 ? [] : [logger])), provideService(MinimumLogLevel, "Debug"));
68837
69915
  }, scoped);
68838
- const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
69916
+ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
68839
69917
  const fs = yield* FileSystem;
68840
69918
  const parsed = parseAnswers(flags.answers);
68841
69919
  if ("invalid" in parsed) return yield* fail$1(new ShipError(parsed.invalid));
@@ -68853,6 +69931,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68853
69931
  if (problem !== void 0) return yield* fail$1(new ShipError(problem));
68854
69932
  const fleet = new Set(config.machines);
68855
69933
  const own = flags.config === void 0 ? tailnet.self.name : void 0;
69934
+ const base = (own === void 0 ? void 0 : yield* readConfigRecord)?.reached ?? {};
68856
69935
  const order = config.machines;
68857
69936
  const targetsList = tailnet.fleet.filter((m) => fleet.has(m.name) && (flags.only.length === 0 || flags.only.includes(m.name))).toSorted((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
68858
69937
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
@@ -68923,13 +70002,13 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68923
70002
  machine: machine.name,
68924
70003
  phase: "check"
68925
70004
  })), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
68926
- const stale = yield* staleConfigs(config, own, checked, ledger);
70005
+ const stale = yield* staleConfigs(yield* fs.readFileString(path), base, own, checked, ledger);
68927
70006
  if (stale.length > 0) {
68928
- const what = `newer config on ${stale.map((s) => s.description).join(", ")}`;
70007
+ const what = stale.map((s) => s.description).join("; ");
68929
70008
  const answers = stale.flatMap((s) => s.machines).map((m) => given("config", m));
68930
70009
  if (flags.dryRun) yield* ledger.note("info", `${what}; a Ship would ask before overwriting it`);
68931
70010
  else if (answers.every((a) => a === "overwrite")) yield* ledger.note("info", `${what}; overwriting it, as answered`);
68932
- else if (answers.includes("stop")) return yield* fail$1(new ShipError("aborted: the config here is older"));
70011
+ else if (answers.includes("stop")) return yield* fail$1(new ShipError("stopped before overwriting the config there"));
68933
70012
  else if (live.ask === void 0) return yield* fail$1(new ShipError(`${what}; copy it here first, or pass --overwrite-config`));
68934
70013
  else {
68935
70014
  const usable = [];
@@ -68955,12 +70034,17 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68955
70034
  ]);
68956
70035
  if (choice === "stop") {
68957
70036
  yield* ledger.note("answer", "stop");
68958
- return yield* fail$1(new ShipError("aborted: the config here is older"));
70037
+ return yield* fail$1(new ShipError("stopped before overwriting the config there"));
68959
70038
  }
68960
70039
  if (choice === "overwrite") yield* ledger.note("answer", "overwrite it");
68961
70040
  else {
68962
70041
  yield* ledger.note("answer", `use the config on ${choice.machines[0]}`);
68963
70042
  yield* fs.writeFileString(path, choice.text);
70043
+ if (own !== void 0) yield* writeConfigRecord(encodeConfigRecord({
70044
+ ship: id,
70045
+ sha: choice.sha,
70046
+ reached: reachedAfter(mergeReached([base, choice.reached]), formatIso(yield* now), [own], choice.config.machines)
70047
+ }));
68964
70048
  adopted = true;
68965
70049
  return "adopted";
68966
70050
  }
@@ -68975,7 +70059,30 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68975
70059
  if (lent.token !== void 0) hidden.add(lent.token);
68976
70060
  yield* logInfo(lent.token !== void 0 ? "gh token: lent" : `gh token: none (${lent.reason ?? "no repos"})`);
68977
70061
  if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
68978
- const resolved = Object.fromEntries(yield* forEach$1(targeted, ({ provider, items }) => provider.resolve(items, lent.token === void 0 ? {} : { token: lent.token }).pipe(map$5((r) => [provider.name, r])), { concurrency: "unbounded" }));
70062
+ const self = {
70063
+ name: tailnet.self.name,
70064
+ platform: tailnet.self.platform
70065
+ };
70066
+ const resolved = Object.fromEntries(yield* forEach$1(targeted, ({ provider, items }) => provider.resolve(items, {
70067
+ ...lent.token === void 0 ? {} : { token: lent.token },
70068
+ starting: {
70069
+ machine: self,
70070
+ config,
70071
+ items: targetedItems(provider, config, self)
70072
+ },
70073
+ checked: checked.map(({ machine, state }) => {
70074
+ const identity = {
70075
+ name: machine.name,
70076
+ platform: machine.platform
70077
+ };
70078
+ return {
70079
+ machine: identity,
70080
+ items: targetedItems(provider, config, identity),
70081
+ facts: state.facts[provider.name] ?? [],
70082
+ records: state.records[provider.name] ?? {}
70083
+ };
70084
+ })
70085
+ }).pipe(map$5((r) => [provider.name, r])), { concurrency: "unbounded" }));
68979
70086
  yield* ledger.resolved;
68980
70087
  const planAll = (cfg, answer) => checked.map((g) => ({
68981
70088
  ...g,
@@ -68998,6 +70105,18 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68998
70105
  let cfg = config;
68999
70106
  const decided = /* @__PURE__ */ new Map();
69000
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}`)));
69001
70120
  let planned = planAll(cfg, answer);
69002
70121
  for (;;) {
69003
70122
  const asked = planned.flatMap((g) => g.plan.providers.flatMap((pp) => pp.decisions.flatMap((d) => d.kind === "ask" ? [{
@@ -69009,6 +70128,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
69009
70128
  answer,
69010
70129
  ledger,
69011
70130
  ask: live.ask,
70131
+ diff,
69012
70132
  dryRun: flags.dryRun
69013
70133
  });
69014
70134
  let progressed = false;
@@ -69027,33 +70147,55 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
69027
70147
  if (!progressed) break;
69028
70148
  planned = planAll(cfg, answer);
69029
70149
  }
69030
- for (const { machine, plan: p } of planned) yield* ledger.plan(machine.name, p);
70150
+ const adopting = [...decided].flatMap(([key, choice]) => {
70151
+ const subject = key.slice(key.indexOf(":") + 1);
70152
+ const at = parseAddress(subject);
70153
+ const provider = providers.find((p) => p.name === at?.provider);
70154
+ const run = at === void 0 || provider === void 0 ? void 0 : provider.adopt?.(at.item, choice, resolved[provider.name]);
70155
+ return run === void 0 ? [] : [{
70156
+ subject,
70157
+ run
70158
+ }];
70159
+ });
70160
+ if (adopting.length > 0 && !flags.dryRun) {
70161
+ for (const { subject, run } of adopting) {
70162
+ changing = true;
70163
+ const result = yield* run;
70164
+ if ("error" in result) return yield* fail$1(new ShipError(`${subject}: ${result.error}`));
70165
+ yield* ledger.note("info", `${subject}: ${result.done}; starting over`);
70166
+ }
70167
+ adopted = true;
70168
+ return "adopted";
70169
+ }
70170
+ const text = yield* fs.readFileString(path);
70171
+ const sha = yield* shaOf(text);
70172
+ for (const { machine, state, plan: p } of planned) yield* ledger.plan(machine.name, p, {
70173
+ ...state.config === void 0 ? {} : { from: state.config.sha.slice(0, 7) },
70174
+ to: sha.slice(0, 7)
70175
+ });
69031
70176
  if (flags.dryRun) return;
69032
- const shippedAt = formatIso(yield* now);
69033
- changing = true;
69034
- const original = yield* fs.readFileString(path);
69035
- const stampWith = (reached) => stampText(original, {
69036
- shippedAt,
69037
- shippedFrom: tailnet.self.name,
69038
- reached,
69039
- own
70177
+ const at = formatIso(yield* now);
70178
+ const recordFor = (reached) => encodeConfigRecord({
70179
+ ship: id,
70180
+ sha,
70181
+ reached: reachedAfter(base, at, [...reached, ...own === void 0 ? [] : [own]], cfg.machines)
69040
70182
  });
69041
- const stamped = stampWith(planned.map((g) => g.machine.name));
69042
- yield* fs.writeFileString(path, stamped);
69043
- yield* logInfo(`stamped the config: shippedAt ${shippedAt}`);
70183
+ changing = true;
70184
+ const record = recordFor(planned.map((g) => g.machine.name));
70185
+ if (own !== void 0) yield* writeConfigRecord(record);
70186
+ yield* logInfo(`the config's record: ${record}`);
69044
70187
  const schema = `${schemaJson(fleetConfigJsonSchema())}\n`;
69045
- const stdin = applyStdin(lent.token, stamped, schema);
69046
70188
  const delivered = yield* forEach$1(planned, fn("applyOne")(function* ({ machine, local, plan: p }) {
69047
70189
  return (yield* apply({
69048
70190
  name: machine.name,
69049
70191
  local
69050
- }, p, applyScript(p), stdin, ledger)).done.includes("config") ? machine.name : void 0;
70192
+ }, p, applyScript(p, record), applyStdin(lent.token, text, schema, p), ledger)).done.includes("config") ? machine.name : void 0;
69051
70193
  }, (effect, { machine }) => annotateLogs(effect, {
69052
70194
  machine: machine.name,
69053
70195
  phase: "apply"
69054
70196
  })), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((name) => name !== void 0)));
69055
70197
  yield* logInfo(`the config reached: ${delivered.join(", ")}`);
69056
- yield* fs.writeFileString(path, stampWith(delivered));
70198
+ if (own !== void 0) yield* writeConfigRecord(recordFor(delivered));
69057
70199
  }, scoped);
69058
70200
  //#endregion
69059
70201
  //#region src/command.ts