fleetfizz 0.9.0 → 0.10.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 +1127 -157
  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";
@@ -7809,6 +7993,15 @@ const sleep$1 = (duration) => clockWith$1((clock) => clock.sleep(fromInputUnsafe
7809
7993
  /** @internal */
7810
7994
  const currentTimeMillis$1 = /*#__PURE__*/ clockWith$1((clock) => clock.currentTimeMillis);
7811
7995
  /** @internal */
7996
+ const TimeoutErrorTypeId = "~effect/Cause/TimeoutError";
7997
+ /** @internal */
7998
+ var TimeoutError = class extends (/*#__PURE__*/ TaggedError$2("TimeoutError")) {
7999
+ [TimeoutErrorTypeId] = TimeoutErrorTypeId;
8000
+ constructor(message) {
8001
+ super({ message });
8002
+ }
8003
+ };
8004
+ /** @internal */
7812
8005
  const IllegalArgumentErrorTypeId = "~effect/Cause/IllegalArgumentError";
7813
8006
  /** @internal */
7814
8007
  var IllegalArgumentError$1 = class extends (/*#__PURE__*/ TaggedError$2("IllegalArgumentError")) {
@@ -11846,6 +12039,45 @@ const ignore = ignore$1;
11846
12039
  */
11847
12040
  const orElseSucceed = orElseSucceed$1;
11848
12041
  /**
12042
+ * Adds a time limit to an effect, triggering a timeout if the effect exceeds
12043
+ * the duration.
12044
+ *
12045
+ * **When to use**
12046
+ *
12047
+ * Use when you need a timeout of an `Effect` to be represented as a typed
12048
+ * failure.
12049
+ *
12050
+ * **Details**
12051
+ *
12052
+ * The `timeout` function allows you to specify a time limit for an
12053
+ * effect's execution. If the effect does not complete within the given time, a
12054
+ * `TimeoutError` is raised. This can be useful for controlling how long your
12055
+ * program waits for a task to finish, ensuring that it doesn't hang
12056
+ * indefinitely if the task takes too long.
12057
+ *
12058
+ * **Gotchas**
12059
+ *
12060
+ * If the timeout wins, the source effect is interrupted.
12061
+ *
12062
+ * **Example** (Failing when work takes too long)
12063
+ *
12064
+ * ```ts import.meta.vitest
12065
+ * import { Effect } from "effect"
12066
+ *
12067
+ * const timedEffect = Effect.never.pipe(Effect.timeout(0))
12068
+ * const error = await Effect.runPromise(Effect.flip(timedEffect))
12069
+ * error._tag // => "TimeoutError"
12070
+ * ```
12071
+ *
12072
+ * @see {@link timeoutOption} for returning `Option.none` on timeout.
12073
+ * @see {@link timeoutOrElse} for a version that allows specifying both success and timeout handlers.
12074
+ *
12075
+ * @stability stable
12076
+ * @category delays & timeouts
12077
+ * @since 2.0.0
12078
+ */
12079
+ const timeout = timeout$1;
12080
+ /**
11849
12081
  * Applies a timeout to an effect, lazily evaluating `orElse` after interrupting
11850
12082
  * the source if the timeout is reached.
11851
12083
  *
@@ -18066,7 +18298,7 @@ const defaultLeafHook = (issue) => {
18066
18298
  */
18067
18299
  const defaultCheckHook = (issue) => findMessage(issue.issue) ?? findMessage(issue);
18068
18300
  function formatInput(issue) {
18069
- return hasInput(issue) ? format$2(issue.input) : void 0;
18301
+ return hasInput(issue) ? format$3(issue.input) : void 0;
18070
18302
  }
18071
18303
  function findExpected(issue) {
18072
18304
  const expected = issue.annotations?.expected;
@@ -18978,9 +19210,9 @@ const decodeJsonError = (input) => {
18978
19210
  const encodeUnknownAsJson = (input) => {
18979
19211
  try {
18980
19212
  const json = formatJson(input);
18981
- return json === void 0 ? format$2(input) : JSON.parse(json);
19213
+ return json === void 0 ? format$3(input) : JSON.parse(json);
18982
19214
  } catch {
18983
- return format$2(input);
19215
+ return format$3(input);
18984
19216
  }
18985
19217
  };
18986
19218
  const encodeJsonError = (input, options, encodeDefect) => {
@@ -19346,7 +19578,7 @@ const Literal$1 = class extends ASTNodeImpl {
19346
19578
  literal;
19347
19579
  constructor(literal, annotations, checks, encoding, context) {
19348
19580
  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)}`);
19581
+ if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) throw new Error(`A numeric literal must be finite, got ${format$3(literal)}`);
19350
19582
  this.literal = literal;
19351
19583
  }
19352
19584
  /** @internal */
@@ -25686,7 +25918,7 @@ function isPattern(regExp, annotations) {
25686
25918
  const source = regExp.source;
25687
25919
  const flags = regExp.flags;
25688
25920
  const canExport = /^[dg]*uy?$/.test(flags);
25689
- const runtimeRegExp = flags === "" ? `new RegExp(${format$2(source)})` : `new RegExp(${format$2(source)}, ${format$2(flags)})`;
25921
+ const runtimeRegExp = flags === "" ? `new RegExp(${format$3(source)})` : `new RegExp(${format$3(source)}, ${format$3(flags)})`;
25690
25922
  return isPattern$1(regExp, {
25691
25923
  toJsonSchema: () => canExport ? { pattern: flags.endsWith("y") ? `^(?:${source})` : source } : [{}, true],
25692
25924
  toCode: () => ({ runtime: `Schema.isPattern(${runtimeRegExp})` }),
@@ -27118,7 +27350,7 @@ const String$5 = /*#__PURE__*/ makePrimitive("String", (value) => succeed$3(valu
27118
27350
  */
27119
27351
  const Choice = (choices) => {
27120
27352
  const choiceMap = new Map(choices);
27121
- const validChoices = choices.map(([key]) => format$2(key)).join(" | ");
27353
+ const validChoices = choices.map(([key]) => format$3(key)).join(" | ");
27122
27354
  const primitive = makePrimitive("Choice", (value) => {
27123
27355
  if (choiceMap.has(value)) return succeed$3(choiceMap.get(value));
27124
27356
  return fail$1(validChoices);
@@ -33032,7 +33264,7 @@ const runWith = (command, config) => {
33032
33264
  };
33033
33265
  //#endregion
33034
33266
  //#region package.json
33035
- var version = "0.9.0";
33267
+ var version = "0.10.0";
33036
33268
  //#endregion
33037
33269
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Logger.js
33038
33270
  const TypeId = "~effect/Logger";
@@ -33877,7 +34109,7 @@ var ParseOptions;
33877
34109
  * 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
34110
  * Therefore always check the errors list to find out if the input was valid.
33879
34111
  */
33880
- function parse$1(text, errors = [], options = ParseOptions.DEFAULT) {
34112
+ function parse$2(text, errors = [], options = ParseOptions.DEFAULT) {
33881
34113
  let currentProperty = null;
33882
34114
  let currentParent = [];
33883
34115
  const previousParents = [];
@@ -34453,7 +34685,7 @@ var SyntaxKind;
34453
34685
  * 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
34686
  * Therefore, always check the errors list to find out if the input was valid.
34455
34687
  */
34456
- const parse = parse$1;
34688
+ const parse$1 = parse$2;
34457
34689
  /**
34458
34690
  * 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
34691
  */
@@ -35989,6 +36221,7 @@ const Origin = String$6.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35989
36221
  const Ref = NonEmptyString;
35990
36222
  const isPin = (ref) => /^[0-9a-f]{40}$/.test(ref);
35991
36223
  const ProviderName = Literals$2([
36224
+ "files",
35992
36225
  "vp",
35993
36226
  "brew",
35994
36227
  "cask",
@@ -36072,12 +36305,41 @@ const ReposSection = section({ cloneRoot: optionalKey(variants(withShorthand(Str
36072
36305
  decode: (path) => ({ path }),
36073
36306
  encode: (root) => root.path
36074
36307
  }))).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}.`);
36308
+ const HomePath = NonEmptyString.check(isPattern(/^(\/|~\/)./, {
36309
+ title: "path",
36310
+ description: "an absolute path, or one under ~"
36311
+ }));
36312
+ const OWN_DIRS = ["~/.config/fleetfizz", "~/.local/state/fleetfizz"];
36313
+ const SharedFilePath = HomePath.check(makeFilter((path) => {
36314
+ const p = path.replace(/\/+$/, "");
36315
+ const dir = OWN_DIRS.find((d) => p === "~" || d === p || d.startsWith(`${p}/`) || p.startsWith(`${d}/`));
36316
+ return dir === void 0 || `overlaps ${dir}, which a Ship writes itself`;
36317
+ }, {
36318
+ title: "Shared File path",
36319
+ description: "a path outside fleetfizz's own directories"
36320
+ }));
36321
+ const SharedFile = Struct({
36322
+ ...Targeting,
36323
+ path: SharedFilePath.annotateKey({ description: "A file or directory, absolute or under `~`, which expands on each Machine." }),
36324
+ 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." })
36325
+ });
36326
+ const sameExclude = (a, b) => [...new Set(a.exclude ?? [])].toSorted().join("\n") === [...new Set(b.exclude ?? [])].toSorted().join("\n");
36327
+ const FilesSection = section({}, Record(NonEmptyString, variants(withShorthand(SharedFile, SharedFilePath, {
36328
+ decode: (path) => ({ path }),
36329
+ encode: (file) => file.path
36330
+ }))).check(makeFilter((items) => {
36331
+ const badName = Object.keys(items).find((n) => !/^[A-Za-z0-9._-]+$/.test(n) || n === "." || n === "..");
36332
+ if (badName !== void 0) return `"${badName}": a Shared File's name uses only letters, digits, ., _ and -, and is not . or ..`;
36333
+ const name = Object.entries(items).find(([, item]) => Array.isArray(item) && item.some((v) => !sameExclude(v, item[0])))?.[0];
36334
+ return name === void 0 || `"${name}": every Variant needs the same exclude`;
36335
+ }, {
36336
+ title: "Shared Files",
36337
+ description: "names of letters, digits, ., _ and -, and one exclude across a Shared File's Variants"
36338
+ })), `Shared Files, keyed by name, which names the \`files:<name>\` Item. Each is a path, \`{ path, exclude?, platforms?, machines? }\`, ${VARIANTS}.`);
36075
36339
  const FleetConfig = Struct({
36076
36340
  $schema: optionalKey(String$6),
36077
- shippedAt: optionalKey(String$6),
36078
- shippedFrom: optionalKey(NonEmptyString),
36079
- shippedTo: optionalKey(Record(NonEmptyString, String$6)),
36080
36341
  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." }),
36342
+ files: optionalKey(FilesSection).annotateKey({ description: "Files and directories copied from the Starting Machine to every targeted Machine. A directory is mirrored." }),
36081
36343
  repos: optionalKey(ReposSection).annotateKey({ description: "Each Tracked Repo is cloned on every targeted Machine and installed from its origin ref." }),
36082
36344
  vp: optionalKey(VpSection).annotateKey({ description: "The vite-plus toolchain." }),
36083
36345
  brew: optionalKey(BrewSection).annotateKey({ description: "Homebrew Formulae. Needs Homebrew on the Machine; a Ship never installs it." }),
@@ -36133,10 +36395,17 @@ const repeatedKeys = (text) => {
36133
36395
  if (tree !== void 0) visit(tree, []);
36134
36396
  return problems;
36135
36397
  };
36398
+ const STAMPS = [
36399
+ "shippedAt",
36400
+ "shippedFrom",
36401
+ "shippedTo"
36402
+ ];
36136
36403
  const movedKeys = (parsed) => {
36137
36404
  if (typeof parsed !== "object" || parsed === null) return [];
36138
36405
  const config = parsed;
36406
+ const stamps = STAMPS.filter((key) => key in parsed);
36139
36407
  return [
36408
+ ...stamps.length > 0 ? [`${stamps.join(", ")}: no longer written; a Ship keeps them in its Install Record now, so delete them`] : [],
36140
36409
  ...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
36410
  ...Array.isArray(config.repos) ? ["repos is now { items: { <name>: { origin, ref, install } } }, keyed by each repo's name"] : [],
36142
36411
  ...typeof config.vp === "object" && config.vp !== null && "globals" in config.vp ? ["vp.globals is now vp.items"] : []
@@ -36144,7 +36413,7 @@ const movedKeys = (parsed) => {
36144
36413
  };
36145
36414
  const parseConfig = fn("parseConfig")(function* (path, text) {
36146
36415
  const parseErrors = [];
36147
- const parsed = parse(text, parseErrors, { allowTrailingComma: true });
36416
+ const parsed = parse$1(text, parseErrors, { allowTrailingComma: true });
36148
36417
  if (parseErrors.length > 0) return yield* new ConfigError({
36149
36418
  path,
36150
36419
  problems: parseErrors.map((e) => `line ${lineOf(text, e.offset)}: ${printParseErrorCode(e.error)}`)
@@ -36171,49 +36440,26 @@ const loadConfig = fn("loadConfig")(function* (path) {
36171
36440
  const text = yield* fs.readFileString(path);
36172
36441
  return yield* parseConfig(path, text);
36173
36442
  });
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"));
36443
+ const ConfigRecord = Struct({
36444
+ ship: NonEmptyString,
36445
+ sha: String$6,
36446
+ reached: Record(NonEmptyString, String$6)
36447
+ });
36448
+ const decodeConfigRecord = decodeUnknownOption(ConfigRecord);
36449
+ const decodeConfigRecordJson = decodeUnknownOption(fromJsonString(ConfigRecord));
36450
+ const encodeConfigRecord = encodeSync(fromJsonString(ConfigRecord));
36451
+ 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("")));
36452
+ const reachedAfter = (previous, at, machines, fleet) => ({
36453
+ ...Object.fromEntries(Object.entries(previous).filter(([name]) => fleet.includes(name))),
36454
+ ...Object.fromEntries(machines.map((name) => [name, at]))
36455
+ });
36456
+ const mergeReached = (all) => {
36457
+ const merged = {};
36458
+ for (const reached of all) for (const [name, at] of Object.entries(reached)) if (isNewer(at, merged[name])) merged[name] = at;
36459
+ return merged;
36214
36460
  };
36215
36461
  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]));
36462
+ 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
36463
  //#endregion
36218
36464
  //#region ../../packages/remote/src/shell.ts
36219
36465
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
@@ -36232,6 +36478,7 @@ const prelude = [
36232
36478
  `export PATH="$PATH:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin"`,
36233
36479
  `ff_json() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' | tr -d '\\n'; }`,
36234
36480
  `ff_state() { printf 'ff-state %s\\n' "$1"; }`,
36481
+ `ff_sha() { if command -v shasum >/dev/null 2>&1; then shasum -a 256; else sha256sum; fi | cut -d" " -f1; }`,
36235
36482
  `ff_at() { ff_state "{\\"provider\\":\\"$(ff_json "$1")\\",\\"fact\\":\\"at\\",\\"item\\":\\"$(ff_json "$2")\\",\\"at\\":\\"$(ff_json "$3")\\"}"; }`
36236
36483
  ].join("\n");
36237
36484
  const fingerprintFn = [
@@ -36250,6 +36497,7 @@ const LOGS_DIR = `${STATE_DIR}/logs`;
36250
36497
  const CONFIG_DIR_SH = "\"${XDG_CONFIG_HOME:-$HOME/.config}/fleetfizz\"";
36251
36498
  const recordDir = (provider) => `${quotePath(RECORDS_DIR)}/${quote(provider)}`;
36252
36499
  const recordFile = (provider, item) => `${recordDir(provider)}/${quote(`${item}.json`)}`;
36500
+ const CONFIG_RECORD = `${RECORDS_DIR}/config.json`;
36253
36501
  const LEGACY_RECORD_PROVIDER = "repos";
36254
36502
  const recordFact = (provider, item) => [
36255
36503
  `ff_r=${recordFile(provider, item)}`,
@@ -36266,7 +36514,7 @@ const checkScript = (input) => {
36266
36514
  lines.push(provider.observe(items, input.scope));
36267
36515
  if (provider.rule === "record") for (const item of items) lines.push(...recordFact(provider.name, item.name));
36268
36516
  }
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`);
36517
+ 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
36518
  lines.push(`ff_state '{"fact":"end"}'`);
36271
36519
  return lines.join("\n");
36272
36520
  };
@@ -36283,7 +36531,7 @@ const StepSkip = Struct({
36283
36531
  id: String$6,
36284
36532
  reason: String$6
36285
36533
  });
36286
- const decodeFact$3 = decodeUnknownResult(fromJsonString(Fact));
36534
+ const decodeFact$4 = decodeUnknownResult(fromJsonString(Fact));
36287
36535
  const decodeStart = decodeUnknownResult(fromJsonString(StepStart));
36288
36536
  const decodeDone = decodeUnknownResult(fromJsonString(StepDone));
36289
36537
  const decodeSkip = decodeUnknownResult(fromJsonString(StepSkip));
@@ -36293,7 +36541,7 @@ const DONE = "ff-done ";
36293
36541
  const SKIP = "ff-skip ";
36294
36542
  const parseLine = (line) => {
36295
36543
  if (line.startsWith(STATE)) {
36296
- const result = decodeFact$3(line.slice(9));
36544
+ const result = decodeFact$4(line.slice(9));
36297
36545
  if (result._tag === "Success") return {
36298
36546
  kind: "state",
36299
36547
  fact: result.success
@@ -36339,12 +36587,14 @@ const marked = (id, run, failed = []) => [
36339
36587
  doneLine(id, true),
36340
36588
  "fi"
36341
36589
  ];
36342
- const configStep = () => {
36590
+ const configStep = (record) => {
36343
36591
  const dir = CONFIG_DIR_SH;
36344
36592
  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)} )`]);
36593
+ const recordFile = quotePath(CONFIG_RECORD);
36594
+ const recordTmp = quotePath(`${CONFIG_RECORD}.tmp`);
36595
+ 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
36596
  };
36347
- const providerSteps = (plan) => {
36597
+ const providerSteps = (plan, offsets) => {
36348
36598
  if (plan.decisions.length === 0 && plan.steps.length === 0) return [];
36349
36599
  const held = `ff_held_${plan.name}`;
36350
36600
  const items = [...new Set(plan.steps.flatMap((step) => step.items))];
@@ -36353,6 +36603,7 @@ const providerSteps = (plan) => {
36353
36603
  `if [ -n "${s.items.map((item) => `\${${flag(item)}:-}`).join("")}" ]; then`,
36354
36604
  skipLine(s.id, "'an earlier step failed'"),
36355
36605
  "else",
36606
+ ...(s.input ?? []).map((_, i) => `ff_input_${i}="$ff_in_${(offsets.get(s) ?? 0) + i}"`),
36356
36607
  ...marked(s.id, s.run, [...s.items.map((item) => `${flag(item)}=1`), `${held}=1`]),
36357
36608
  "fi"
36358
36609
  ];
@@ -36368,10 +36619,21 @@ const providerSteps = (plan) => {
36368
36619
  "fi"
36369
36620
  ];
36370
36621
  };
36371
- const applyStdin = (token, config, schema) => [
36622
+ const inputsOf = (plan) => plan.providers.flatMap((p) => p.steps.flatMap((step) => step.input ?? []));
36623
+ const inputOffsets = (plan) => {
36624
+ const offsets = /* @__PURE__ */ new Map();
36625
+ let next = 0;
36626
+ for (const step of plan.providers.flatMap((p) => p.steps)) {
36627
+ offsets.set(step, next);
36628
+ next += step.input?.length ?? 0;
36629
+ }
36630
+ return offsets;
36631
+ };
36632
+ const applyStdin = (token, config, schema, plan) => [
36372
36633
  token ?? "",
36373
36634
  encodeBase64(config),
36374
- encodeBase64(schema)
36635
+ encodeBase64(schema),
36636
+ ...inputsOf(plan).map((input) => input.reveal())
36375
36637
  ];
36376
36638
  const encodeBase64 = (text) => {
36377
36639
  const bytes = new TextEncoder().encode(text);
@@ -36379,18 +36641,30 @@ const encodeBase64 = (text) => {
36379
36641
  for (const byte of bytes) binary += String.fromCharCode(byte);
36380
36642
  return btoa(binary);
36381
36643
  };
36382
- const applyScript = (plan) => [
36644
+ const applyScript = (plan, configRecord) => {
36645
+ const offsets = inputOffsets(plan);
36646
+ return [
36647
+ prelude,
36648
+ fingerprintFn,
36649
+ `ff_lit() { printf '%s' "$1"; }`,
36650
+ `IFS= read -r ff_token || ff_token=""`,
36651
+ `IFS= read -r ff_config || ff_config=""`,
36652
+ `IFS= read -r ff_schema || ff_schema=""`,
36653
+ ...inputsOf(plan).map((_, i) => `IFS= read -r ff_in_${i} || exit 1`),
36654
+ `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; }`,
36655
+ ...configStep(configRecord),
36656
+ ...plan.providers.flatMap((p) => providerSteps(p, offsets)),
36657
+ doneLine("end", true)
36658
+ ].join("\n");
36659
+ };
36660
+ const stepScript = (step) => [
36383
36661
  prelude,
36384
36662
  fingerprintFn,
36385
36663
  `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)
36664
+ ...(step.input ?? []).flatMap((_, i) => [`IFS= read -r ff_in_${i} || ff_in_${i}=""`, `ff_input_${i}="$ff_in_${i}"`]),
36665
+ ...marked(step.id, step.run)
36393
36666
  ].join("\n");
36667
+ const stepStdin = (step) => (step.input ?? []).map((i) => i.reveal());
36394
36668
  //#endregion
36395
36669
  //#region ../../packages/remote/src/tailnet.ts
36396
36670
  const StatusNode = Struct({
@@ -36544,6 +36818,20 @@ const resolveVersions = fn("resolveVersions")(function* (packages) {
36544
36818
  const entries = yield* forEach$1(unique, (pkg) => resolveOne(pkg).pipe(map$5((v) => [pkg, v])), { concurrency: 8 });
36545
36819
  return Object.fromEntries(entries);
36546
36820
  });
36821
+ //#endregion
36822
+ //#region src/configRecord.ts
36823
+ const recordPath = () => CONFIG_RECORD.replace(/^~/, process.env.HOME ?? "~");
36824
+ const readConfigRecord = gen(function* () {
36825
+ const text = yield* (yield* FileSystem).readFileString(recordPath()).pipe(option);
36826
+ return getOrUndefined$1(flatMap$3(text, decodeConfigRecordJson));
36827
+ });
36828
+ const writeConfigRecord = fn("writeConfigRecord")(function* (json) {
36829
+ const fs = yield* FileSystem;
36830
+ const path = recordPath();
36831
+ yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
36832
+ yield* fs.writeFileString(`${path}.tmp`, `${json}\n`);
36833
+ yield* fs.rename(`${path}.tmp`, path);
36834
+ });
36547
36835
  var LogsError = class extends Error {
36548
36836
  name = "LogsError";
36549
36837
  };
@@ -36616,7 +36904,7 @@ const logs = fn("logs")(function* (flags) {
36616
36904
  for (const id of local.toReversed()) yield* log(id);
36617
36905
  return;
36618
36906
  }
36619
- const id = flags.id ?? (yield* latestShip(local, self, targetOf));
36907
+ const id = flags.id ?? (yield* latestShip(local, self));
36620
36908
  const ship = parseShipId(id);
36621
36909
  if (ship === void 0) return yield* fail$1(new LogsError(`not a Ship id: ${id} (fizz logs --list shows them)`));
36622
36910
  let missing = false;
@@ -36627,17 +36915,13 @@ const logs = fn("logs")(function* (flags) {
36627
36915
  });
36628
36916
  if (missing) return yield* fail$1(new LogsError(`no log for ${id} on ${ship.from}; a Machine keeps its last 20`));
36629
36917
  });
36630
- const latestShip = fn("latestShip")(function* (local, self, targetOf) {
36918
+ const latestShip = fn("latestShip")(function* (local, self) {
36631
36919
  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 ?? "");
36920
+ const record = yield* readConfigRecord;
36921
+ const from = parseShipId(record?.ship ?? "")?.from;
36922
+ const at = self === void 0 ? Infinity : Date.parse(record?.reached[self] ?? "");
36635
36923
  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
- }
36924
+ if (record !== void 0 && from !== void 0 && from !== self && at > newestStarted) return record.ship;
36641
36925
  if (newest === void 0) return yield* fail$1(new LogsError("no Ship Log on this Machine yet"));
36642
36926
  return newest;
36643
36927
  });
@@ -64886,9 +65170,25 @@ const QUESTION_CHOICES = {
64886
65170
  "downgrade"
64887
65171
  ],
64888
65172
  config: ["overwrite", "stop"],
64889
- mode: ["managed", "skip"]
65173
+ mode: ["managed", "skip"],
65174
+ file: ["overwrite", "skip"]
64890
65175
  };
64891
65176
  const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
65177
+ var Hidden = class {
65178
+ #value;
65179
+ constructor(value) {
65180
+ this.#value = value;
65181
+ }
65182
+ reveal() {
65183
+ return this.#value;
65184
+ }
65185
+ toJSON() {
65186
+ return "[hidden]";
65187
+ }
65188
+ toString() {
65189
+ return "[hidden]";
65190
+ }
65191
+ };
64892
65192
  const rangeQuestion = (subject, installed, range, bumpPath) => ({
64893
65193
  kind: "range",
64894
65194
  subject,
@@ -65022,8 +65322,13 @@ const EngineFact = Union([
65022
65322
  Struct({
65023
65323
  fact: Literal("config"),
65024
65324
  present: Boolean$4,
65325
+ sha: optionalKey(String$6),
65025
65326
  base64: optionalKey(String$6)
65026
65327
  }),
65328
+ Struct({
65329
+ fact: Literal("configRecord"),
65330
+ record: Unknown
65331
+ }),
65027
65332
  Struct({
65028
65333
  fact: Literal("record"),
65029
65334
  provider: String$6,
@@ -65066,9 +65371,18 @@ const assembleState = (facts) => {
65066
65371
  };
65067
65372
  break;
65068
65373
  case "config":
65069
- if (f.present && f.base64 !== void 0) state = {
65374
+ if (f.present && f.base64 !== void 0 && f.sha !== void 0) state = {
65375
+ ...state,
65376
+ config: {
65377
+ text: decodeBase64(f.base64),
65378
+ sha: f.sha
65379
+ }
65380
+ };
65381
+ break;
65382
+ case "configRecord":
65383
+ state = {
65070
65384
  ...state,
65071
- config: decodeBase64(f.base64)
65385
+ configRecord: f.record
65072
65386
  };
65073
65387
  break;
65074
65388
  case "record": state = {
@@ -65150,6 +65464,8 @@ const MachineEntry = Struct({
65150
65464
  detail: optionalKey(String$6),
65151
65465
  output: optionalKey(ArraySchema(String$6)),
65152
65466
  items: ArraySchema(Item),
65467
+ connected: optionalKey(Boolean$4),
65468
+ checked: optionalKey(Number$2),
65153
65469
  started: optionalKey(Number$2),
65154
65470
  ended: optionalKey(Number$2)
65155
65471
  });
@@ -65158,10 +65474,14 @@ const LedgerState = Struct({
65158
65474
  dryRun: Boolean$4,
65159
65475
  notes: ArraySchema(Note),
65160
65476
  machines: ArraySchema(MachineEntry),
65477
+ resolving: optionalKey(Struct({
65478
+ started: Number$2,
65479
+ items: Number$2
65480
+ })),
65161
65481
  started: Number$2,
65162
65482
  ended: optionalKey(Number$2)
65163
65483
  });
65164
- const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65484
+ const itemsOf = (p, config) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65165
65485
  const base = {
65166
65486
  name: address(pp.name, d.item),
65167
65487
  kind: pp.rule,
@@ -65204,7 +65524,8 @@ const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65204
65524
  })), {
65205
65525
  name: CONFIG_STEP,
65206
65526
  kind: "config",
65207
- changes: false,
65527
+ ...config,
65528
+ changes: config.from !== config.to,
65208
65529
  status: "idle",
65209
65530
  steps: [{
65210
65531
  id: CONFIG_STEP,
@@ -65271,9 +65592,14 @@ const makeLedger = fn("makeLedger")(function* (input) {
65271
65592
  });
65272
65593
  yield* logInfo(`${kind}: ${text}`);
65273
65594
  }),
65274
- checked: (name) => machine(name, (m) => ({
65595
+ connected: (name) => machine(name, (m) => ({
65596
+ ...m,
65597
+ connected: true
65598
+ })).pipe(andThen(log(name, logInfo("connected")))),
65599
+ checked: (name) => machine(name, (m, now) => ({
65275
65600
  ...m,
65276
- status: "waiting"
65601
+ status: "waiting",
65602
+ checked: now
65277
65603
  })).pipe(andThen(log(name, logInfo("checked")))),
65278
65604
  skip: (name, reason) => machine(name, (m) => ({
65279
65605
  ...m,
@@ -65287,10 +65613,18 @@ const makeLedger = fn("makeLedger")(function* (input) {
65287
65613
  ...output.length === 0 ? {} : { output },
65288
65614
  ended: now
65289
65615
  })).pipe(andThen(log(name, logError(`failed: ${reason}`)))),
65290
- plan: (name, p) => machine(name, (m) => ({
65616
+ resolving: (n) => update((state, now) => ({
65617
+ ...state,
65618
+ resolving: {
65619
+ started: now,
65620
+ items: n
65621
+ }
65622
+ })).pipe(andThen(logInfo(`resolving ${n} items`))),
65623
+ resolved: update(({ resolving: _, ...state }) => state).pipe(andThen(logInfo("resolved"))),
65624
+ plan: (name, p, config) => machine(name, (m) => ({
65291
65625
  ...m,
65292
65626
  status: "planned",
65293
- items: itemsOf(p)
65627
+ items: itemsOf(p, config)
65294
65628
  })).pipe(andThen(forEach$1(p.providers.flatMap((pp) => pp.decisions.map((d) => ({
65295
65629
  pp,
65296
65630
  d
@@ -65298,6 +65632,7 @@ const makeLedger = fn("makeLedger")(function* (input) {
65298
65632
  start: (name) => machine(name, (m, now) => ({
65299
65633
  ...m,
65300
65634
  status: "running",
65635
+ connected: false,
65301
65636
  started: now
65302
65637
  })).pipe(andThen(log(name, logInfo("applying")))),
65303
65638
  stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" || item.status === "skipped" ? item : {
@@ -65348,7 +65683,7 @@ const makeLedger = fn("makeLedger")(function* (input) {
65348
65683
  ended: m.ended ?? now
65349
65684
  })).pipe(andThen(log(name, logInfo(`finished: ${outcome}`)))),
65350
65685
  end: gen(function* () {
65351
- yield* update((state, now) => ({
65686
+ yield* update(({ resolving: _, ...state }, now) => ({
65352
65687
  ...state,
65353
65688
  ended: now,
65354
65689
  machines: state.machines.map((m) => m.status === "running" || m.status === "checking" ? {
@@ -67347,7 +67682,7 @@ const SPINNER = cli_spinners_default.dots;
67347
67682
  const runGlyph = (live, now) => (live ? SPINNER.frames[Math.floor(now / SPINNER.interval) % SPINNER.frames.length] : void 0) ?? "▸";
67348
67683
  const unchanged = (i) => ({
67349
67684
  text: "",
67350
- detail: i.kind === "record" ? "up to date" : void 0
67685
+ detail: i.kind === "record" || i.kind === "config" ? "up to date" : void 0
67351
67686
  });
67352
67687
  const statusOf = (i, dryRun, glyph) => {
67353
67688
  switch (i.status) {
@@ -67366,11 +67701,11 @@ const statusOf = (i, dryRun, glyph) => {
67366
67701
  detail: i.detail
67367
67702
  };
67368
67703
  case "ok":
67704
+ if (!i.changes) return unchanged(i);
67369
67705
  if (i.kind === "config") return {
67370
67706
  text: "✓ copied",
67371
- tone: "dim"
67707
+ tone: "ok"
67372
67708
  };
67373
- if (!i.changes) return unchanged(i);
67374
67709
  if (i.kind === "record") return i.from !== void 0 && i.from === i.to ? {
67375
67710
  text: "✓ rebuilt",
67376
67711
  tone: "ok",
@@ -67402,15 +67737,23 @@ const countsOf = (m) => {
67402
67737
  skipped: m.items.filter((i) => i.status === "skipped").length
67403
67738
  };
67404
67739
  };
67405
- const settled = (m, dryRun) => dryRun || m.status === "ok" || m.status === "failed";
67740
+ const settled = (m, dryRun) => dryRun && m.status === "planned" || m.status === "ok" || m.status === "failed";
67406
67741
  const upToDate = (m, dryRun) => {
67407
67742
  const c = countsOf(m);
67408
67743
  return c.changes === 0 && c.skipped === 0 && settled(m, dryRun);
67409
67744
  };
67410
- const summaryOf = (m, dryRun) => {
67411
- if (m.status === "checking" || m.status === "waiting") return [{
67412
- text: m.status,
67413
- tone: "dim"
67745
+ const summaryOf = (m, dryRun, glyph) => {
67746
+ if (m.status === "checking") return [{
67747
+ text: `${glyph} ${m.connected === true ? "checking" : "connecting"}`,
67748
+ tone: "run"
67749
+ }];
67750
+ if (m.status === "waiting") return [{
67751
+ text: "✓ checked",
67752
+ tone: "ok"
67753
+ }];
67754
+ if (m.status === "running" && m.connected !== true) return [{
67755
+ text: `${glyph} connecting`,
67756
+ tone: "run"
67414
67757
  }];
67415
67758
  if (m.status === "skipped") return [{
67416
67759
  text: "– skipped",
@@ -67421,6 +67764,10 @@ const summaryOf = (m, dryRun) => {
67421
67764
  tone: "fail"
67422
67765
  }, { text: ` ${m.detail}` }];
67423
67766
  const { changes, done, failed, skipped } = countsOf(m);
67767
+ if (changes === 0 && skipped === 0 && m.status === "running") return [{
67768
+ text: "applying",
67769
+ tone: "dim"
67770
+ }];
67424
67771
  if (changes === 0 && skipped === 0) return [{
67425
67772
  text: "up to date",
67426
67773
  tone: settled(m, dryRun) ? "ok" : "dim"
@@ -67478,6 +67825,7 @@ const shipSummary = (state) => {
67478
67825
  }] : parts;
67479
67826
  };
67480
67827
  const elapsed = (started, ended, now) => started === void 0 ? "" : duration((ended ?? now) - started);
67828
+ const machineTime = (m, shipStarted, now) => m.status === "checking" || m.status === "waiting" ? elapsed(shipStarted, m.checked, now) : elapsed(m.started, m.ended, now);
67481
67829
  const Parts = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, { children: props.parts.map((part, n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
67482
67830
  tone: part.tone,
67483
67831
  children: part.text
@@ -67518,9 +67866,9 @@ const MachineLine = (props) => {
67518
67866
  width: w.time,
67519
67867
  right: true,
67520
67868
  tone: "dim",
67521
- children: upToDate(m, props.dryRun) ? "" : elapsed(m.started, m.ended, props.now)
67869
+ children: upToDate(m, props.dryRun) ? "" : machineTime(m, props.shipStarted, props.now)
67522
67870
  }),
67523
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: summaryOf(m, props.dryRun) })
67871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: summaryOf(m, props.dryRun, props.glyph) })
67524
67872
  ]
67525
67873
  });
67526
67874
  };
@@ -67565,8 +67913,18 @@ const LedgerView = (props) => {
67565
67913
  const { live } = (0, import_react.useContext)(ModeContext);
67566
67914
  const running = live && state.ended === void 0;
67567
67915
  const glyph = runGlyph(live, now);
67568
- const shownItems = (m) => m.items.filter((i) => full || i.changes || i.status === "failed" || i.status === "skipped");
67569
- const visible = state.machines.flatMap(shownItems);
67916
+ const shownItems = (m) => m.items.filter((i) => full || i.changes || i.status === "failed" || i.status === "skipped" || i.status === "running");
67917
+ const resolve = state.resolving === void 0 || state.ended !== void 0 ? void 0 : {
67918
+ name: "versions and commits",
67919
+ kind: "range",
67920
+ from: `${state.resolving.items} items`,
67921
+ changes: false,
67922
+ status: "running",
67923
+ doing: "resolving",
67924
+ steps: [],
67925
+ started: state.resolving.started
67926
+ };
67927
+ const visible = [...state.machines.flatMap(shownItems), ...resolve === void 0 ? [] : [resolve]];
67570
67928
  const width = (texts) => Math.max(0, ...texts.map((t) => t.length));
67571
67929
  const item = width(visible.map((i) => i.name));
67572
67930
  const status = width(visible.map((i) => statusOf(i, dryRun, glyph).text));
@@ -67574,7 +67932,7 @@ const LedgerView = (props) => {
67574
67932
  status,
67575
67933
  item,
67576
67934
  value: Math.max(width(visible.map(value)), width(state.machines.map((m) => m.name)) - (status + GAP + INDENT + item + GAP)),
67577
- time: width([...visible.map((i) => elapsed(i.started, i.ended, now)), ...state.machines.map((m) => elapsed(m.started, m.ended, now))])
67935
+ time: width([...visible.map((i) => elapsed(i.started, i.ended, now)), ...state.machines.map((m) => machineTime(m, state.started, now))])
67578
67936
  };
67579
67937
  const blocks = state.machines.map((m) => ({
67580
67938
  m,
@@ -67582,7 +67940,9 @@ const LedgerView = (props) => {
67582
67940
  m,
67583
67941
  w,
67584
67942
  dryRun,
67585
- now
67943
+ shipStarted: state.started,
67944
+ now,
67945
+ glyph
67586
67946
  }, m.name),
67587
67947
  body: [...m.output === void 0 ? [] : outputLines(m.name, m.output, nameAt(w), running), ...shownItems(m).flatMap((i) => [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ItemLine, {
67588
67948
  i,
@@ -67593,7 +67953,7 @@ const LedgerView = (props) => {
67593
67953
  }, `${m.name}/${i.kind}:${i.name}`), ...i.output === void 0 ? [] : outputLines(`${m.name}/${i.kind}:${i.name}`, i.output, nameAt(w), running)])]
67594
67954
  }));
67595
67955
  const legend = state.ended === void 0 ? `f ${full ? "changes only" : "full list"} · ctrl-c stop` : void 0;
67596
- const room = height === void 0 ? Infinity : height - (legend === void 0 ? 1 : 3);
67956
+ const room = height === void 0 ? Infinity : height - (legend === void 0 ? 1 : 3) - (resolve === void 0 ? 0 : 2);
67597
67957
  const count = (bs) => bs.reduce((n, b) => n + 1 + b.body.length, Math.max(0, bs.length - 1));
67598
67958
  const fits = count(blocks) <= room;
67599
67959
  let spare = room - count(blocks.map((b) => ({
@@ -67642,6 +68002,16 @@ const LedgerView = (props) => {
67642
68002
  tone: "dim",
67643
68003
  children: "…"
67644
68004
  }),
68005
+ resolve !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
68006
+ marginTop: 1,
68007
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ItemLine, {
68008
+ i: resolve,
68009
+ w,
68010
+ dryRun,
68011
+ now,
68012
+ glyph
68013
+ })
68014
+ }),
67645
68015
  state.ended !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
67646
68016
  marginTop: 1,
67647
68017
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: [...shipSummary(state), ...dryRun ? [] : [{
@@ -67739,7 +68109,7 @@ const report = fn("report")(function* () {
67739
68109
  ]
67740
68110
  })));
67741
68111
  });
67742
- const decodeFact$2 = decodeUnknownOption(Union([Struct({
68112
+ const decodeFact$3 = decodeUnknownOption(Union([Struct({
67743
68113
  fact: Literal("brew"),
67744
68114
  version: NullOr(String$6)
67745
68115
  }), AtFact]));
@@ -67791,7 +68161,7 @@ function homebrew(name) {
67791
68161
  let brew;
67792
68162
  const at = /* @__PURE__ */ new Map();
67793
68163
  for (const raw of facts) {
67794
- const fact = decodeFact$2(raw);
68164
+ const fact = decodeFact$3(raw);
67795
68165
  if (fact._tag === "None") continue;
67796
68166
  const f = fact.value;
67797
68167
  if (f.fact === "brew") brew = f.version ?? void 0;
@@ -67868,6 +68238,532 @@ function homebrew(name) {
67868
68238
  }
67869
68239
  const brew = homebrew("brew");
67870
68240
  const cask = homebrew("cask");
68241
+ //#endregion
68242
+ //#region ../../packages/providers/src/files/index.ts
68243
+ const ALWAYS_EXCLUDED = [".DS_Store", "._*"];
68244
+ const SIZE_LIMIT = 10485760;
68245
+ const decodeFact$2 = decodeUnknownOption(Union([Struct({
68246
+ fact: Literal("entry"),
68247
+ item: String$6,
68248
+ path: String$6,
68249
+ entry: String$6,
68250
+ size: Number$2
68251
+ }), Struct({
68252
+ fact: Literal("openssl"),
68253
+ present: Boolean$4
68254
+ })]));
68255
+ const FileRecord = Struct({
68256
+ at: String$6,
68257
+ entries: Record(String$6, String$6)
68258
+ });
68259
+ const decodeRecord$1 = decodeUnknownOption(FileRecord);
68260
+ const jsonRecord = encodeSync(fromJsonString(FileRecord));
68261
+ const fingerprint = (entries) => [...entries].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([rel, entry]) => `${rel}\t${entry}\n`).join("");
68262
+ const short$1 = (entries) => (string$1(fingerprint(entries)) >>> 0).toString(16).padStart(8, "0").slice(0, 7);
68263
+ const recordEntries = (record) => new Map(Object.entries(record.entries));
68264
+ const kindOf = (entries) => ({
68265
+ f: "a file",
68266
+ d: "a directory",
68267
+ l: "a symlink",
68268
+ "?": "something it cannot copy"
68269
+ })[entries.get(".")?.[0] ?? ""] ?? "missing";
68270
+ const depth = (rel) => rel === "." ? 0 : rel.split("/").length;
68271
+ const changedPaths = (a, b) => {
68272
+ const paths = [.../* @__PURE__ */ new Set([...a.keys(), ...b.keys()])].filter((rel) => a.get(rel) !== b.get(rel)).toSorted();
68273
+ const shown = paths.slice(0, 3).join(", ");
68274
+ return paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown;
68275
+ };
68276
+ const SHELL_FNS = [
68277
+ `ff_mode() { stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1"; }`,
68278
+ `ff_entry() {`,
68279
+ ` if [ -L "$3" ]; then ff_e="l $(readlink "$3")"; ff_s=0`,
68280
+ ` elif [ -d "$3" ]; then if [ -r "$3" ] && [ -x "$3" ]; then ff_e="d $(ff_mode "$3")"; else ff_e="?"; fi; ff_s=0`,
68281
+ ` 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`,
68282
+ ` else ff_e="?"; ff_s=0; fi`,
68283
+ ` ff_state "{\\"provider\\":\\"files\\",\\"fact\\":\\"entry\\",\\"item\\":\\"$(ff_json "$1")\\",\\"path\\":\\"$(ff_json "$2")\\",\\"entry\\":\\"$(ff_json "$ff_e")\\",\\"size\\":$ff_s}"`,
68284
+ `}`
68285
+ ].join("\n");
68286
+ const pruneExpr = (exclude) => [...ALWAYS_EXCLUDED, ...exclude].map((pattern) => pattern.includes("/") ? `-path ${quote(`./${pattern.replace(/^(\.?\/)+|\/+$/g, "")}`)}` : `-name ${quote(pattern)}`).join(" -o ");
68287
+ const observe$1 = (items) => {
68288
+ if (items.length === 0) return "";
68289
+ return [
68290
+ SHELL_FNS,
68291
+ `if command -v openssl >/dev/null 2>&1; then ff_o=true; else ff_o=false; fi`,
68292
+ `ff_state "{\\"provider\\":\\"files\\",\\"fact\\":\\"openssl\\",\\"present\\":$ff_o}"`,
68293
+ ...items.flatMap((item) => {
68294
+ const name = quote(item.name);
68295
+ return [
68296
+ `ff_p=${quotePath(item.path)}`,
68297
+ `if [ -d "$ff_p" ] && [ ! -L "$ff_p" ]; then`,
68298
+ ` ff_entry ${name} . "$ff_p"`,
68299
+ ` (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`,
68300
+ `elif [ -e "$ff_p" ] || [ -L "$ff_p" ]; then`,
68301
+ ` ff_entry ${name} . "$ff_p"`,
68302
+ "fi"
68303
+ ];
68304
+ })
68305
+ ].join("\n");
68306
+ };
68307
+ const parse = (facts, records) => {
68308
+ const items = /* @__PURE__ */ new Map();
68309
+ let openssl = false;
68310
+ for (const raw of facts) {
68311
+ const fact = decodeFact$2(raw);
68312
+ if (fact._tag === "None") continue;
68313
+ const f = fact.value;
68314
+ if (f.fact === "openssl") {
68315
+ openssl = f.present;
68316
+ continue;
68317
+ }
68318
+ const item = items.get(f.item) ?? {
68319
+ entries: /* @__PURE__ */ new Map(),
68320
+ size: 0
68321
+ };
68322
+ item.entries.set(f.path, f.entry);
68323
+ item.size += f.size;
68324
+ items.set(f.item, item);
68325
+ }
68326
+ const decoded = /* @__PURE__ */ new Map();
68327
+ for (const [name, raw] of Object.entries(records)) {
68328
+ const record = decodeRecord$1(raw);
68329
+ if (record._tag === "Some") decoded.set(name, record.value);
68330
+ }
68331
+ return {
68332
+ items,
68333
+ records: decoded,
68334
+ openssl
68335
+ };
68336
+ };
68337
+ const at = (rel) => rel === "." ? `"$ff_p"` : `"$ff_p"/${quote(rel)}`;
68338
+ const readContents = (files) => gen(function* () {
68339
+ const contents = /* @__PURE__ */ new Map();
68340
+ 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");
68341
+ return (yield* runScript({
68342
+ name: "local",
68343
+ local: true
68344
+ }, script, [], (line) => sync(() => {
68345
+ const match = /^ff-file (\d+) (.*)$/.exec(line);
68346
+ if (match !== null) contents.set(Number(match[1]), new Hidden(match[2] ?? ""));
68347
+ }))) === 0 && contents.size === files.length ? contents : void 0;
68348
+ });
68349
+ const resolveSources = fn("resolveSources")(function* (items, scope) {
68350
+ const facts = [];
68351
+ yield* runScript({
68352
+ name: scope.machine.name,
68353
+ local: true
68354
+ }, checkScript({
68355
+ scope,
68356
+ providers: [{
68357
+ provider: files,
68358
+ items
68359
+ }]
68360
+ }), [], (line) => sync(() => {
68361
+ const parsed = parseLine(line);
68362
+ if (parsed.kind === "state") facts.push(parsed.fact);
68363
+ }));
68364
+ const state = assembleState(facts);
68365
+ const observed = parse(state.facts.files ?? [], state.records.files ?? {});
68366
+ const sources = /* @__PURE__ */ new Map();
68367
+ for (const item of items) {
68368
+ const copy = observed.items.get(item.name);
68369
+ if (!state.complete) {
68370
+ sources.set(item.name, {
68371
+ item,
68372
+ kind: "unreadable",
68373
+ reason: "could not read it here"
68374
+ });
68375
+ continue;
68376
+ }
68377
+ if (copy === void 0) {
68378
+ sources.set(item.name, {
68379
+ item,
68380
+ kind: "missing"
68381
+ });
68382
+ continue;
68383
+ }
68384
+ const unreadable = [...copy.entries].find(([, entry]) => entry === "?")?.[0];
68385
+ if (unreadable !== void 0) {
68386
+ sources.set(item.name, {
68387
+ item,
68388
+ kind: "unreadable",
68389
+ reason: `cannot read or copy ${unreadable === "." ? item.path : unreadable} here`
68390
+ });
68391
+ continue;
68392
+ }
68393
+ if (copy.size > 10485760) {
68394
+ sources.set(item.name, {
68395
+ item,
68396
+ kind: "unreadable",
68397
+ reason: `${(copy.size / 1024 / 1024).toFixed(1)} MB, over the ${SIZE_LIMIT / 1024 / 1024} MB limit`
68398
+ });
68399
+ continue;
68400
+ }
68401
+ const list = [...copy.entries].flatMap(([rel, entry]) => entry.startsWith("f ") ? [{
68402
+ rel,
68403
+ abs: rel === "." ? quotePath(item.path) : `${quotePath(item.path)}/${quote(rel)}`
68404
+ }] : []);
68405
+ const read = yield* readContents(list);
68406
+ if (read === void 0) {
68407
+ sources.set(item.name, {
68408
+ item,
68409
+ kind: "unreadable",
68410
+ reason: "could not read it here"
68411
+ });
68412
+ continue;
68413
+ }
68414
+ const record = observed.records.get(item.name);
68415
+ sources.set(item.name, {
68416
+ item,
68417
+ kind: "present",
68418
+ entries: copy.entries,
68419
+ ...record === void 0 ? {} : { record },
68420
+ contents: new Map(list.map(({ rel }, n) => [rel, read.get(n) ?? new Hidden("")]))
68421
+ });
68422
+ }
68423
+ return {
68424
+ sources,
68425
+ openssl: observed.openssl
68426
+ };
68427
+ });
68428
+ const copiesOf = (checked, from) => {
68429
+ const copies = /* @__PURE__ */ new Map();
68430
+ for (const { machine, items, facts, records } of checked) {
68431
+ if (machine.name === from) continue;
68432
+ const observed = parse(facts, records);
68433
+ for (const item of items) {
68434
+ const copy = observed.items.get(item.name);
68435
+ if (copy === void 0 || copy.size > 10485760 || [...copy.entries.values()].includes("?")) continue;
68436
+ const record = observed.records.get(item.name);
68437
+ copies.set(item.name, [...copies.get(item.name) ?? [], {
68438
+ machine: machine.name,
68439
+ item,
68440
+ entries: copy.entries,
68441
+ ...record === void 0 ? {} : { record },
68442
+ openssl: observed.openssl
68443
+ }]);
68444
+ }
68445
+ }
68446
+ return copies;
68447
+ };
68448
+ const PULL = "pull:";
68449
+ const askWhy = (current, record, source, from) => {
68450
+ if (record === void 0) return "differs, and no Ship wrote it";
68451
+ const recorded = recordEntries(record);
68452
+ if (fingerprint(recorded) !== fingerprint(current)) return `was edited there${current.size === 1 && recorded.size === 1 ? "" : ` (${changedPaths(recorded, current)})`}`;
68453
+ 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`;
68454
+ };
68455
+ const fileQuestion = (subject, found, problem, pull, from) => ({
68456
+ kind: "file",
68457
+ subject,
68458
+ found,
68459
+ problem,
68460
+ choices: [
68461
+ {
68462
+ value: "overwrite",
68463
+ title: "overwrite it with this Machine's copy"
68464
+ },
68465
+ ...pull === void 0 || !pull.openssl ? [] : [{
68466
+ value: `${PULL}${pull.machine}`,
68467
+ title: `use ${pull.machine}'s copy (${short$1(pull.entries)}) on ${from} instead`
68468
+ }],
68469
+ {
68470
+ value: "skip",
68471
+ title: "skip on these Machines (leave as is, shown as skipped)"
68472
+ }
68473
+ ],
68474
+ default: "skip"
68475
+ });
68476
+ const missingQuestion = (subject, found, copies, from) => ({
68477
+ kind: "file",
68478
+ subject,
68479
+ found,
68480
+ problem: `is not on ${from}`,
68481
+ choices: [...Map.groupBy(copies.filter((c) => c.openssl), (c) => fingerprint(c.entries)).values().flatMap((same) => {
68482
+ const [first] = same;
68483
+ return first === void 0 ? [] : [{
68484
+ value: `${PULL}${first.machine}`,
68485
+ title: `pull ${short$1(first.entries)} from ${same.map((c) => c.machine).join(", ")}`
68486
+ }];
68487
+ }), {
68488
+ value: "skip",
68489
+ title: "skip it everywhere"
68490
+ }],
68491
+ default: "skip"
68492
+ });
68493
+ const decideFile = (item, observed, resolved, ctx) => {
68494
+ const { from } = resolved;
68495
+ const source = resolved.sources.get(item.name);
68496
+ if (source === void 0) return {
68497
+ kind: "skip",
68498
+ item: item.name,
68499
+ reason: `${from} has no Variant for it; ship from a Machine it targets`
68500
+ };
68501
+ const subject = address("files", item.name);
68502
+ const answer = ctx.answer("file", subject);
68503
+ const pulling = answer?.startsWith(PULL) === true;
68504
+ const copies = resolved.copies.get(item.name) ?? [];
68505
+ const mine = copies.find((c) => c.machine === ctx.machine.name);
68506
+ if (source.kind === "missing") {
68507
+ const notHere = {
68508
+ kind: "skip",
68509
+ item: item.name,
68510
+ reason: `not on ${from}`
68511
+ };
68512
+ if (mine === void 0) return notHere;
68513
+ if (pulling) return {
68514
+ ...notHere,
68515
+ at: short$1(mine.entries),
68516
+ reason: `pulling it from ${answer?.slice(5)}`
68517
+ };
68518
+ if (answer === "skip" || !resolved.openssl || !copies.some((c) => c.openssl)) {
68519
+ const without = resolved.openssl ? copies.filter((c) => !c.openssl).map((c) => c.machine) : [from];
68520
+ return {
68521
+ ...notHere,
68522
+ at: short$1(mine.entries),
68523
+ ...answer !== "skip" && without.length > 0 ? { reason: `not on ${from}; install openssl on ${without.join(", ")} to pull it` } : {}
68524
+ };
68525
+ }
68526
+ return {
68527
+ kind: "ask",
68528
+ item: item.name,
68529
+ at: short$1(mine.entries),
68530
+ question: missingQuestion(subject, short$1(mine.entries), copies, from)
68531
+ };
68532
+ }
68533
+ if (source.kind === "unreadable") return {
68534
+ kind: "fail",
68535
+ item: item.name,
68536
+ reason: `${from}: ${source.reason}`
68537
+ };
68538
+ const current = observed.items.get(item.name)?.entries;
68539
+ const record = observed.records.get(item.name);
68540
+ const ours = fingerprint(source.entries);
68541
+ const change = {
68542
+ item,
68543
+ entries: source.entries,
68544
+ ...current === void 0 ? {} : { current },
68545
+ contents: source.contents,
68546
+ at: resolved.at
68547
+ };
68548
+ const recorded = record === void 0 ? void 0 : fingerprint(recordEntries(record));
68549
+ if (ctx.machine.name === from || current !== void 0 && fingerprint(current) === ours) return {
68550
+ kind: "leave",
68551
+ item: item.name,
68552
+ at: short$1(source.entries),
68553
+ ...recorded === ours ? {} : { upkeep: change }
68554
+ };
68555
+ const to = short$1(source.entries);
68556
+ if (current === void 0) return {
68557
+ kind: "change",
68558
+ item: item.name,
68559
+ to,
68560
+ change
68561
+ };
68562
+ if (kindOf(current) !== kindOf(source.entries)) return {
68563
+ kind: "fail",
68564
+ item: item.name,
68565
+ at: short$1(current),
68566
+ reason: `${kindOf(current)} here, ${kindOf(source.entries)} on ${from}`
68567
+ };
68568
+ const fromAt = short$1(current);
68569
+ const why = askWhy(current, record, source, from);
68570
+ if (why === void 0) return {
68571
+ kind: "change",
68572
+ item: item.name,
68573
+ from: fromAt,
68574
+ to,
68575
+ change
68576
+ };
68577
+ if (pulling) return {
68578
+ kind: "skip",
68579
+ item: item.name,
68580
+ at: fromAt,
68581
+ reason: `pulling it from ${answer?.slice(5)}`
68582
+ };
68583
+ if (answer === "skip") return {
68584
+ kind: "skip",
68585
+ item: item.name,
68586
+ at: fromAt,
68587
+ reason: why
68588
+ };
68589
+ if (answer === "overwrite") return {
68590
+ kind: "change",
68591
+ item: item.name,
68592
+ from: fromAt,
68593
+ to,
68594
+ reason: why,
68595
+ change
68596
+ };
68597
+ return {
68598
+ kind: "ask",
68599
+ item: item.name,
68600
+ at: fromAt,
68601
+ question: fileQuestion(subject, fromAt, why, resolved.openssl ? mine : void 0, from)
68602
+ };
68603
+ };
68604
+ const CIPHER = "-aes-256-cbc -pbkdf2 -a -A";
68605
+ const writeRecord = (name, entries, at) => {
68606
+ const file = recordFile("files", name);
68607
+ const temporary = `${recordDir("files")}/${quote(`${name}.json.tmp`)}`;
68608
+ return [
68609
+ `mkdir -p ${recordDir("files")}`,
68610
+ `ff_lit ${quote(jsonRecord({
68611
+ at,
68612
+ entries: Object.fromEntries(entries)
68613
+ }))} > ${temporary}`,
68614
+ `mv -f ${temporary} ${file}`
68615
+ ];
68616
+ };
68617
+ const copyStep = (change) => {
68618
+ const { item, entries, current, contents } = change;
68619
+ const typeOf = (entry) => entry?.[0];
68620
+ const deletes = [...current ?? []].filter(([rel, entry]) => {
68621
+ const ours = entries.get(rel);
68622
+ return ours === void 0 || typeOf(ours) !== typeOf(entry);
68623
+ }).toSorted(([a], [b]) => depth(b) - depth(a));
68624
+ const writes = [...entries].filter(([rel, entry]) => current?.get(rel) !== entry).toSorted(([a], [b]) => depth(a) - depth(b));
68625
+ const input = change.key === void 0 ? [] : [change.key];
68626
+ const decode = change.key === void 0 ? "base64 -d" : `FF_KEY="$ff_input_0" openssl enc -d ${CIPHER} -pass env:FF_KEY`;
68627
+ const write = ([rel, entry]) => {
68628
+ const [type, mode = ""] = entry.split(" ");
68629
+ if (type === "d") return [`(umask 077 && mkdir -p ${at(rel)})`];
68630
+ if (type === "l") return [`rm -f ${at(rel)}`, `ln -s -- ${quote(entry.slice(2))} ${at(rel)}`];
68631
+ const n = input.push(contents.get(rel) ?? new Hidden("")) - 1;
68632
+ return [
68633
+ `ff_t=$(mktemp "$(dirname ${at(rel)})/.fftmp.XXXXXX")`,
68634
+ `printf '%s' "$ff_input_${n}" | ${decode} > "$ff_t"`,
68635
+ `chmod ${mode} "$ff_t"`,
68636
+ `mv -f "$ff_t" ${at(rel)}`,
68637
+ `ff_t=""`
68638
+ ];
68639
+ };
68640
+ const modes = writes.filter(([, entry]) => entry.startsWith("d ")).toReversed().map(([rel, entry]) => `chmod ${entry.slice(2)} ${at(rel)}`);
68641
+ const links = [...entries].filter(([, entry]) => entry.startsWith("l ")).map(([rel]) => rel);
68642
+ const reached = short$1(entries);
68643
+ return {
68644
+ id: `files:${item.name}:copy`,
68645
+ items: [item.name],
68646
+ doing: "copying",
68647
+ run: [
68648
+ `ff_p=${quotePath(item.path)}`,
68649
+ "(",
68650
+ " set -e",
68651
+ ` ff_t=""`,
68652
+ ` trap '[ -z "$ff_t" ] || rm -f "$ff_t"' EXIT`,
68653
+ ` trap 'exit 1' HUP INT TERM`,
68654
+ ...[
68655
+ `mkdir -p "$(dirname "$ff_p")"`,
68656
+ ...deletes.map(([rel, entry]) => entry.startsWith("d ") ? `rmdir ${at(rel)}` : `rm -f ${at(rel)}`),
68657
+ ...writes.flatMap(write),
68658
+ ...modes,
68659
+ ...writeRecord(item.name, entries, change.at)
68660
+ ].map((l) => ` ${l}`),
68661
+ ")",
68662
+ `ff_rc=$?`,
68663
+ `if [ "$ff_rc" -eq 0 ]; then`,
68664
+ ` ff_dl=""`,
68665
+ ...links.map((rel) => ` [ -e ${at(rel)} ] || ff_dl="$ff_dl, "${quote(rel)}`),
68666
+ ` 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`,
68667
+ "fi",
68668
+ `(exit "$ff_rc")`
68669
+ ],
68670
+ input
68671
+ };
68672
+ };
68673
+ const pull = (name, machine, resolved) => gen(function* () {
68674
+ const source = resolved.sources.get(name);
68675
+ const copy = resolved.copies.get(name)?.find((c) => c.machine === machine);
68676
+ if (source === void 0 || copy === void 0 || source.kind === "unreadable") return { error: `cannot pull ${machine}'s copy here` };
68677
+ const key = [...globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("");
68678
+ const rels = [...copy.entries].flatMap(([rel, entry]) => entry.startsWith("f ") ? [rel] : []);
68679
+ const contents = /* @__PURE__ */ new Map();
68680
+ let finished = false;
68681
+ const code = yield* runScript({
68682
+ name: machine,
68683
+ local: false
68684
+ }, [
68685
+ prelude,
68686
+ `IFS= read -r FF_KEY || FF_KEY=""`,
68687
+ "export FF_KEY",
68688
+ `ff_p=${quotePath(copy.item.path)}`,
68689
+ ...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"`),
68690
+ `printf 'ff-pulled\\n'`
68691
+ ].join("\n"), [key], (line) => sync(() => {
68692
+ const match = /^ff-file (\d+) (.*)$/.exec(line);
68693
+ if (match !== null) contents.set(Number(match[1]), new Hidden(match[2] ?? ""));
68694
+ else if (line === "ff-pulled") finished = true;
68695
+ }));
68696
+ if (!finished || contents.size !== rels.length) return { error: code === 255 ? `could not reach ${machine}` : `could not read its copy on ${machine}` };
68697
+ const step = copyStep({
68698
+ item: source.item,
68699
+ entries: copy.entries,
68700
+ ...source.kind === "present" ? { current: source.entries } : {},
68701
+ contents: new Map(rels.map((rel, n) => [rel, contents.get(n) ?? new Hidden("")])),
68702
+ at: copy.record?.at ?? resolved.at,
68703
+ key: new Hidden(key)
68704
+ });
68705
+ let written = false;
68706
+ yield* runScript({
68707
+ name: resolved.from,
68708
+ local: true
68709
+ }, stepScript(step), stepStdin(step), (line) => sync(() => {
68710
+ const parsed = parseLine(line);
68711
+ if (parsed.kind === "done" && parsed.done.id === step.id) written = parsed.done.ok;
68712
+ }));
68713
+ return written ? { done: `pulled ${short$1(copy.entries)} from ${machine}` } : { error: `could not write ${machine}'s copy on ${resolved.from}` };
68714
+ }).pipe(catch_((e) => succeed$3({ error: e.message })));
68715
+ const recordStep = (change) => ({
68716
+ id: `files:${change.item.name}:record`,
68717
+ items: [change.item.name],
68718
+ doing: "recording",
68719
+ run: [
68720
+ "(",
68721
+ " set -e",
68722
+ ...writeRecord(change.item.name, change.entries, change.at).map((l) => ` ${l}`),
68723
+ ")"
68724
+ ]
68725
+ });
68726
+ const files = {
68727
+ name: "files",
68728
+ rule: "record",
68729
+ dependsOn: [],
68730
+ items: (_section, listed) => listed.map(({ name, def }) => ({
68731
+ name,
68732
+ path: def.path,
68733
+ exclude: def.exclude ?? []
68734
+ })),
68735
+ observe: (items) => observe$1(items),
68736
+ parse,
68737
+ resolve: (items, { starting: all, checked }) => gen(function* () {
68738
+ const at = formatIso(yield* now);
68739
+ const needed = new Set(items.map((item) => item.name));
68740
+ const starting = {
68741
+ ...all,
68742
+ items: all.items.filter((item) => needed.has(item.name))
68743
+ };
68744
+ const { sources, openssl } = starting.items.length === 0 ? {
68745
+ sources: /* @__PURE__ */ new Map(),
68746
+ openssl: false
68747
+ } : yield* resolveSources(starting.items, starting).pipe(timeout("2 minutes"), orElseSucceed(() => ({
68748
+ sources: new Map(starting.items.map((item) => [item.name, {
68749
+ item,
68750
+ kind: "unreadable",
68751
+ reason: "could not read it here"
68752
+ }])),
68753
+ openssl: false
68754
+ })));
68755
+ return {
68756
+ from: starting.machine.name,
68757
+ at,
68758
+ sources,
68759
+ copies: copiesOf(checked, starting.machine.name),
68760
+ openssl
68761
+ };
68762
+ }),
68763
+ decide: ({ items, observed, resolved, ctx }) => items.map((item) => decideFile(item, observed, resolved, ctx)),
68764
+ act: (decisions) => decisions.flatMap((d) => d.kind === "change" ? [copyStep(d.change)] : d.kind === "leave" && d.upkeep !== void 0 ? [recordStep(d.upkeep)] : []),
68765
+ adopt: (item, answer, resolved) => answer.startsWith(PULL) ? pull(item, answer.slice(5), resolved) : void 0
68766
+ };
67871
68767
  const decodeFact$1 = decodeUnknownOption(Union([
67872
68768
  Struct({
67873
68769
  fact: Literal("cloneRoot"),
@@ -68001,11 +68897,12 @@ const decideRepo = (repo, root, observed, resolved, ctx) => {
68001
68897
  };
68002
68898
  const fetchStep = ({ item, root, commit }) => {
68003
68899
  const url = originUrl(item.origin);
68900
+ const fetch = (what) => `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(commit)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(what)}; fi`;
68004
68901
  const sync = isPin(item.ref) ? [
68005
- `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(commit)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(commit)}; fi`,
68902
+ fetch(commit),
68006
68903
  `git -C "$ff_d" checkout --quiet --detach ${quote(commit)}`,
68007
68904
  `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`
68008
- ] : [`ff_git -C "$ff_d" fetch --quiet origin ${quote(item.ref)}`, `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`];
68905
+ ] : [fetch(item.ref), `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`];
68009
68906
  return {
68010
68907
  id: `repos:${item.name}:fetch`,
68011
68908
  items: [item.name],
@@ -68297,6 +69194,7 @@ const decideRanged = (item, observed, resolved, ctx) => {
68297
69194
  //#endregion
68298
69195
  //#region ../../packages/providers/src/index.ts
68299
69196
  const PROVIDERS = [
69197
+ files,
68300
69198
  {
68301
69199
  name: "vp",
68302
69200
  rule: "range",
@@ -68556,12 +69454,15 @@ const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "d
68556
69454
  const decodeAt = decodeUnknownOption(AtFact);
68557
69455
  const runOn = fn("runOn")(function* (target, script, secrets, onLine, ledger) {
68558
69456
  let first;
69457
+ let connected = false;
68559
69458
  yield* logDebug(`script:\n${script}`);
68560
69459
  const started = yield* currentTimeMillis;
68561
69460
  const run = yield* runScript(target, script, secrets, (line) => {
68562
69461
  const parsed = parseLine(line);
68563
69462
  if (parsed.kind === "text" && first === void 0 && line.trim() !== "") first = line.trim();
68564
- return logDebug(`> ${line}`).pipe(andThen(onLine(parsed)));
69463
+ const connect = connected ? void_ : ledger.connected(target.name);
69464
+ connected = true;
69465
+ return logDebug(`> ${line}`).pipe(andThen(connect), andThen(onLine(parsed)));
68565
69466
  }).pipe(map$5((exitCode) => ({
68566
69467
  kind: "ran",
68567
69468
  exitCode
@@ -68701,7 +69602,7 @@ const askQuestions = fn("askQuestions")(function* (input) {
68701
69602
  yield* input.ledger.note("answer", "not asked (dry run)");
68702
69603
  continue;
68703
69604
  }
68704
- const choices = question.choices.map((c) => ({
69605
+ const choices = [...new Map(group.flatMap((a) => a.question.choices).map((c) => [c.value, c])).values()].map((c) => ({
68705
69606
  value: c.value,
68706
69607
  title: c.value === "bump" && bumps.length > 0 ? `bump the config to ${bumps.map((b) => bumpedRange(b.range, b.newest)).join(" and ")}` : c.title
68707
69608
  }));
@@ -68738,27 +69639,43 @@ const unshippable = (config, tailnet, only, source) => {
68738
69639
  const outsideFleet = only.filter((name) => !fleet.has(name));
68739
69640
  if (outsideFleet.length > 0) return `not in machines in ${source}: ${outsideFleet.join(", ")}`;
68740
69641
  };
68741
- const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger) {
69642
+ const minute = (iso) => iso?.slice(0, 16).replace("T", " ") ?? "an unknown time";
69643
+ const staleConfigs = fn("staleConfigs")(function* (text, base, own, checked, ledger) {
68742
69644
  const copies = [];
68743
69645
  for (const { machine, state } of checked) {
68744
- if (state.config === void 0) continue;
68745
- const parsed = yield* result(parseConfig(machine.name, state.config));
68746
- if (parsed._tag === "Failure") yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
68747
- else copies.push({
69646
+ if (machine.name === own || state.config === void 0 || state.config.text === text) continue;
69647
+ const parsed = yield* result(parseConfig(machine.name, state.config.text));
69648
+ if (parsed._tag === "Failure") {
69649
+ yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
69650
+ continue;
69651
+ }
69652
+ const record = getOrUndefined$1(decodeConfigRecord(state.configRecord));
69653
+ const why = staleWhy({
69654
+ machine: machine.name,
69655
+ sha: state.config.sha,
69656
+ record
69657
+ }, base);
69658
+ if (why === void 0) continue;
69659
+ copies.push({
68748
69660
  machine: machine.name,
69661
+ text: state.config.text,
69662
+ sha: state.config.sha,
68749
69663
  config: parsed.success,
68750
- text: state.config
69664
+ record,
69665
+ 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`
68751
69666
  });
68752
69667
  }
68753
- return [...Map.groupBy(staleCopies(config, copies, own), (c) => c.text)].flatMap(([text, [first, ...rest]]) => {
69668
+ return [...Map.groupBy(copies, (c) => c.text)].flatMap(([text, group]) => {
69669
+ const [first] = group;
68754
69670
  if (first === void 0) return [];
68755
- const { config, machine } = first;
68756
- const machines = [machine, ...rest.map((c) => c.machine)];
69671
+ const machines = group.map((c) => c.machine);
68757
69672
  return [{
68758
69673
  text,
68759
- config,
69674
+ sha: first.sha,
69675
+ config: first.config,
68760
69676
  machines,
68761
- description: `${machines.join(", ")} (shipped ${config.shippedAt} from ${config.shippedFrom ?? "unknown"})`
69677
+ reached: mergeReached(group.flatMap((c) => c.record === void 0 ? [] : [c.record.reached])),
69678
+ description: `the config on ${machines.join(", ")} ${first.why}`
68762
69679
  }];
68763
69680
  });
68764
69681
  });
@@ -68773,10 +69690,10 @@ const ship = fn("ship")(function* (flags) {
68773
69690
  yield* logInfo(`Ship ${id}: fleetfizz ${flags.version}, node ${process.version} on ${process.platform}`);
68774
69691
  yield* logInfo(`fizz ${process.argv.slice(2).join(" ")}`);
68775
69692
  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)" : ""}`);
68776
- while ((yield* shipPass(flags, tailnet, hidden)) === "adopted");
69693
+ while ((yield* shipPass(flags, tailnet, hidden, id)) === "adopted");
68777
69694
  }).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"));
68778
69695
  }, scoped);
68779
- const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
69696
+ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
68780
69697
  const fs = yield* FileSystem;
68781
69698
  const parsed = parseAnswers(flags.answers);
68782
69699
  if ("invalid" in parsed) return yield* fail$1(new ShipError(parsed.invalid));
@@ -68794,6 +69711,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68794
69711
  if (problem !== void 0) return yield* fail$1(new ShipError(problem));
68795
69712
  const fleet = new Set(config.machines);
68796
69713
  const own = flags.config === void 0 ? tailnet.self.name : void 0;
69714
+ const base = (own === void 0 ? void 0 : yield* readConfigRecord)?.reached ?? {};
68797
69715
  const order = config.machines;
68798
69716
  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));
68799
69717
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
@@ -68864,13 +69782,13 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68864
69782
  machine: machine.name,
68865
69783
  phase: "check"
68866
69784
  })), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
68867
- const stale = yield* staleConfigs(config, own, checked, ledger);
69785
+ const stale = yield* staleConfigs(yield* fs.readFileString(path), base, own, checked, ledger);
68868
69786
  if (stale.length > 0) {
68869
- const what = `newer config on ${stale.map((s) => s.description).join(", ")}`;
69787
+ const what = stale.map((s) => s.description).join("; ");
68870
69788
  const answers = stale.flatMap((s) => s.machines).map((m) => given("config", m));
68871
69789
  if (flags.dryRun) yield* ledger.note("info", `${what}; a Ship would ask before overwriting it`);
68872
69790
  else if (answers.every((a) => a === "overwrite")) yield* ledger.note("info", `${what}; overwriting it, as answered`);
68873
- else if (answers.includes("stop")) return yield* fail$1(new ShipError("aborted: the config here is older"));
69791
+ else if (answers.includes("stop")) return yield* fail$1(new ShipError("stopped before overwriting the config there"));
68874
69792
  else if (live.ask === void 0) return yield* fail$1(new ShipError(`${what}; copy it here first, or pass --overwrite-config`));
68875
69793
  else {
68876
69794
  const usable = [];
@@ -68896,12 +69814,17 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68896
69814
  ]);
68897
69815
  if (choice === "stop") {
68898
69816
  yield* ledger.note("answer", "stop");
68899
- return yield* fail$1(new ShipError("aborted: the config here is older"));
69817
+ return yield* fail$1(new ShipError("stopped before overwriting the config there"));
68900
69818
  }
68901
69819
  if (choice === "overwrite") yield* ledger.note("answer", "overwrite it");
68902
69820
  else {
68903
69821
  yield* ledger.note("answer", `use the config on ${choice.machines[0]}`);
68904
69822
  yield* fs.writeFileString(path, choice.text);
69823
+ if (own !== void 0) yield* writeConfigRecord(encodeConfigRecord({
69824
+ ship: id,
69825
+ sha: choice.sha,
69826
+ reached: reachedAfter(mergeReached([base, choice.reached]), formatIso(yield* now), [own], choice.config.machines)
69827
+ }));
68905
69828
  adopted = true;
68906
69829
  return "adopted";
68907
69830
  }
@@ -68911,11 +69834,36 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68911
69834
  provider,
68912
69835
  items: checked.flatMap(({ machine }) => targetedItems(provider, config, machine))
68913
69836
  }));
69837
+ yield* ledger.resolving(new Set(targeted.flatMap(({ provider, items }) => items.map((i) => `${provider.name}:${i.name}`))).size);
68914
69838
  const lent = targeted.some(({ provider, items }) => provider.name === "repos" && items.length > 0) ? yield* readLentToken() : { token: void 0 };
68915
69839
  if (lent.token !== void 0) hidden.add(lent.token);
68916
69840
  yield* logInfo(lent.token !== void 0 ? "gh token: lent" : `gh token: none (${lent.reason ?? "no repos"})`);
68917
69841
  if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
68918
- 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" }));
69842
+ const self = {
69843
+ name: tailnet.self.name,
69844
+ platform: tailnet.self.platform
69845
+ };
69846
+ const resolved = Object.fromEntries(yield* forEach$1(targeted, ({ provider, items }) => provider.resolve(items, {
69847
+ ...lent.token === void 0 ? {} : { token: lent.token },
69848
+ starting: {
69849
+ machine: self,
69850
+ config,
69851
+ items: targetedItems(provider, config, self)
69852
+ },
69853
+ checked: checked.map(({ machine, state }) => {
69854
+ const identity = {
69855
+ name: machine.name,
69856
+ platform: machine.platform
69857
+ };
69858
+ return {
69859
+ machine: identity,
69860
+ items: targetedItems(provider, config, identity),
69861
+ facts: state.facts[provider.name] ?? [],
69862
+ records: state.records[provider.name] ?? {}
69863
+ };
69864
+ })
69865
+ }).pipe(map$5((r) => [provider.name, r])), { concurrency: "unbounded" }));
69866
+ yield* ledger.resolved;
68919
69867
  const planAll = (cfg, answer) => checked.map((g) => ({
68920
69868
  ...g,
68921
69869
  plan: planMachine({
@@ -68966,33 +69914,55 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden) {
68966
69914
  if (!progressed) break;
68967
69915
  planned = planAll(cfg, answer);
68968
69916
  }
68969
- for (const { machine, plan: p } of planned) yield* ledger.plan(machine.name, p);
69917
+ const adopting = [...decided].flatMap(([key, choice]) => {
69918
+ const subject = key.slice(key.indexOf(":") + 1);
69919
+ const at = parseAddress(subject);
69920
+ const provider = providers.find((p) => p.name === at?.provider);
69921
+ const run = at === void 0 || provider === void 0 ? void 0 : provider.adopt?.(at.item, choice, resolved[provider.name]);
69922
+ return run === void 0 ? [] : [{
69923
+ subject,
69924
+ run
69925
+ }];
69926
+ });
69927
+ if (adopting.length > 0 && !flags.dryRun) {
69928
+ for (const { subject, run } of adopting) {
69929
+ changing = true;
69930
+ const result = yield* run;
69931
+ if ("error" in result) return yield* fail$1(new ShipError(`${subject}: ${result.error}`));
69932
+ yield* ledger.note("info", `${subject}: ${result.done}; starting over`);
69933
+ }
69934
+ adopted = true;
69935
+ return "adopted";
69936
+ }
69937
+ const text = yield* fs.readFileString(path);
69938
+ const sha = yield* shaOf(text);
69939
+ for (const { machine, state, plan: p } of planned) yield* ledger.plan(machine.name, p, {
69940
+ ...state.config === void 0 ? {} : { from: state.config.sha.slice(0, 7) },
69941
+ to: sha.slice(0, 7)
69942
+ });
68970
69943
  if (flags.dryRun) return;
68971
- const shippedAt = formatIso(yield* now);
68972
- changing = true;
68973
- const original = yield* fs.readFileString(path);
68974
- const stampWith = (reached) => stampText(original, {
68975
- shippedAt,
68976
- shippedFrom: tailnet.self.name,
68977
- reached,
68978
- own
69944
+ const at = formatIso(yield* now);
69945
+ const recordFor = (reached) => encodeConfigRecord({
69946
+ ship: id,
69947
+ sha,
69948
+ reached: reachedAfter(base, at, [...reached, ...own === void 0 ? [] : [own]], cfg.machines)
68979
69949
  });
68980
- const stamped = stampWith(planned.map((g) => g.machine.name));
68981
- yield* fs.writeFileString(path, stamped);
68982
- yield* logInfo(`stamped the config: shippedAt ${shippedAt}`);
69950
+ changing = true;
69951
+ const record = recordFor(planned.map((g) => g.machine.name));
69952
+ if (own !== void 0) yield* writeConfigRecord(record);
69953
+ yield* logInfo(`the config's record: ${record}`);
68983
69954
  const schema = `${schemaJson(fleetConfigJsonSchema())}\n`;
68984
- const stdin = applyStdin(lent.token, stamped, schema);
68985
69955
  const delivered = yield* forEach$1(planned, fn("applyOne")(function* ({ machine, local, plan: p }) {
68986
69956
  return (yield* apply({
68987
69957
  name: machine.name,
68988
69958
  local
68989
- }, p, applyScript(p), stdin, ledger)).done.includes("config") ? machine.name : void 0;
69959
+ }, p, applyScript(p, record), applyStdin(lent.token, text, schema, p), ledger)).done.includes("config") ? machine.name : void 0;
68990
69960
  }, (effect, { machine }) => annotateLogs(effect, {
68991
69961
  machine: machine.name,
68992
69962
  phase: "apply"
68993
69963
  })), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((name) => name !== void 0)));
68994
69964
  yield* logInfo(`the config reached: ${delivered.join(", ")}`);
68995
- yield* fs.writeFileString(path, stampWith(delivered));
69965
+ if (own !== void 0) yield* writeConfigRecord(recordFor(delivered));
68996
69966
  }, scoped);
68997
69967
  //#endregion
68998
69968
  //#region src/command.ts