fleetfizz 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/fleetfizz.mjs +3029 -1134
  2. package/package.json +1 -1
@@ -1849,7 +1849,7 @@ const emptyContext$1 = {
1849
1849
  * @category formatting
1850
1850
  * @since 2.0.0
1851
1851
  */
1852
- function format$3(input, options) {
1852
+ function format$4(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$3(redacted);
2015
+ if (typeof redacted === "bigint") return format$4(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$3(u, { space: whitespace });
2151
+ return typeof u === "object" ? formatJson(u, { space: whitespace }) : format$4(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$3(this.toJSON());
2201
+ return format$4(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$3(this.toJSON(), {
2364
+ return format$4(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$3(this.reasons)})`;
2414
+ return `Cause(${format$4(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$3(this);
2457
+ return format$4(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$3(this.error)})`;
2472
+ return `Fail(${format$4(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$3(this.defect)})`;
2520
+ return `Die(${format$4(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$3(this[args])})`;
2581
+ return `${options.op}(${format$4(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$3(this.value)})`;
2771
+ return `some(${format$4(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$3(this.success)})`;
2843
+ return `success(${format$4(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$3(this.failure)})`;
2863
+ return `failure(${format$4(this.failure)})`;
2864
2864
  },
2865
2865
  toJSON() {
2866
2866
  return {
@@ -5404,11 +5404,11 @@ const parts = (self) => {
5404
5404
  * @category converting
5405
5405
  * @since 2.0.0
5406
5406
  */
5407
- const format$2 = (self) => {
5407
+ const format$3 = (self) => {
5408
5408
  if (self.value._tag === "Infinity") return "Infinity";
5409
5409
  if (self.value._tag === "NegativeInfinity") return "-Infinity";
5410
5410
  if (isZero$1(self)) return "0";
5411
- if (isNegative(self)) return "-" + format$2(abs(self));
5411
+ if (isNegative(self)) return "-" + format$3(abs(self));
5412
5412
  const fragments = parts(self);
5413
5413
  const pieces = [];
5414
5414
  if (fragments.days !== 0) pieces.push(`${fragments.days}d`);
@@ -7231,7 +7231,7 @@ const exitPrimitive = /*#__PURE__*/ makePrimitive$1({
7231
7231
  /** @internal */
7232
7232
  const timeoutOrElse$1 = /*#__PURE__*/ dual(2, (self, options) => flatMap$2(timeoutOption(self, options.duration), (option) => isNone(option) ? options.orElse() : succeed$6(option.value)));
7233
7233
  /** @internal */
7234
- const timeoutErrorFromDuration = (duration) => new TimeoutError(`Operation timed out after '${format$2(duration)}'`);
7234
+ const timeoutErrorFromDuration = (duration) => new TimeoutError(`Operation timed out after '${format$3(duration)}'`);
7235
7235
  /** @internal */
7236
7236
  const timeout$1 = /*#__PURE__*/ dual(2, (self, duration) => {
7237
7237
  const decoded = fromInputUnsafe$1(duration);
@@ -7714,6 +7714,23 @@ const forEachConcurrent = /*#__PURE__*/ iterateConcurrentImpl({
7714
7714
  }
7715
7715
  });
7716
7716
  /** @internal */
7717
+ const filter$1 = /*#__PURE__*/ dual((args) => isIterable(args[0]) && !isEffect$1(args[0]), (elements, predicate, options) => suspend$3(() => {
7718
+ const out = [];
7719
+ return as$1(forEach$2(elements, (a, i) => {
7720
+ const result = predicate(a, i);
7721
+ if (typeof result === "boolean") {
7722
+ if (result) out.push(a);
7723
+ return void_$2;
7724
+ }
7725
+ return map$7(result, (keep) => {
7726
+ if (keep) out.push(a);
7727
+ });
7728
+ }, {
7729
+ discard: true,
7730
+ concurrency: options?.concurrency
7731
+ }), out);
7732
+ }));
7733
+ /** @internal */
7717
7734
  const forkUnsafe$1 = (parent, effect, immediate = false, daemon = false, uninterruptible = false) => {
7718
7735
  const parentRuntime = parent;
7719
7736
  const interruptible = uninterruptible === "inherit" ? parentRuntime.interruptible : !uninterruptible;
@@ -9910,6 +9927,8 @@ const hasZone = (input) => /Z|GMT|[+-]\d{2}$|[+-]\d{2}:?\d{2}$|\]$/.test(input);
9910
9927
  const now$1 = /*#__PURE__*/ map$7(currentTimeMillis, makeUtc);
9911
9928
  /** @internal */
9912
9929
  const toDateUtc$1 = (self) => new Date(self.epochMilliseconds);
9930
+ /** @internal */
9931
+ const toEpochMillis$1 = (self) => self.epochMilliseconds;
9913
9932
  const setPartsDate = (date, parts) => {
9914
9933
  if (parts.year !== void 0 || parts.month !== void 0 || parts.day !== void 0) date.setUTCFullYear(parts.year ?? date.getUTCFullYear(), parts.month !== void 0 ? parts.month - 1 : date.getUTCMonth(), parts.day ?? date.getUTCDate());
9915
9934
  if (parts.weekDay !== void 0) {
@@ -9927,6 +9946,29 @@ const formatLocal$1 = /*#__PURE__*/ dual(isDateTimeArgs, (self, options) => new
9927
9946
  const formatIso$1 = (self) => toDateUtc$1(self).toISOString();
9928
9947
  globalThis.Number;
9929
9948
  /**
9949
+ * Order instance for `number` values.
9950
+ *
9951
+ * **When to use**
9952
+ *
9953
+ * Use when you need to sort or compare numbers through APIs that accept an
9954
+ * ordering instance.
9955
+ *
9956
+ * **Example** (Comparing numbers)
9957
+ *
9958
+ * ```ts import.meta.vitest
9959
+ * import { Number } from "effect"
9960
+ *
9961
+ * Number.Order(1, 2) // => -1
9962
+ * Number.Order(2, 1) // => 1
9963
+ * Number.Order(1, 1) // => 0
9964
+ * ```
9965
+ *
9966
+ * @stability stable
9967
+ * @category instances
9968
+ * @since 2.0.0
9969
+ */
9970
+ const Order$1 = Number$6;
9971
+ /**
9930
9972
  * Returns the number rounded with the given precision.
9931
9973
  *
9932
9974
  * **When to use**
@@ -12173,6 +12215,32 @@ const sleep = sleep$1;
12173
12215
  */
12174
12216
  const raceFirst = raceFirst$1;
12175
12217
  /**
12218
+ * Filters elements of an iterable using a predicate, refinement, or effectful
12219
+ * predicate.
12220
+ *
12221
+ * **Example** (Filtering success values)
12222
+ *
12223
+ * ```ts import.meta.vitest
12224
+ * import { Effect } from "effect"
12225
+ * const output: Array<unknown> = []
12226
+ *
12227
+ * // Sync predicate
12228
+ * const evens = Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0)
12229
+ *
12230
+ * // Effectful predicate
12231
+ * const checked = Effect.filter([1, 2, 3], (n) => Effect.succeed(n > 1))
12232
+ *
12233
+ * void output.push(Effect.runSync(evens))
12234
+ * void output.push(Effect.runSync(checked))
12235
+ * output // => [[2, 4], [2, 3]]
12236
+ * ```
12237
+ *
12238
+ * @stability stable
12239
+ * @category collecting
12240
+ * @since 2.0.0
12241
+ */
12242
+ const filter = filter$1;
12243
+ /**
12176
12244
  * Handles both success and failure cases of an effect without performing side
12177
12245
  * effects.
12178
12246
  *
@@ -18376,7 +18444,7 @@ const defaultLeafHook = (issue) => {
18376
18444
  */
18377
18445
  const defaultCheckHook = (issue) => findMessage(issue.issue) ?? findMessage(issue);
18378
18446
  function formatInput(issue) {
18379
- return hasInput(issue) ? format$3(issue.input) : void 0;
18447
+ return hasInput(issue) ? format$4(issue.input) : void 0;
18380
18448
  }
18381
18449
  function findExpected(issue) {
18382
18450
  const expected = issue.annotations?.expected;
@@ -18540,6 +18608,27 @@ const makeUnsafe = makeUnsafe$2;
18540
18608
  */
18541
18609
  const now = now$1;
18542
18610
  /**
18611
+ * Gets the milliseconds since the Unix epoch of a `DateTime`.
18612
+ *
18613
+ * **Details**
18614
+ *
18615
+ * This returns the UTC timestamp regardless of any time zone information.
18616
+ *
18617
+ * **Example** (Reading epoch milliseconds)
18618
+ *
18619
+ * ```ts import.meta.vitest
18620
+ * import { DateTime } from "effect"
18621
+ *
18622
+ * const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z")
18623
+ * DateTime.toEpochMillis(dt) // => 1704067200000
18624
+ * ```
18625
+ *
18626
+ * @stability stable
18627
+ * @category converting
18628
+ * @since 3.6.0
18629
+ */
18630
+ const toEpochMillis = toEpochMillis$1;
18631
+ /**
18543
18632
  * Formats a `DateTime` with `Intl.DateTimeFormat` using the system local time
18544
18633
  * zone and locale.
18545
18634
  *
@@ -18820,7 +18909,7 @@ const BigDecimalProto = {
18820
18909
  return isBigDecimal(that) && compare(this, that) === 0;
18821
18910
  },
18822
18911
  toString() {
18823
- return `BigDecimal(${format$1(this)})`;
18912
+ return `BigDecimal(${format$2(this)})`;
18824
18913
  },
18825
18914
  toJSON() {
18826
18915
  return {
@@ -19020,7 +19109,7 @@ const sign = (n) => n.value === bigint0 ? 0 : n.value < bigint0 ? -1 : 1;
19020
19109
  * @category converting
19021
19110
  * @since 2.0.0
19022
19111
  */
19023
- const format$1 = (n) => {
19112
+ const format$2 = (n) => {
19024
19113
  const normalized = normalize(n);
19025
19114
  if (Math.abs(normalized.scale) >= 16) return toExponential(normalized);
19026
19115
  const negative = normalized.value < bigint0;
@@ -19288,9 +19377,9 @@ const decodeJsonError = (input) => {
19288
19377
  const encodeUnknownAsJson = (input) => {
19289
19378
  try {
19290
19379
  const json = formatJson(input);
19291
- return json === void 0 ? format$3(input) : JSON.parse(json);
19380
+ return json === void 0 ? format$4(input) : JSON.parse(json);
19292
19381
  } catch {
19293
- return format$3(input);
19382
+ return format$4(input);
19294
19383
  }
19295
19384
  };
19296
19385
  const encodeJsonError = (input, options, encodeDefect) => {
@@ -19656,7 +19745,7 @@ const Literal$1 = class extends ASTNodeImpl {
19656
19745
  literal;
19657
19746
  constructor(literal, annotations, checks, encoding, context) {
19658
19747
  super(annotations, checks, encoding, context);
19659
- if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) throw new Error(`A numeric literal must be finite, got ${format$3(literal)}`);
19748
+ if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) throw new Error(`A numeric literal must be finite, got ${format$4(literal)}`);
19660
19749
  this.literal = literal;
19661
19750
  }
19662
19751
  /** @internal */
@@ -23106,7 +23195,7 @@ const layer$5 = /* @__PURE__ */ succeed$4(Stdio, /*#__PURE__*/ make$9({
23106
23195
  /** @internal */
23107
23196
  const redactedRegistry = /*#__PURE__*/ new WeakMap();
23108
23197
  /** @internal */
23109
- const value$2 = (self) => {
23198
+ const value$3 = (self) => {
23110
23199
  if (redactedRegistry.has(self)) return redactedRegistry.get(self);
23111
23200
  else throw new Error("Unable to get redacted value" + (self.label ? ` with label: "${self.label}"` : ""));
23112
23201
  };
@@ -23227,7 +23316,7 @@ const Proto$4 = {
23227
23316
  * @category getters
23228
23317
  * @since 3.3.0
23229
23318
  */
23230
- const value$1 = value$2;
23319
+ const value$2 = value$3;
23231
23320
  ({ ...PipeInspectableProto });
23232
23321
  //#endregion
23233
23322
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/internal/schema/interpreter.js
@@ -23574,6 +23663,8 @@ function decodeUnknownEffect$1(schema, options) {
23574
23663
  function decodeUnknownOption$1(schema, options) {
23575
23664
  return asOption(decodeUnknownEffect$1(schema, options));
23576
23665
  }
23666
+ /** @internal */
23667
+ const decodeOption$1 = decodeUnknownOption$1;
23577
23668
  /**
23578
23669
  * Creates a decoder for `unknown` input that reports failure safely as a
23579
23670
  * `Result`.
@@ -25218,6 +25309,33 @@ const decodeEffect = decodeUnknownEffect;
25218
25309
  */
25219
25310
  const decodeUnknownOption = decodeUnknownOption$1;
25220
25311
  /**
25312
+ * Decodes a typed input (the schema's `Encoded` type) against a schema,
25313
+ * returning an `Option` that is `Some` with the decoded value on success or
25314
+ * `None` for schema mismatches.
25315
+ *
25316
+ * **When to use**
25317
+ *
25318
+ * Use when you already have input typed as the schema's `Encoded` type and
25319
+ * only need to know whether decoding succeeded.
25320
+ *
25321
+ * **Details**
25322
+ *
25323
+ * For `unknown` input use {@link decodeUnknownOption}.
25324
+ * Options may be provided either when creating the decoder or when applying it;
25325
+ * application options override creation options.
25326
+ *
25327
+ * **Gotchas**
25328
+ *
25329
+ * Only causes made entirely of schema issues are converted to `None`. Causes
25330
+ * that contain defects, interruptions, or other non-schema reasons throw
25331
+ * instead.
25332
+ *
25333
+ * @stability stable
25334
+ * @category decoding
25335
+ * @since 3.10.0
25336
+ */
25337
+ const decodeOption = decodeOption$1;
25338
+ /**
25221
25339
  * Decodes an `unknown` input against a schema, returning a `Result` that
25222
25340
  * succeeds with the decoded value or fails with a {@link SchemaError} for schema
25223
25341
  * mismatches.
@@ -25996,7 +26114,7 @@ function isPattern(regExp, annotations) {
25996
26114
  const source = regExp.source;
25997
26115
  const flags = regExp.flags;
25998
26116
  const canExport = /^[dg]*uy?$/.test(flags);
25999
- const runtimeRegExp = flags === "" ? `new RegExp(${format$3(source)})` : `new RegExp(${format$3(source)}, ${format$3(flags)})`;
26117
+ const runtimeRegExp = flags === "" ? `new RegExp(${format$4(source)})` : `new RegExp(${format$4(source)}, ${format$4(flags)})`;
26000
26118
  return isPattern$1(regExp, {
26001
26119
  toJsonSchema: () => canExport ? { pattern: flags.endsWith("y") ? `^(?:${source})` : source } : [{}, true],
26002
26120
  toCode: () => ({ runtime: `Schema.isPattern(${runtimeRegExp})` }),
@@ -27428,7 +27546,7 @@ const String$5 = /*#__PURE__*/ makePrimitive("String", (value) => succeed$3(valu
27428
27546
  */
27429
27547
  const Choice = (choices) => {
27430
27548
  const choiceMap = new Map(choices);
27431
- const validChoices = choices.map(([key]) => format$3(key)).join(" | ");
27549
+ const validChoices = choices.map(([key]) => format$4(key)).join(" | ");
27432
27550
  const primitive = makePrimitive("Choice", (value) => {
27433
27551
  if (choiceMap.has(value)) return succeed$3(choiceMap.get(value));
27434
27552
  return fail$1(validChoices);
@@ -27978,6 +28096,40 @@ const Select = (options) => {
27978
28096
  });
27979
28097
  };
27980
28098
  /**
28099
+ * Creates a prompt that lets the user select multiple choices and returns their
28100
+ * values as an array.
28101
+ *
28102
+ * **Details**
28103
+ *
28104
+ * The prompt supports default selected choices, bulk-selection commands, and
28105
+ * minimum or maximum selection counts.
28106
+ *
28107
+ * @stability unstable
28108
+ * @category constructors
28109
+ * @since 4.0.0
28110
+ */
28111
+ const MultiSelect = (options) => {
28112
+ const opts = {
28113
+ maxPerPage: 10,
28114
+ ...options
28115
+ };
28116
+ const initialSelected = /* @__PURE__ */ new Set();
28117
+ for (let i = 0; i < opts.choices.length; i++) {
28118
+ const choice = opts.choices[i];
28119
+ if (choice.selected === true && !choice.disabled) initialSelected.add(i);
28120
+ }
28121
+ const initialState = {
28122
+ index: 0,
28123
+ selectedIndices: initialSelected,
28124
+ error: none()
28125
+ };
28126
+ return Custom(initialState, {
28127
+ render: handleMultiSelectRender(opts),
28128
+ process: handleMultiSelectProcess(opts),
28129
+ clear: handleMultiSelectClear(opts)
28130
+ });
28131
+ };
28132
+ /**
27981
28133
  * Creates a `Prompt` which immediately succeeds with the specified value.
27982
28134
  *
27983
28135
  * **Details**
@@ -28090,12 +28242,12 @@ const eraseText = (text, columns) => {
28090
28242
  for (const line of lines) rows += 1 + Math.floor(Math.max(line.length - 1, 0) / columns);
28091
28243
  return eraseLines$1(rows);
28092
28244
  };
28093
- const lines = (prompt, columns) => {
28245
+ const lines$1 = (prompt, columns) => {
28094
28246
  const lines = prompt.split(NEWLINE_REGEXP);
28095
28247
  return columns === 0 ? lines.length : pipe$1(map$8(lines, (line) => Math.ceil(line.length / columns)), reduce(0, (left, right) => left + right));
28096
28248
  };
28097
28249
  const clearOutputWithError = (outputText, columns, errorText) => {
28098
- if (errorText !== void 0 && errorText.length > 0) return cursorDown$1(lines(errorText, columns)) + eraseText(`\n${errorText}`, columns) + eraseText(outputText, columns);
28250
+ if (errorText !== void 0 && errorText.length > 0) return cursorDown$1(lines$1(errorText, columns)) + eraseText(`\n${errorText}`, columns) + eraseText(outputText, columns);
28099
28251
  return eraseText(outputText, columns);
28100
28252
  };
28101
28253
  const renderBeep = "\x07";
@@ -28582,6 +28734,18 @@ const renderPrompt = (confirm, message, leadingSymbol, trailingSymbol, options)
28582
28734
  }
28583
28735
  });
28584
28736
  };
28737
+ const renderMultiSelectError = (state, pointer, theme, renderOptions) => {
28738
+ if (isSome(state.error)) return match$3(state.error.value.split(NEWLINE_REGEXP), {
28739
+ onEmpty: () => "",
28740
+ onNonEmpty: (errorLines) => {
28741
+ if (renderOptions?.plain === true) return separateSymbol(pointer, errorLines.join("\n"));
28742
+ const prefix = annotateSymbol(pointer, theme.errorColor);
28743
+ const lines = map$8(errorLines, (str) => annotateErrorLine(str, theme.errorColor));
28744
+ return cursorSavePosition + "\n" + separateSymbol(prefix, lines.join("\n")) + cursorRestorePosition;
28745
+ }
28746
+ });
28747
+ return "";
28748
+ };
28585
28749
  const renderChoiceDescription = (choice, isActive, theme, renderOptions) => {
28586
28750
  if (!choice.disabled && choice.description && isActive) {
28587
28751
  const text = theme.descriptionSeparator + choice.description;
@@ -28589,6 +28753,143 @@ const renderChoiceDescription = (choice, isActive, theme, renderOptions) => {
28589
28753
  }
28590
28754
  return "";
28591
28755
  };
28756
+ const metaOptionsCount = 2;
28757
+ const renderMultiSelectTitle = (title, isHighlighted, theme, renderOptions) => {
28758
+ if (renderOptions?.plain === true || !isHighlighted) return title;
28759
+ return annotate(title, combine(underlined, theme.primaryColor));
28760
+ };
28761
+ const renderMultiSelectChoices = (state, options, figures, renderOptions) => {
28762
+ const choices = options.choices;
28763
+ const checkboxWidth = Math.max(figures.checkboxOn.length, figures.checkboxOff.length);
28764
+ const selectableCount = choices.filter((choice) => !choice.disabled).length;
28765
+ const selectAllText = Array.from(state.selectedIndices).filter((index) => !choices[index].disabled).length === selectableCount ? options?.selectNone ?? "Select None" : options?.selectAll ?? "Select All";
28766
+ const inverseSelectionText = options?.inverseSelection ?? "Inverse Selection";
28767
+ const metaOptions = [{ title: selectAllText }, { title: inverseSelectionText }];
28768
+ const allChoices = [...metaOptions, ...choices];
28769
+ const toDisplay = entriesToDisplay(state.index, allChoices.length, options.maxPerPage);
28770
+ const documents = [];
28771
+ for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
28772
+ const choice = allChoices[index];
28773
+ const isHighlighted = state.index === index;
28774
+ const prefix = renderPagingPrefix(figures, index === toDisplay.startIndex && toDisplay.startIndex > 0, index === toDisplay.endIndex - 1 && toDisplay.endIndex < allChoices.length);
28775
+ if (index < metaOptions.length) {
28776
+ const title = renderMultiSelectTitle(choice.title, isHighlighted, figures, renderOptions);
28777
+ documents.push(prefix + (prefix.length === 0 ? "" : " ") + title);
28778
+ } else {
28779
+ const choiceIndex = index - metaOptions.length;
28780
+ const checkbox = state.selectedIndices.has(choiceIndex) ? figures.checkboxOn : figures.checkboxOff;
28781
+ const annotatedCheckbox = isHighlighted && renderOptions?.plain !== true ? annotate(checkbox, figures.primaryColor) : checkbox;
28782
+ const selectChoice = choice;
28783
+ const title = renderChoiceTitle(selectChoice, isHighlighted, figures, renderOptions);
28784
+ const description = renderChoiceDescription(selectChoice, isHighlighted, figures, renderOptions);
28785
+ const checkboxPrefix = checkboxWidth === 0 ? "" : annotatedCheckbox + " ".repeat(checkboxWidth - checkbox.length + 1);
28786
+ documents.push(prefix + (prefix.length === 0 ? "" : " ") + checkboxPrefix + title + " " + description);
28787
+ }
28788
+ }
28789
+ return documents.join("\n");
28790
+ };
28791
+ const renderMultiSelectNextFrame = /*#__PURE__*/ fnUntraced(function* (state, options) {
28792
+ const figures = yield* getTheme(options);
28793
+ const choices = renderMultiSelectChoices(state, options, figures);
28794
+ const leadingSymbol = annotateSymbol(figures.prefix, figures.primaryColor);
28795
+ const trailingSymbol = annotateSymbol(figures.pointerSmall, figures.mutedColor);
28796
+ const promptMsg = renderSelectOutput(leadingSymbol, trailingSymbol, options);
28797
+ const error = renderMultiSelectError(state, figures.pointer, figures);
28798
+ return cursorHide + withSelectHeader(promptMsg, choices) + error;
28799
+ });
28800
+ const renderMultiSelectSubmission = /*#__PURE__*/ fnUntraced(function* (state, options) {
28801
+ const figures = yield* getTheme(options);
28802
+ const selectedText = Array.from(state.selectedIndices).sort(Order$1).map((index) => options.choices[index].title).join(", ");
28803
+ const leadingSymbol = annotateSymbol(figures.tick, figures.successColor);
28804
+ const trailingSymbol = annotateSymbol(figures.ellipsis, figures.mutedColor);
28805
+ return (renderSelectOutput(leadingSymbol, trailingSymbol, options) ?? leadingSymbol) + " " + annotate(selectedText, figures.submittedColor) + "\n";
28806
+ });
28807
+ const processMultiSelectCursorUp = (state, totalChoices) => {
28808
+ const newIndex = state.index === 0 ? totalChoices - 1 : state.index - 1;
28809
+ return succeed$3(Action$1.NextFrame({ state: {
28810
+ ...state,
28811
+ index: newIndex
28812
+ } }));
28813
+ };
28814
+ const processMultiSelectCursorDown = (state, totalChoices) => {
28815
+ const newIndex = (state.index + 1) % totalChoices;
28816
+ return succeed$3(Action$1.NextFrame({ state: {
28817
+ ...state,
28818
+ index: newIndex
28819
+ } }));
28820
+ };
28821
+ const processSpace = (state, options) => {
28822
+ const selectedIndices = new Set(state.selectedIndices);
28823
+ if (state.index === 0) {
28824
+ const selectableCount = options.choices.filter((choice) => !choice.disabled).length;
28825
+ if (Array.from(state.selectedIndices).filter((index) => !options.choices[index].disabled).length === selectableCount) selectedIndices.clear();
28826
+ else for (let i = 0; i < options.choices.length; i++) if (!options.choices[i].disabled) selectedIndices.add(i);
28827
+ } else if (state.index === 1) for (let i = 0; i < options.choices.length; i++) if (options.choices[i].disabled || state.selectedIndices.has(i)) selectedIndices.delete(i);
28828
+ else selectedIndices.add(i);
28829
+ else {
28830
+ const choiceIndex = state.index - metaOptionsCount;
28831
+ if (options.choices[choiceIndex].disabled) return succeed$3(Action$1.Beep());
28832
+ else if (selectedIndices.has(choiceIndex)) selectedIndices.delete(choiceIndex);
28833
+ else selectedIndices.add(choiceIndex);
28834
+ }
28835
+ return succeed$3(Action$1.NextFrame({ state: {
28836
+ ...state,
28837
+ selectedIndices
28838
+ } }));
28839
+ };
28840
+ const handleMultiSelectClear = (options) => fnUntraced(function* (state, _) {
28841
+ const columns = yield* (yield* Terminal).columns;
28842
+ const figures = yield* getTheme(options);
28843
+ const clearPrompt = eraseLine$1 + cursorLeft$1;
28844
+ const promptText = renderSelectOutput(figures.prefix, figures.pointerSmall, options, { plain: true });
28845
+ const choicesText = renderMultiSelectChoices(state, options, figures, { plain: true });
28846
+ const errorText = renderMultiSelectError(state, figures.pointer, figures, { plain: true });
28847
+ return clearOutputWithError(withSelectHeader(promptText, choicesText), columns, errorText) + clearPrompt;
28848
+ });
28849
+ const handleMultiSelectProcess = (options) => {
28850
+ return (input, state) => {
28851
+ const totalChoices = options.choices.length + metaOptionsCount;
28852
+ switch (input.key.name) {
28853
+ case "k":
28854
+ case "up": return processMultiSelectCursorUp({
28855
+ ...state,
28856
+ error: none()
28857
+ }, totalChoices);
28858
+ case "j":
28859
+ case "down":
28860
+ case "tab": return processMultiSelectCursorDown({
28861
+ ...state,
28862
+ error: none()
28863
+ }, totalChoices);
28864
+ case "space": return processSpace(state, options);
28865
+ case "enter":
28866
+ case "return": {
28867
+ const selectedIndices = Array.from(state.selectedIndices).filter((index) => !options.choices[index].disabled);
28868
+ const selectedCount = selectedIndices.length;
28869
+ if (options.min !== void 0 && selectedCount < options.min) return succeed$3(Action$1.NextFrame({ state: {
28870
+ ...state,
28871
+ error: some(`At least ${options.min} are required`)
28872
+ } }));
28873
+ if (options.max !== void 0 && selectedCount > options.max) return succeed$3(Action$1.NextFrame({ state: {
28874
+ ...state,
28875
+ error: some(`At most ${options.max} choices are allowed`)
28876
+ } }));
28877
+ const selectedValues = selectedIndices.sort(Order$1).map((index) => options.choices[index].value);
28878
+ return succeed$3(Action$1.Submit({ value: selectedValues }));
28879
+ }
28880
+ default: return succeed$3(Action$1.Beep());
28881
+ }
28882
+ };
28883
+ };
28884
+ const handleMultiSelectRender = (options) => {
28885
+ return (state, action) => {
28886
+ return Action$1.$match(action, {
28887
+ Beep: () => succeed$3(renderBeep),
28888
+ NextFrame: ({ state }) => renderMultiSelectNextFrame(state, options),
28889
+ Submit: () => renderMultiSelectSubmission(state, options)
28890
+ });
28891
+ };
28892
+ };
28592
28893
  const handleNumberClear = (options) => {
28593
28894
  return fnUntraced(function* (state, _) {
28594
28895
  const columns = yield* (yield* Terminal).columns;
@@ -29597,7 +29898,7 @@ const withDefault$1 = /*#__PURE__*/ dual(2, (self, defaultValue) => {
29597
29898
  * @category combinators
29598
29899
  * @since 4.0.0
29599
29900
  */
29600
- const variadic = (self, options) => {
29901
+ const variadic$1 = (self, options) => {
29601
29902
  const single = getUnderlyingSingleOrThrow(self);
29602
29903
  const parse = (args) => {
29603
29904
  if (single.kind === "argument") return parsePositionalVariadic(self, single, args, options);
@@ -29642,7 +29943,7 @@ const variadic = (self, options) => {
29642
29943
  */
29643
29944
  const atLeast$1 = /*#__PURE__*/ dual(2, (self, min) => {
29644
29945
  if (min < 0) throw new Error("atLeast: min must be non-negative");
29645
- return variadic(self, { min });
29946
+ return variadic$1(self, { min });
29646
29947
  });
29647
29948
  /**
29648
29949
  * Sets a custom metavar (placeholder name) for the param in help documentation.
@@ -29771,7 +30072,7 @@ const transformSingle = (param, f) => {
29771
30072
  Map: (mapped) => map$1(transformSingle(mapped.param, f), mapped.f),
29772
30073
  Transform: (mapped) => transform(transformSingle(mapped.param, f), mapped.f, mapped.alternatives.map((alternative) => () => transformSingle(alternative(), f))),
29773
30074
  Optional: (p) => optional$2(transformSingle(p.param, f)),
29774
- Variadic: (p) => variadic(transformSingle(p.param, f), {
30075
+ Variadic: (p) => variadic$1(transformSingle(p.param, f), {
29775
30076
  min: getOrUndefined$1(p.min),
29776
30077
  max: getOrUndefined$1(p.max)
29777
30078
  })
@@ -29886,6 +30187,35 @@ const optional$1 = (arg) => optional$2(arg);
29886
30187
  * @since 4.0.0
29887
30188
  */
29888
30189
  const withDescription$2 = /*#__PURE__*/ dual(2, (self, description) => withDescription$3(self, description));
30190
+ /**
30191
+ * Creates a variadic positional argument that accepts multiple values.
30192
+ *
30193
+ * **Example** (Accepting multiple values)
30194
+ *
30195
+ * ```ts import.meta.vitest
30196
+ * import { Argument } from "effect/cli"
30197
+ *
30198
+ * // Accept any number of files
30199
+ * const anyFiles = Argument.String("files").pipe(Argument.variadic)
30200
+ *
30201
+ * // Accept at least 1 file
30202
+ * const atLeastOneFile = Argument.String("files").pipe(
30203
+ * Argument.variadic({ min: 1 })
30204
+ * )
30205
+ *
30206
+ * // Accept between 1 and 5 files
30207
+ * const limitedFiles = Argument.String("files").pipe(
30208
+ * Argument.variadic({ min: 1, max: 5 })
30209
+ * )
30210
+ *
30211
+ * const kinds = [anyFiles.kind, atLeastOneFile.kind, limitedFiles.kind] // => ["argument", "argument", "argument"]
30212
+ * ```
30213
+ *
30214
+ * @stability unstable
30215
+ * @category combinators
30216
+ * @since 4.0.0
30217
+ */
30218
+ const variadic = /*#__PURE__*/ dual((args) => isParam(args[0]), (self, options) => variadic$1(self, options));
29889
30219
  //#endregion
29890
30220
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Console.js
29891
30221
  /**
@@ -30183,7 +30513,7 @@ const visualLength = (text) => {
30183
30513
  * Helper function to pad strings to a specified width.
30184
30514
  * @internal
30185
30515
  */
30186
- const pad = (s, width) => {
30516
+ const pad$1 = (s, width) => {
30187
30517
  const actualLength = visualLength(s);
30188
30518
  const padding = Math.max(0, width - actualLength);
30189
30519
  return s + " ".repeat(padding);
@@ -30195,7 +30525,7 @@ const pad = (s, width) => {
30195
30525
  const renderTable = (rows, widthCap) => {
30196
30526
  const maxColumn = Math.max(...rows.map((r) => visualLength(r.left))) + 4;
30197
30527
  const col = widthCap === void 0 ? maxColumn : Math.min(maxColumn, widthCap);
30198
- return rows.map(({ left, right }) => ` ${pad(left, Math.max(col, visualLength(left) + 1))}${right}`).join("\n");
30528
+ return rows.map(({ left, right }) => ` ${pad$1(left, Math.max(col, visualLength(left) + 1))}${right}`).join("\n");
30199
30529
  };
30200
30530
  const formatSubcommandName = (name, alias) => alias ? `${name}, ${alias}` : name;
30201
30531
  /**
@@ -31273,6 +31603,40 @@ const parseConfig$1 = (config) => {
31273
31603
  * @internal
31274
31604
  */
31275
31605
  const emptyConfig = /*#__PURE__*/ parseConfig$1({});
31606
+ const shiftNodeIndexes = (node, offset) => {
31607
+ switch (node._tag) {
31608
+ case "Param": return {
31609
+ _tag: "Param",
31610
+ index: node.index + offset
31611
+ };
31612
+ case "Array": return {
31613
+ _tag: "Array",
31614
+ children: node.children.map((child) => shiftNodeIndexes(child, offset))
31615
+ };
31616
+ case "Nested": return {
31617
+ _tag: "Nested",
31618
+ tree: shiftTreeIndexes(node.tree, offset)
31619
+ };
31620
+ }
31621
+ };
31622
+ const shiftTreeIndexes = (tree, offset) => {
31623
+ const output = Object.create(null);
31624
+ for (const key of Object.keys(tree)) output[key] = shiftNodeIndexes(tree[key], offset);
31625
+ return output;
31626
+ };
31627
+ /**
31628
+ * @internal
31629
+ */
31630
+ const mergeConfig = (left, right) => {
31631
+ const offset = left.orderedParams.length;
31632
+ return {
31633
+ [ConfigInternalTypeId]: ConfigInternalTypeId,
31634
+ flags: [...left.flags, ...right.flags],
31635
+ arguments: [...left.arguments, ...right.arguments],
31636
+ orderedParams: [...left.orderedParams, ...right.orderedParams],
31637
+ tree: Object.assign(Object.create(null), left.tree, shiftTreeIndexes(right.tree, offset))
31638
+ };
31639
+ };
31276
31640
  /**
31277
31641
  * Reconstructs the original nested config shape from parsed values.
31278
31642
  *
@@ -32652,7 +33016,7 @@ const promptSingle = (single) => {
32652
33016
  case "Date": return map$5(run$2(Date$1({ message })), (date) => commandLineArg(date.toISOString()));
32653
33017
  case "Finite": return map$5(run$2(Number$1({ message })), (value) => commandLineArg(String(value)));
32654
33018
  case "Int": return map$5(run$2(Int({ message })), (value) => commandLineArg(String(value)));
32655
- case "Redacted": return map$5(run$2(Password({ message })), (value) => commandLineArg(value$1(value), "<redacted>"));
33019
+ case "Redacted": return map$5(run$2(Password({ message })), (value) => commandLineArg(value$2(value), "<redacted>"));
32656
33020
  default: return map$5(run$2(String$4({ message })), commandLineArg);
32657
33021
  }
32658
33022
  };
@@ -33033,6 +33397,64 @@ const withSubcommands = /*#__PURE__*/ dual(2, (self, subcommands) => {
33033
33397
  });
33034
33398
  });
33035
33399
  /**
33400
+ * Adds flags that are inherited by subcommands.
33401
+ *
33402
+ * **Details**
33403
+ *
33404
+ * Shared flags are available to this command's handler and to descendant
33405
+ * handlers via `yield* parentCommand`. Shared flags are accepted both before
33406
+ * and after a selected subcommand name (npm-style).
33407
+ *
33408
+ * @stability unstable
33409
+ * @category combinators
33410
+ * @since 4.0.0
33411
+ */
33412
+ const withSharedFlags = /*#__PURE__*/ dual(2, (self, sharedFlags) => {
33413
+ const impl = toImpl(self);
33414
+ const sharedConfig = parseConfig$1(sharedFlags);
33415
+ const mergedConfig = mergeConfig(impl.config, sharedConfig);
33416
+ const mergedContextConfig = mergeConfig(impl.contextConfig, sharedConfig);
33417
+ if (impl.subcommands.length > 0) {
33418
+ const flatSubcommands = impl.subcommands.flatMap((group) => group.commands);
33419
+ checkForDuplicateFlags(self, flatSubcommands, { contextConfig: mergedContextConfig });
33420
+ }
33421
+ const parseShared = makeParser(sharedConfig, { allowLeftovers: true });
33422
+ const parse = fnUntraced(function* (raw) {
33423
+ const base = yield* impl.parse(raw);
33424
+ const shared = yield* parseShared(raw);
33425
+ return {
33426
+ ...base,
33427
+ ...shared
33428
+ };
33429
+ });
33430
+ const parseContext = fnUntraced(function* (raw) {
33431
+ const base = yield* impl.parseContext(raw);
33432
+ const shared = yield* parseShared(raw);
33433
+ return {
33434
+ ...base,
33435
+ ...shared
33436
+ };
33437
+ });
33438
+ const handle = (input, commandPath) => impl.handle(input, commandPath);
33439
+ return makeCommand({
33440
+ name: impl.name,
33441
+ config: mergedConfig,
33442
+ contextConfig: mergedContextConfig,
33443
+ description: impl.description,
33444
+ shortDescription: impl.shortDescription,
33445
+ alias: impl.alias,
33446
+ unlisted: impl.unlisted,
33447
+ annotations: impl.annotations,
33448
+ globalFlags: impl.globalFlags,
33449
+ examples: impl.examples,
33450
+ service: impl.service,
33451
+ subcommands: impl.subcommands,
33452
+ parse,
33453
+ parseContext,
33454
+ handle
33455
+ });
33456
+ });
33457
+ /**
33036
33458
  * Sets the description for a command.
33037
33459
  *
33038
33460
  * **Details**
@@ -33342,264 +33764,14 @@ const runWith = (command, config) => {
33342
33764
  };
33343
33765
  //#endregion
33344
33766
  //#region package.json
33345
- var version = "0.11.0";
33346
- //#endregion
33347
- //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Logger.js
33348
- const TypeId = "~effect/Logger";
33349
- /**
33350
- * Returns `true` if the specified value is a `Logger`, otherwise returns `false`.
33351
- *
33352
- * **Example** (Checking logger values)
33353
- *
33354
- * ```ts import.meta.vitest
33355
- * import { Logger } from "effect"
33356
- *
33357
- * const myLogger = Logger.make(() => undefined)
33358
- *
33359
- * Logger.isLogger(myLogger) // => true
33360
- * Logger.isLogger("not a logger") // => false
33361
- * Logger.isLogger({ log: () => {} }) // => false
33362
- * ```
33363
- *
33364
- * @stability stable
33365
- * @category guards
33366
- * @since 4.0.0
33367
- */
33368
- const isLogger = (u) => hasProperty(u, TypeId);
33369
- /**
33370
- * Context reference containing the active loggers for the current fiber.
33371
- *
33372
- * **Details**
33373
- *
33374
- * By default this set includes the default logger and the tracer logger.
33375
- * Providing `Logger.layer` replaces or merges with this set depending on its
33376
- * options.
33377
- *
33378
- * **Example** (Accessing current loggers)
33379
- *
33380
- * ```ts import.meta.vitest
33381
- * import { Effect, Logger } from "effect"
33382
- *
33383
- * const messages: Array<unknown> = []
33384
- * const customLogger = Logger.make((options) => {
33385
- * messages.push(options.message)
33386
- * })
33387
- * const program = Effect.gen(function*() {
33388
- * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers)
33389
- * yield* Effect.log("Hello from custom logger").pipe(
33390
- * Effect.provide(Logger.layer([customLogger]))
33391
- * )
33392
- * return currentLoggers.has(Logger.defaultLogger)
33393
- * })
33394
- *
33395
- * Effect.runSync(program) // => true
33396
- * messages // => [["Hello from custom logger"]]
33397
- * ```
33398
- *
33399
- * @stability stable
33400
- * @category services
33401
- * @since 4.0.0
33402
- */
33403
- const CurrentLoggers = CurrentLoggers$1;
33404
- /**
33405
- * Creates a new `Logger` from a log function.
33406
- *
33407
- * **Details**
33408
- *
33409
- * The log function receives an options object containing the message, log level,
33410
- * cause, fiber information, and timestamp, and should return the desired output.
33411
- *
33412
- * **Example** (Creating loggers from functions)
33413
- *
33414
- * ```ts import.meta.vitest
33415
- * import { Effect, Logger } from "effect"
33416
- *
33417
- * const outputs: Array<string> = []
33418
- * const textLogger = Logger.make((options) =>
33419
- * `${options.logLevel}: ${options.message}`
33420
- * )
33421
- * const collector = Logger.make((options) => outputs.push(textLogger.log(options)))
33422
- *
33423
- * const program = Effect.log("Hello World").pipe(
33424
- * Effect.provide(Logger.layer([collector]))
33425
- * )
33426
- * Effect.runSync(program)
33427
- * outputs // => ["Info: Hello World"]
33428
- * ```
33429
- *
33430
- * @stability stable
33431
- * @category constructors
33432
- * @since 2.0.0
33433
- */
33434
- const make = loggerMake;
33435
- /**
33436
- * Creates a scoped logger that batches the output of another logger.
33437
- *
33438
- * **Details**
33439
- *
33440
- * The returned effect starts a scoped background process that periodically
33441
- * passes buffered outputs to `flush`. When the scope closes, the background
33442
- * process is interrupted and any remaining buffered entries are flushed.
33443
- *
33444
- * **Example** (Batching logger output)
33445
- *
33446
- * ```ts import.meta.vitest
33447
- * import { Effect, Logger } from "effect"
33448
- *
33449
- * const flushed: Array<ReadonlyArray<string>> = []
33450
- * const messageLogger = Logger.make((options) => String(options.message))
33451
- * const batchedLogger = Logger.batched(messageLogger, {
33452
- * window: "1 hour",
33453
- * flush: (messages) =>
33454
- * Effect.sync(() => {
33455
- * flushed.push(messages)
33456
- * })
33457
- * })
33458
- *
33459
- * const program = Effect.scoped(Effect.gen(function*() {
33460
- * const logger = yield* batchedLogger
33461
- * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger])))
33462
- * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger])))
33463
- * }))
33464
- * await Effect.runPromise(program)
33465
- * flushed // => [["Event 1", "Event 2"]]
33466
- * ```
33467
- *
33468
- * @stability stable
33469
- * @category constructors
33470
- * @since 2.0.0
33471
- */
33472
- const batched = /*#__PURE__*/ dual(2, (self, options) => suspend$3(() => {
33473
- let buffer = [];
33474
- const flush = uninterruptible$1(suspend$3(() => {
33475
- if (buffer.length === 0) return void_$2;
33476
- const arr = buffer;
33477
- buffer = [];
33478
- return options.flush(arr);
33479
- }));
33480
- return uninterruptibleMask$1((restore) => restore(sleep$1(options.window).pipe(andThen$1(flush), forever$2)).pipe(forkDetach, flatMap$2((fiber) => addFinalizer$2(() => andThen$1(fiberInterrupt(fiber), flush))), as$1(loggerMake((options) => {
33481
- buffer.push(self.log(options));
33482
- }))));
33483
- }));
33484
- /**
33485
- * Creates a `Layer` which will overwrite the current set of loggers with the
33486
- * specified array of `loggers`.
33487
- *
33488
- * **Details**
33489
- *
33490
- * If the specified array of `loggers` should be _merged_ with the current set
33491
- * of loggers (instead of overwriting them), set `mergeWithExisting` to `true`.
33492
- *
33493
- * **Example** (Providing logger layers)
33494
- *
33495
- * ```ts import.meta.vitest
33496
- * import { Effect, Logger } from "effect"
33497
- *
33498
- * const messages: Array<unknown> = []
33499
- * const customLogger = Logger.make((options) => {
33500
- * messages.push(options.message)
33501
- * })
33502
- * const CustomLoggerLayer = Logger.layer([customLogger])
33503
- *
33504
- * const program = Effect.log("Application started").pipe(
33505
- * Effect.provide(CustomLoggerLayer)
33506
- * )
33507
- * Effect.runSync(program)
33508
- * messages // => [["Application started"]]
33509
- * ```
33510
- *
33511
- * @stability stable
33512
- * @category layers
33513
- * @since 4.0.0
33514
- */
33515
- const layer$1 = (loggers, options) => effect(CurrentLoggers, withFiber$1(fnUntraced$1(function* (fiber) {
33516
- const currentLoggers = new Set(options?.mergeWithExisting === true ? fiber.getRef(CurrentLoggers$1) : []);
33517
- for (const logger of loggers) currentLoggers.add(isEffect$1(logger) ? yield* logger : logger);
33518
- return currentLoggers;
33519
- })));
33520
- /**
33521
- * Creates a scoped logger that writes string logger output to a file.
33522
- *
33523
- * **Details**
33524
- *
33525
- * The returned effect requires `FileSystem` and `Scope`. The file logger batches
33526
- * string output, writes each batch to the specified path, and flushes remaining
33527
- * entries when the scope closes.
33528
- *
33529
- * **Example** (Writing JSON logs to a file)
33530
- *
33531
- * ```ts import.meta.vitest
33532
- * import { Effect, FileSystem, Logger } from "effect"
33533
- *
33534
- * const writes: Array<string> = []
33535
- * const file = {
33536
- * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
33537
- * writes.push(new TextDecoder().decode(buffer).trim())
33538
- * })
33539
- * } as unknown as FileSystem.File
33540
- * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
33541
- * const messageLogger = Logger.make((options) => String(options.message))
33542
- *
33543
- * const program = Effect.scoped(Effect.gen(function*() {
33544
- * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt")
33545
- * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger])))
33546
- * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger])))
33547
- * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger])))
33548
- * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
33549
- *
33550
- * await Effect.runPromise(program)
33551
- * writes // => ["a\nb\nc"]
33552
- * ```
33553
- *
33554
- * **Example** (Writing logs to files)
33555
- *
33556
- * ```ts import.meta.vitest
33557
- * import { Effect, FileSystem, Logger } from "effect"
33558
- *
33559
- * const writes: Array<string> = []
33560
- * const file = {
33561
- * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
33562
- * writes.push(new TextDecoder().decode(buffer).trim())
33563
- * })
33564
- * } as unknown as FileSystem.File
33565
- * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
33566
- * const messageLogger = Logger.make((options) => String(options.message))
33567
- *
33568
- * const program = Effect.scoped(Effect.gen(function*() {
33569
- * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", {
33570
- * batchWindow: "1 hour"
33571
- * })
33572
- * yield* Effect.log("Application started").pipe(
33573
- * Effect.provide(Logger.layer([fileLogger]))
33574
- * )
33575
- * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
33576
- *
33577
- * await Effect.runPromise(program)
33578
- * writes // => ["Application started"]
33579
- * ```
33580
- *
33581
- * @stability unstable
33582
- * @category logging
33583
- * @since 4.0.0
33584
- */
33585
- const toFile = /*#__PURE__*/ dual((args) => isLogger(args[0]), (self, path, options) => gen$1(function* () {
33586
- const logFile = yield* (yield* FileSystem).open(path, {
33587
- flag: "a+",
33588
- ...options
33589
- });
33590
- const encoder = new TextEncoder();
33591
- return yield* batched(self, {
33592
- window: options?.batchWindow ?? 1e3,
33593
- flush: (output) => ignore$1(logFile.writeAll(encoder.encode(output.join("\n") + "\n")))
33594
- });
33595
- }));
33767
+ var version = "0.12.0";
33596
33768
  //#endregion
33597
33769
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
33598
33770
  /**
33599
33771
  * Creates a JSON scanner on the given text.
33600
33772
  * If ignoreTrivia is set, whitespaces or comments are ignored.
33601
33773
  */
33602
- function createScanner(text, ignoreTrivia = false) {
33774
+ function createScanner$1(text, ignoreTrivia = false) {
33603
33775
  const len = text.length;
33604
33776
  let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
33605
33777
  function scanHexDigits(count, exact) {
@@ -34010,7 +34182,7 @@ const supportedEols = [
34010
34182
  ];
34011
34183
  //#endregion
34012
34184
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/format.js
34013
- function format(documentText, range, options) {
34185
+ function format$1(documentText, range, options) {
34014
34186
  let initialIndentLevel;
34015
34187
  let formatText;
34016
34188
  let formatTextStart;
@@ -34040,7 +34212,7 @@ function format(documentText, range, options) {
34040
34212
  if (options.insertSpaces) indentValue = cachedSpaces[options.tabSize || 4] ?? repeat(cachedSpaces[1], options.tabSize || 4);
34041
34213
  else indentValue = " ";
34042
34214
  const indentType = indentValue === " " ? " " : " ";
34043
- let scanner = createScanner(formatText, false);
34215
+ let scanner = createScanner$1(formatText, false);
34044
34216
  let hasError = false;
34045
34217
  function newLinesAndIndent() {
34046
34218
  if (numberLineBreaks > 1) return repeat(eol, numberLineBreaks) + repeat(indentValue, initialIndentLevel + indentLevel);
@@ -34187,7 +34359,7 @@ var ParseOptions;
34187
34359
  * 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.
34188
34360
  * Therefore always check the errors list to find out if the input was valid.
34189
34361
  */
34190
- function parse$2(text, errors = [], options = ParseOptions.DEFAULT) {
34362
+ function parse$3(text, errors = [], options = ParseOptions.DEFAULT) {
34191
34363
  let currentProperty = null;
34192
34364
  let currentParent = [];
34193
34365
  const previousParents = [];
@@ -34347,11 +34519,27 @@ function findNodeAtLocation$1(root, path) {
34347
34519
  }
34348
34520
  return node;
34349
34521
  }
34522
+ function contains(node, offset, includeRightBound = false) {
34523
+ return offset >= node.offset && offset < node.offset + node.length || includeRightBound && offset === node.offset + node.length;
34524
+ }
34525
+ /**
34526
+ * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset.
34527
+ */
34528
+ function findNodeAtOffset$1(node, offset, includeRightBound = false) {
34529
+ if (contains(node, offset, includeRightBound)) {
34530
+ const children = node.children;
34531
+ if (Array.isArray(children)) for (let i = 0; i < children.length && children[i].offset <= offset; i++) {
34532
+ const item = findNodeAtOffset$1(children[i], offset, includeRightBound);
34533
+ if (item) return item;
34534
+ }
34535
+ return node;
34536
+ }
34537
+ }
34350
34538
  /**
34351
34539
  * Parses the given text and invokes the visitor functions for each object, array and literal reached.
34352
34540
  */
34353
34541
  function visit(text, visitor, options = ParseOptions.DEFAULT) {
34354
- const _scanner = createScanner(text, false);
34542
+ const _scanner = createScanner$1(text, false);
34355
34543
  const _jsonPath = [];
34356
34544
  let suppressedCallbacks = 0;
34357
34545
  function toNoArgVisit(visitFunction) {
@@ -34703,7 +34891,7 @@ function withFormatting(text, edit, options) {
34703
34891
  while (begin > 0 && !isEOL(newText, begin - 1)) begin--;
34704
34892
  while (end < newText.length && !isEOL(newText, end)) end++;
34705
34893
  }
34706
- const edits = format(newText, {
34894
+ const edits = format$1(newText, {
34707
34895
  offset: begin,
34708
34896
  length: end - begin
34709
34897
  }, {
@@ -34729,6 +34917,11 @@ function applyEdit(text, edit) {
34729
34917
  }
34730
34918
  //#endregion
34731
34919
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
34920
+ /**
34921
+ * Creates a JSON scanner on the given text.
34922
+ * If ignoreTrivia is set, whitespaces or comments are ignored.
34923
+ */
34924
+ const createScanner = createScanner$1;
34732
34925
  var ScanError;
34733
34926
  (function(ScanError) {
34734
34927
  ScanError[ScanError["None"] = 0] = "None";
@@ -34763,7 +34956,7 @@ var SyntaxKind;
34763
34956
  * 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.
34764
34957
  * Therefore, always check the errors list to find out if the input was valid.
34765
34958
  */
34766
- const parse$1 = parse$2;
34959
+ const parse$2 = parse$3;
34767
34960
  /**
34768
34961
  * 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.
34769
34962
  */
@@ -34772,6 +34965,10 @@ const parseTree = parseTree$1;
34772
34965
  * Finds the node at the given path in a JSON DOM.
34773
34966
  */
34774
34967
  const findNodeAtLocation = findNodeAtLocation$1;
34968
+ /**
34969
+ * Finds the innermost node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset.
34970
+ */
34971
+ const findNodeAtOffset = findNodeAtOffset$1;
34775
34972
  var ParseErrorCode;
34776
34973
  (function(ParseErrorCode) {
34777
34974
  ParseErrorCode[ParseErrorCode["InvalidSymbol"] = 1] = "InvalidSymbol";
@@ -36257,6 +36454,10 @@ var BumpError = class extends TaggedError()("BumpError", {
36257
36454
  }
36258
36455
  };
36259
36456
  const bumpedRange = (current, installed) => import_semver.valid(current) !== null ? installed : `^${installed}`;
36457
+ const addedRange = (installed, published) => {
36458
+ const version = installed ?? import_semver.maxSatisfying([...published], "*") ?? void 0;
36459
+ return version === void 0 ? void 0 : `^${version}`;
36460
+ };
36260
36461
  const bumpText = (text, path, installed) => {
36261
36462
  const tree = parseTree(text) ?? {
36262
36463
  type: "null",
@@ -36298,13 +36499,14 @@ const Origin = String$6.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
36298
36499
  }));
36299
36500
  const Ref = NonEmptyString;
36300
36501
  const isPin = (ref) => /^[0-9a-f]{40}$/.test(ref);
36301
- const ProviderName = Literals$2([
36502
+ const PROVIDER_NAMES = [
36302
36503
  "files",
36303
36504
  "vp",
36304
36505
  "brew",
36305
36506
  "cask",
36306
36507
  "repos"
36307
- ]);
36508
+ ];
36509
+ const ProviderName = Literals$2(PROVIDER_NAMES);
36308
36510
  const Range = NonEmptyString.check(makeFilter((range) => range === "latest" || import_semver.validRange(range) !== null, {
36309
36511
  title: "Range",
36310
36512
  description: "a semver range (leave inside it, install below it, ask above it), \"*\" (install when missing, never upgrade), or \"latest\" (upgrade to the newest on every Ship)"
@@ -36347,6 +36549,7 @@ const NOT_GLOBALS = {
36347
36549
  "vite-plus": "set vp with vp.version",
36348
36550
  fleetfizz: "a Ship installs the fleetfizz that ships"
36349
36551
  };
36552
+ const isGlobalName = (name) => !Object.hasOwn(NOT_GLOBALS, name);
36350
36553
  const VpSection = section({
36351
36554
  version: optionalKey(Range).annotateKey({ description: "The vp version. Pin it exactly." }),
36352
36555
  node: optionalKey(Range).annotateKey({ description: "The default Node version. Pin it exactly." }),
@@ -36355,7 +36558,7 @@ const VpSection = section({
36355
36558
  yarn: optionalKey(Range).annotateKey({ description: "The default Yarn version." }),
36356
36559
  bun: optionalKey(Range).annotateKey({ description: "The default Bun version." })
36357
36560
  }, Record(NonEmptyString, variants(Global)).check(makeFilter((globals) => {
36358
- const name = Object.keys(globals).find((n) => n in NOT_GLOBALS);
36561
+ const name = Object.keys(globals).find((n) => !isGlobalName(n));
36359
36562
  return name === void 0 || `"${name}" is not a Global: ${NOT_GLOBALS[name]}`;
36360
36563
  }, {
36361
36564
  title: "Globals",
@@ -36425,17 +36628,6 @@ const FleetConfig = Struct({
36425
36628
  });
36426
36629
  const SELF_PACKAGE = "fleetfizz";
36427
36630
  //#endregion
36428
- //#region ../../packages/config/src/jsonSchema.ts
36429
- const fleetConfigJsonSchema = () => {
36430
- const document = toJsonSchemaDocument(FleetConfig, { onExcessProperty: "error" });
36431
- return {
36432
- $schema: META_SCHEMA_URI_DRAFT_2020_12,
36433
- title: "fleetfizz config",
36434
- ...document.schema,
36435
- $defs: document.definitions
36436
- };
36437
- };
36438
- //#endregion
36439
36631
  //#region ../../packages/config/src/load.ts
36440
36632
  const CONFIG_FILE = "config.jsonc";
36441
36633
  const SCHEMA_FILE = "schema.json";
@@ -36451,7 +36643,7 @@ var ConfigError = class extends TaggedError()("ConfigError", {
36451
36643
  };
36452
36644
  var ConfigNotFound = class extends TaggedError()("ConfigNotFound", { path: String$6 }) {
36453
36645
  get message() {
36454
- return `no config at ${this.path}; write one there (the README has an example) or pass --config`;
36646
+ return `no config at ${this.path}; run fizz init to write one, or pass --config`;
36455
36647
  }
36456
36648
  };
36457
36649
  const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
@@ -36491,7 +36683,7 @@ const movedKeys = (parsed) => {
36491
36683
  };
36492
36684
  const parseConfig = fn("parseConfig")(function* (path, text) {
36493
36685
  const parseErrors = [];
36494
- const parsed = parse$1(text, parseErrors, { allowTrailingComma: true });
36686
+ const parsed = parse$2(text, parseErrors, { allowTrailingComma: true });
36495
36687
  if (parseErrors.length > 0) return yield* new ConfigError({
36496
36688
  path,
36497
36689
  problems: parseErrors.map((e) => `line ${lineOf(text, e.offset)}: ${printParseErrorCode(e.error)}`)
@@ -36518,6 +36710,239 @@ const loadConfig = fn("loadConfig")(function* (path) {
36518
36710
  const text = yield* fs.readFileString(path);
36519
36711
  return yield* parseConfig(path, text);
36520
36712
  });
36713
+ //#endregion
36714
+ //#region ../../packages/config/src/edit.ts
36715
+ const WIDTH = 100;
36716
+ const parse$1 = (text) => parseTree(text, void 0, { allowTrailingComma: true }) ?? {
36717
+ type: "object",
36718
+ offset: 0,
36719
+ length: 0,
36720
+ children: []
36721
+ };
36722
+ const lineStart = (text, offset) => text.lastIndexOf("\n", offset - 1) + 1;
36723
+ const indentAt = (text, offset) => /^[ \t]*/.exec(text.slice(lineStart(text, offset)))?.[0] ?? "";
36724
+ const format = (value, indent, column) => {
36725
+ const inline = formatInline(value);
36726
+ if (column + inline.length <= WIDTH || typeof value !== "object" || value === null) return inline;
36727
+ const inner = `${indent} `;
36728
+ if (Array.isArray(value)) return `[\n${value.map((v) => `${inner}${format(v, inner, inner.length)},\n`).join("")}${indent}]`;
36729
+ return `{\n${Object.entries(value).map(([k, v]) => {
36730
+ const key = `${JSON.stringify(k)}: `;
36731
+ return `${inner}${key}${format(v, inner, inner.length + key.length)},\n`;
36732
+ }).join("")}${indent}}`;
36733
+ };
36734
+ const formatInline = (value) => {
36735
+ if (Array.isArray(value)) return `[${value.map(formatInline).join(", ")}]`;
36736
+ if (typeof value === "object" && value !== null) {
36737
+ const entries = Object.entries(value).map(([k, v]) => `${JSON.stringify(k)}: ${formatInline(v)}`);
36738
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
36739
+ }
36740
+ return JSON.stringify(value);
36741
+ };
36742
+ const BLOCK_COMMENTS = /^(?:[ \t]*\/\*(?:(?!\*\/)[^\n])*\*\/)*/;
36743
+ const tail = (text, end) => {
36744
+ const close = end + (BLOCK_COMMENTS.exec(text.slice(end))?.[0].length ?? 0);
36745
+ let at = close;
36746
+ while (text[at] === " " || text[at] === " ") at++;
36747
+ const comma = text[at] === ",";
36748
+ if (comma) at++;
36749
+ const blocks = BLOCK_COMMENTS.exec(text.slice(at))?.[0].length ?? 0;
36750
+ const rest = /^[ \t]*(\/\/[^\n]*)?(?=\n|$)/.exec(text.slice(at + blocks));
36751
+ return {
36752
+ comma,
36753
+ close,
36754
+ end: comma ? at : end,
36755
+ lineEnd: rest === null ? void 0 : at + blocks + rest[0].length
36756
+ };
36757
+ };
36758
+ const memberOf = (node) => node.parent?.type === "property" ? node.parent : node;
36759
+ const insertMember = (text, container, render) => {
36760
+ const open = container.offset;
36761
+ const close = container.offset + container.length - 1;
36762
+ const last = (container.children ?? []).at(-1);
36763
+ const inside = text.slice(open + 1, close);
36764
+ if (last === void 0 && inside.trim() !== "") {
36765
+ const indent = indentAt(text, open);
36766
+ if (!inside.includes("\n")) return `${text.slice(0, close).trimEnd()} ${render(indent, close - lineStart(text, close) + 1)} ${text.slice(close)}`;
36767
+ const inner = `${indent} `;
36768
+ const at = lineStart(text, close);
36769
+ if (text.slice(at, close).trim() !== "") return `${text.slice(0, close).trimEnd()}\n${inner}${render(inner, inner.length)},\n${indent}${text.slice(close)}`;
36770
+ return `${text.slice(0, at)}${inner}${render(inner, inner.length)},\n${text.slice(at)}`;
36771
+ }
36772
+ if (last === void 0) {
36773
+ const member = render(indentAt(text, open), open - lineStart(text, open) + 2);
36774
+ const [l, r] = container.type === "array" ? ["[", "]"] : ["{ ", " }"];
36775
+ const body = `${l}${member}${r}`;
36776
+ return text.slice(0, open) + body + text.slice(close + 1);
36777
+ }
36778
+ const multiline = text.slice(open, close).includes("\n");
36779
+ const after = tail(text, last.offset + last.length);
36780
+ if (!multiline || after.lineEnd === void 0) {
36781
+ const at = after.close;
36782
+ return `${text.slice(0, at)}, ${render("", at - lineStart(text, at) + 2)}${text.slice(at)}`;
36783
+ }
36784
+ const indent = indentAt(text, last.offset);
36785
+ const member = `\n${indent}${render(indent, indent.length)}${after.comma ? "," : ""}`;
36786
+ const comma = after.comma ? "" : ",";
36787
+ const at = last.offset + last.length;
36788
+ return text.slice(0, at) + comma + text.slice(at, after.lineEnd) + member + text.slice(after.lineEnd);
36789
+ };
36790
+ const spread = (text, container) => {
36791
+ const open = container.offset;
36792
+ const close = container.offset + container.length - 1;
36793
+ const indent = indentAt(text, open);
36794
+ const [l, r] = container.type === "array" ? ["[", "]"] : ["{", "}"];
36795
+ const body = (container.children ?? []).map((m) => `${indent} ${text.slice(m.offset, m.offset + m.length)},\n`).join("");
36796
+ return `${text.slice(0, open)}${l}\n${body}${indent}${r}${text.slice(close + 1)}`;
36797
+ };
36798
+ const oneLineAround = (text, tree, offset) => {
36799
+ const start = lineStart(text, offset);
36800
+ let found;
36801
+ for (let node = findNodeAtOffset(tree, offset, true); node !== void 0; node = node.parent) if ((node.type === "object" || node.type === "array") && node.offset >= start && !text.slice(node.offset, node.offset + node.length).includes("\n")) found = node;
36802
+ return found;
36803
+ };
36804
+ const lineLength = (text, offset) => {
36805
+ const end = text.indexOf("\n", offset);
36806
+ return (end < 0 ? text.length : end) - lineStart(text, offset);
36807
+ };
36808
+ const valueOf = (text) => parse$2(text, void 0, { allowTrailingComma: true });
36809
+ const commentsOf = (text) => {
36810
+ const scanner = createScanner(text, false);
36811
+ const found = [];
36812
+ for (let kind = scanner.scan(); kind !== SyntaxKind.EOF; kind = scanner.scan()) if (kind === SyntaxKind.LineCommentTrivia || kind === SyntaxKind.BlockCommentTrivia) found.push(scanner.getTokenValue());
36813
+ return found;
36814
+ };
36815
+ const sameComments = (a, b) => commentsOf(a).join("\0") === commentsOf(b).join("\0");
36816
+ const changedAt = (root, path, change) => {
36817
+ const copy = structuredClone(root);
36818
+ let parent = copy;
36819
+ for (const key of path.slice(0, -1)) {
36820
+ parent[key] ??= {};
36821
+ parent = parent[key];
36822
+ }
36823
+ const key = path.at(-1);
36824
+ if (key !== void 0) change(parent, key);
36825
+ return copy;
36826
+ };
36827
+ const checked = (text, result, expected, comments) => "reason" in result || JSON.stringify(valueOf(result.text)) === JSON.stringify(expected) && (!comments || sameComments(text, result.text)) ? result : { reason: "this part of the config is written in a way fizz cannot edit; use fizz config edit" };
36828
+ const insertAt = (text, path, value) => checked(text, insertSpread(text, path, value), changedAt(valueOf(text), path, (parent, key) => {
36829
+ if (Array.isArray(parent)) parent.push(value);
36830
+ else parent[key] = value;
36831
+ }), true);
36832
+ const insertSpread = (text, path, value) => {
36833
+ const result = insertText(text, path, value);
36834
+ if (!("text" in result)) return result;
36835
+ const at = firstDifference(text, result.text);
36836
+ if (lineLength(result.text, at) <= WIDTH) return result;
36837
+ const container = oneLineAround(text, parse$1(text), Math.min(at, text.length - 1));
36838
+ if (container === void 0) return result;
36839
+ const spreadText = spread(text, container);
36840
+ return sameComments(text, spreadText) ? insertSpread(spreadText, path, value) : result;
36841
+ };
36842
+ const firstDifference = (a, b) => {
36843
+ let i = 0;
36844
+ while (i < a.length && a[i] === b[i]) i++;
36845
+ return i;
36846
+ };
36847
+ const insertText = (text, path, value) => {
36848
+ const tree = parse$1(text);
36849
+ let depth = 0;
36850
+ let node = tree;
36851
+ while (depth < path.length) {
36852
+ const next = findNodeAtLocation(tree, path.slice(0, depth + 1));
36853
+ if (next === void 0) break;
36854
+ node = next;
36855
+ depth++;
36856
+ }
36857
+ const key = path[depth];
36858
+ if (key === void 0) return { reason: `${path.join(".")} is already in the config` };
36859
+ const nested = path.slice(depth + 1).reduceRight((inner, k) => ({ [String(k)]: inner }), value);
36860
+ if (typeof key === "number") {
36861
+ if (node.type !== "array") return { reason: `${path.slice(0, depth).join(".")} is not a list` };
36862
+ return { text: insertMember(text, node, (indent, column) => format(nested, indent, column)) };
36863
+ }
36864
+ if (node.type !== "object") return { reason: `${path.slice(0, depth).join(".")} is not an object` };
36865
+ const name = `${JSON.stringify(key)}: `;
36866
+ return { text: insertMember(text, node, (indent, column) => `${name}${format(nested, indent, column + name.length)}`) };
36867
+ };
36868
+ const removeAt = (text, path) => checked(text, removeText(text, path), changedAt(valueOf(text), path, (parent, key) => {
36869
+ if (Array.isArray(parent)) parent.splice(Number(key), 1);
36870
+ else delete parent[key];
36871
+ }), false);
36872
+ const removeText = (text, path) => {
36873
+ const found = findNodeAtLocation(parse$1(text), [...path]);
36874
+ if (found === void 0) return { reason: `${path.join(".")} is not in the config` };
36875
+ const member = memberOf(found);
36876
+ const siblings = member.parent?.children ?? [];
36877
+ const index = siblings.indexOf(member);
36878
+ const start = member.offset;
36879
+ const after = tail(text, start + member.length);
36880
+ if (text.slice(lineStart(text, start), start).trim() === "" && after.lineEnd !== void 0) {
36881
+ const from = lineStart(text, start);
36882
+ const to = text[after.lineEnd] === "\n" ? after.lineEnd + 1 : after.lineEnd;
36883
+ return { text: text.slice(0, from) + text.slice(to) };
36884
+ }
36885
+ if (after.comma && index < siblings.length - 1) {
36886
+ let to = after.end;
36887
+ while (text[to] === " ") to++;
36888
+ return { text: text.slice(0, start) + text.slice(to) };
36889
+ }
36890
+ const previous = siblings[index - 1];
36891
+ if (previous === void 0) {
36892
+ const parent = member.parent;
36893
+ if (parent === void 0) return { text };
36894
+ const kept = `${text.slice(parent.offset + 1, start)}${text.slice(after.end, parent.offset + parent.length - 1)}`;
36895
+ const empty = parent.type === "array" ? "[]" : "{}";
36896
+ return { text: kept.trim() === "" ? text.slice(0, parent.offset) + empty + text.slice(parent.offset + parent.length) : text.slice(0, start) + text.slice(after.end) };
36897
+ }
36898
+ const before = tail(text, previous.offset + previous.length);
36899
+ const comma = before.comma ? before.end - 1 : previous.offset + previous.length;
36900
+ return { text: text.slice(0, comma) + text.slice(before.comma ? before.end : comma, start).trimEnd() + text.slice(after.end) };
36901
+ };
36902
+ const targetingLists = (text, machine) => {
36903
+ const found = [];
36904
+ const visit = (node, path) => {
36905
+ if (node.type === "array") node.children?.forEach((child, i) => visit(child, [...path, i]));
36906
+ if (node.type !== "object") return;
36907
+ for (const [key, value] of (node.children ?? []).map((p) => p.children ?? [])) {
36908
+ if (key === void 0 || value === void 0) continue;
36909
+ const at = [...path, String(key.value)];
36910
+ if (key.value === "machines" && path.length > 0 && value.type === "array") {
36911
+ const index = (value.children ?? []).findIndex((c) => c.value === machine);
36912
+ if (index >= 0) found.push({
36913
+ path: at,
36914
+ size: value.children?.length ?? 0,
36915
+ index
36916
+ });
36917
+ } else visit(value, at);
36918
+ }
36919
+ };
36920
+ visit(parse$1(text), []);
36921
+ return found;
36922
+ };
36923
+ const saveConfig = fn("saveConfig")(function* (path, text) {
36924
+ const config = yield* parseConfig(path, text);
36925
+ const fs = yield* FileSystem;
36926
+ const target = (yield* fs.exists(path)) ? yield* fs.realPath(path) : path;
36927
+ yield* fs.makeDirectory((yield* Path$1).dirname(target), { recursive: true });
36928
+ yield* fs.writeFileString(`${target}.tmp`, text);
36929
+ if (yield* fs.exists(target)) yield* fs.chmod(`${target}.tmp`, (yield* fs.stat(target)).mode & 4095);
36930
+ yield* fs.rename(`${target}.tmp`, target);
36931
+ return config;
36932
+ });
36933
+ //#endregion
36934
+ //#region ../../packages/config/src/jsonSchema.ts
36935
+ const fleetConfigJsonSchema = () => {
36936
+ const document = toJsonSchemaDocument(FleetConfig, { onExcessProperty: "error" });
36937
+ return {
36938
+ $schema: META_SCHEMA_URI_DRAFT_2020_12,
36939
+ title: "fleetfizz config",
36940
+ ...document.schema,
36941
+ $defs: document.definitions
36942
+ };
36943
+ };
36944
+ const encodeJson = encodeSync(fromJsonString(Unknown, { space: 2 }));
36945
+ const fleetConfigSchemaText = () => `${encodeJson(fleetConfigJsonSchema())}\n`;
36521
36946
  const ConfigRecord = Struct({
36522
36947
  ship: NonEmptyString,
36523
36948
  sha: String$6,
@@ -36539,6 +36964,380 @@ const mergeReached = (all) => {
36539
36964
  const isNewer = (remote, local) => remote !== void 0 && (local === void 0 || remote > local);
36540
36965
  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;
36541
36966
  //#endregion
36967
+ //#region ../../packages/core/src/targeting.ts
36968
+ const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
36969
+ const isVariantList = (entry) => Array.isArray(entry);
36970
+ const variantFor = (entry, machine) => {
36971
+ if (!isVariantList(entry)) return targets(entry, machine) ? { variant: entry } : void 0;
36972
+ const index = entry.findIndex((variant) => targets(variant, machine));
36973
+ const variant = entry[index];
36974
+ return variant === void 0 ? void 0 : {
36975
+ variant,
36976
+ index
36977
+ };
36978
+ };
36979
+ //#endregion
36980
+ //#region ../../packages/core/src/engine.ts
36981
+ const dependenciesOf = (provider, config) => [.../* @__PURE__ */ new Set([...provider.dependsOn, ...config[provider.name]?.dependsOn ?? []])];
36982
+ const withoutTargeting = ({ platforms: _platforms, machines: _machines, ...def }) => def;
36983
+ const targetedItems = (provider, config, machine) => {
36984
+ const section = config[provider.name];
36985
+ if (section === void 0 || !targets(section, machine)) return provider.items(void 0, []);
36986
+ const listed = Object.entries(section.items ?? {}).flatMap(([name, entry]) => {
36987
+ const chosen = variantFor(entry, machine);
36988
+ if (chosen === void 0) return [];
36989
+ const at = chosen.index === void 0 ? [] : [chosen.index];
36990
+ return [{
36991
+ name,
36992
+ def: withoutTargeting(chosen.variant),
36993
+ path: [
36994
+ provider.name,
36995
+ "items",
36996
+ name,
36997
+ ...at
36998
+ ]
36999
+ }];
37000
+ });
37001
+ return provider.items(section, listed);
37002
+ };
37003
+ const machineNames = (value) => {
37004
+ if (Array.isArray(value)) return value.flatMap(machineNames);
37005
+ if (typeof value !== "object" || value === null) return [];
37006
+ return Object.entries(value).flatMap(([key, v]) => key === "machines" && Array.isArray(v) && v.every((name) => typeof name === "string") ? v : machineNames(v));
37007
+ };
37008
+ const machinesNamed = (provider, config) => machineNames(config[provider.name]);
37009
+ const hold = (decisions, by) => decisions.map((d) => d.kind === "change" || d.kind === "ask" ? {
37010
+ kind: "skip",
37011
+ item: d.item,
37012
+ ...d.kind === "change" && d.from !== void 0 ? { at: d.from } : {},
37013
+ ...d.kind === "ask" && d.at !== void 0 ? { at: d.at } : {},
37014
+ reason: `depends on ${by}`
37015
+ } : d);
37016
+ const planMachine = (input) => {
37017
+ const ctx = {
37018
+ ...input.ctx,
37019
+ home: input.state.home ?? "~"
37020
+ };
37021
+ const plans = /* @__PURE__ */ new Map();
37022
+ const held = /* @__PURE__ */ new Set();
37023
+ for (const provider of input.providers) {
37024
+ const dependsOn = dependenciesOf(provider, ctx.config);
37025
+ const items = targetedItems(provider, ctx.config, ctx.machine);
37026
+ const decisions = provider.decide({
37027
+ items,
37028
+ observed: provider.parse(input.state.facts[provider.name] ?? [], input.state.records[provider.name] ?? {}),
37029
+ resolved: input.resolved[provider.name],
37030
+ ctx
37031
+ });
37032
+ const by = dependsOn.find((name) => {
37033
+ const dep = plans.get(name);
37034
+ return dep !== void 0 && dep.decisions.length > 0 && (held.has(name) || dep.decisions.some((d) => d.kind === "fail"));
37035
+ });
37036
+ if (by !== void 0) held.add(provider.name);
37037
+ plans.set(provider.name, {
37038
+ name: provider.name,
37039
+ rule: provider.rule,
37040
+ dependsOn,
37041
+ decisions: by === void 0 ? decisions : hold(decisions, by),
37042
+ steps: by === void 0 ? provider.act(decisions, ctx) : []
37043
+ });
37044
+ }
37045
+ return {
37046
+ machine: ctx.machine,
37047
+ providers: [...plans.values()]
37048
+ };
37049
+ };
37050
+ //#endregion
37051
+ //#region ../../packages/core/src/range.ts
37052
+ const newest = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
37053
+ const decideRange = (range, installed, available) => {
37054
+ if (range === "latest") {
37055
+ const version = newest(available);
37056
+ if (version === void 0) return { kind: "unavailable" };
37057
+ return installed === version ? { kind: "leave" } : {
37058
+ kind: "install",
37059
+ version
37060
+ };
37061
+ }
37062
+ if (range === "*") {
37063
+ if (installed !== void 0) return { kind: "leave" };
37064
+ const version = newest(available);
37065
+ return version === void 0 ? { kind: "unavailable" } : {
37066
+ kind: "install",
37067
+ version
37068
+ };
37069
+ }
37070
+ if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
37071
+ if (installed !== void 0 && import_semver.gtr(installed, range)) return {
37072
+ kind: "ask",
37073
+ installed
37074
+ };
37075
+ const version = import_semver.maxSatisfying([...available], range);
37076
+ return version === null ? { kind: "unavailable" } : {
37077
+ kind: "install",
37078
+ version
37079
+ };
37080
+ };
37081
+ //#endregion
37082
+ //#region ../../packages/core/src/provider.ts
37083
+ const address = (provider, item) => `${provider}:${item}`;
37084
+ const parseAddress = (text) => {
37085
+ const at = text.indexOf(":");
37086
+ return at <= 0 || at === text.length - 1 ? void 0 : {
37087
+ provider: text.slice(0, at),
37088
+ item: text.slice(at + 1)
37089
+ };
37090
+ };
37091
+ const QUESTION_CHOICES = {
37092
+ range: [
37093
+ "bump",
37094
+ "skip",
37095
+ "downgrade"
37096
+ ],
37097
+ config: ["overwrite", "stop"],
37098
+ mode: ["managed", "skip"],
37099
+ file: ["overwrite", "skip"]
37100
+ };
37101
+ const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
37102
+ var Hidden = class {
37103
+ #value;
37104
+ constructor(value) {
37105
+ this.#value = value;
37106
+ }
37107
+ reveal() {
37108
+ return this.#value;
37109
+ }
37110
+ toJSON() {
37111
+ return "[hidden]";
37112
+ }
37113
+ toString() {
37114
+ return "[hidden]";
37115
+ }
37116
+ };
37117
+ const rangeQuestion = (subject, installed, range, bumpPath) => ({
37118
+ kind: "range",
37119
+ subject,
37120
+ found: installed,
37121
+ problem: `is above ${range}`,
37122
+ choices: [
37123
+ {
37124
+ value: "bump",
37125
+ title: "bump the config"
37126
+ },
37127
+ {
37128
+ value: "skip",
37129
+ title: "skip on these Machines (leave as is, shown as skipped)"
37130
+ },
37131
+ {
37132
+ value: "downgrade",
37133
+ title: "downgrade into the range"
37134
+ }
37135
+ ],
37136
+ default: "skip",
37137
+ bump: {
37138
+ path: bumpPath,
37139
+ range
37140
+ }
37141
+ });
37142
+ const decideRangeItem = (input) => {
37143
+ const { item, range, installed, available } = input;
37144
+ const at = installed === void 0 ? {} : { at: installed };
37145
+ const unavailable = {
37146
+ kind: "fail",
37147
+ item,
37148
+ ...at,
37149
+ reason: `no published version satisfies ${range}`
37150
+ };
37151
+ const install = (version) => ({
37152
+ kind: "change",
37153
+ item,
37154
+ ...installed === void 0 ? {} : { from: installed },
37155
+ to: version,
37156
+ change: version
37157
+ });
37158
+ const decision = decideRange(range, installed, available);
37159
+ switch (decision.kind) {
37160
+ case "leave": return {
37161
+ kind: "leave",
37162
+ item,
37163
+ ...at
37164
+ };
37165
+ case "install": return install(decision.version);
37166
+ case "unavailable": return unavailable;
37167
+ case "ask": {
37168
+ const subject = address(input.provider, item);
37169
+ const answer = input.ctx.answer("range", subject);
37170
+ if (answer === "skip") return {
37171
+ kind: "skip",
37172
+ item,
37173
+ ...at,
37174
+ reason: `is above ${range}`
37175
+ };
37176
+ if (answer === "downgrade") {
37177
+ const target = decideRange(range, void 0, available);
37178
+ return target.kind === "install" ? install(target.version) : unavailable;
37179
+ }
37180
+ return {
37181
+ kind: "ask",
37182
+ item,
37183
+ ...at,
37184
+ question: rangeQuestion(subject, decision.installed, range, input.bumpPath)
37185
+ };
37186
+ }
37187
+ }
37188
+ };
37189
+ const orderProviders = (all, dependsOn) => {
37190
+ const byName = new Map(all.map((p) => [p.name, p]));
37191
+ const order = [];
37192
+ const done = /* @__PURE__ */ new Set();
37193
+ const visit = (p, path) => {
37194
+ if (done.has(p.name)) return void 0;
37195
+ if (path.includes(p.name)) return [...path.slice(path.indexOf(p.name)), p.name];
37196
+ for (const name of dependsOn(p)) {
37197
+ const dep = byName.get(name);
37198
+ const cycle = dep === void 0 ? void 0 : visit(dep, [...path, p.name]);
37199
+ if (cycle !== void 0) return cycle;
37200
+ }
37201
+ done.add(p.name);
37202
+ order.push(p);
37203
+ };
37204
+ for (const p of all) {
37205
+ const cycle = visit(p, []);
37206
+ if (cycle !== void 0) return { cycle };
37207
+ }
37208
+ return { order };
37209
+ };
37210
+ //#endregion
37211
+ //#region ../../packages/core/src/record.ts
37212
+ const decideInstall = (record, now, rebuild) => {
37213
+ if (rebuild) return {
37214
+ run: true,
37215
+ reason: "--rebuild"
37216
+ };
37217
+ if (record === void 0) return {
37218
+ run: true,
37219
+ reason: "never installed"
37220
+ };
37221
+ if (record.commit !== now.commit) return {
37222
+ run: true,
37223
+ reason: `new commit ${now.commit.slice(0, 7)}`
37224
+ };
37225
+ if (record.path !== now.path) return {
37226
+ run: true,
37227
+ reason: "clone moved"
37228
+ };
37229
+ if (record.install !== now.install) return {
37230
+ run: true,
37231
+ reason: "install command changed"
37232
+ };
37233
+ for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
37234
+ run: true,
37235
+ reason: `drift: ${path}`
37236
+ };
37237
+ return { run: false };
37238
+ };
37239
+ //#endregion
37240
+ //#region ../../packages/core/src/state.ts
37241
+ const EngineFact = Union([
37242
+ Struct({
37243
+ fact: Literal("home"),
37244
+ path: String$6
37245
+ }),
37246
+ Struct({ fact: Literal("end") }),
37247
+ Struct({
37248
+ fact: Literal("config"),
37249
+ present: Boolean$4,
37250
+ sha: optionalKey(String$6),
37251
+ base64: optionalKey(String$6)
37252
+ }),
37253
+ Struct({
37254
+ fact: Literal("configRecord"),
37255
+ record: Unknown
37256
+ }),
37257
+ Struct({
37258
+ fact: Literal("record"),
37259
+ provider: String$6,
37260
+ item: String$6,
37261
+ record: Unknown
37262
+ })
37263
+ ]);
37264
+ const ProviderFact = Struct({ provider: String$6 });
37265
+ const AtFact = Struct({
37266
+ provider: String$6,
37267
+ fact: Literal("at"),
37268
+ item: String$6,
37269
+ at: String$6
37270
+ });
37271
+ const decodeEngineFact = decodeUnknownOption(EngineFact);
37272
+ const decodeProviderFact = decodeUnknownOption(ProviderFact);
37273
+ const emptyState = {
37274
+ complete: false,
37275
+ records: {},
37276
+ facts: {}
37277
+ };
37278
+ const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
37279
+ const assembleState = (facts) => {
37280
+ let state = emptyState;
37281
+ for (const raw of facts) {
37282
+ const fact = decodeEngineFact(raw);
37283
+ if (fact._tag === "Some") {
37284
+ const f = fact.value;
37285
+ switch (f.fact) {
37286
+ case "home":
37287
+ state = {
37288
+ ...state,
37289
+ home: f.path
37290
+ };
37291
+ break;
37292
+ case "end":
37293
+ state = {
37294
+ ...state,
37295
+ complete: true
37296
+ };
37297
+ break;
37298
+ case "config":
37299
+ if (f.present && f.base64 !== void 0 && f.sha !== void 0) state = {
37300
+ ...state,
37301
+ config: {
37302
+ text: decodeBase64(f.base64),
37303
+ sha: f.sha
37304
+ }
37305
+ };
37306
+ break;
37307
+ case "configRecord":
37308
+ state = {
37309
+ ...state,
37310
+ configRecord: f.record
37311
+ };
37312
+ break;
37313
+ case "record": state = {
37314
+ ...state,
37315
+ records: {
37316
+ ...state.records,
37317
+ [f.provider]: {
37318
+ ...state.records[f.provider],
37319
+ [f.item]: f.record
37320
+ }
37321
+ }
37322
+ };
37323
+ }
37324
+ continue;
37325
+ }
37326
+ const owned = decodeProviderFact(raw);
37327
+ if (owned._tag === "Some") {
37328
+ const { provider } = owned.value;
37329
+ state = {
37330
+ ...state,
37331
+ facts: {
37332
+ ...state.facts,
37333
+ [provider]: [...state.facts[provider] ?? [], raw]
37334
+ }
37335
+ };
37336
+ }
37337
+ }
37338
+ return state;
37339
+ };
37340
+ //#endregion
36542
37341
  //#region ../../packages/remote/src/shell.ts
36543
37342
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
36544
37343
  const quotePath = (path) => {
@@ -36897,113 +37696,6 @@ const resolveVersions = fn("resolveVersions")(function* (packages) {
36897
37696
  return Object.fromEntries(entries);
36898
37697
  });
36899
37698
  //#endregion
36900
- //#region src/configRecord.ts
36901
- const recordPath = () => CONFIG_RECORD.replace(/^~/, process.env.HOME ?? "~");
36902
- const readConfigRecord = gen(function* () {
36903
- const text = yield* (yield* FileSystem).readFileString(recordPath()).pipe(option);
36904
- return getOrUndefined$1(flatMap$3(text, decodeConfigRecordJson));
36905
- });
36906
- const writeConfigRecord = fn("writeConfigRecord")(function* (json) {
36907
- const fs = yield* FileSystem;
36908
- const path = recordPath();
36909
- yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
36910
- yield* fs.writeFileString(`${path}.tmp`, `${json}\n`);
36911
- yield* fs.rename(`${path}.tmp`, path);
36912
- });
36913
- var LogsError = class extends Error {
36914
- name = "LogsError";
36915
- };
36916
- const shipId = (started, from) => `${formatIso(started).slice(0, 19).replaceAll(":", "-")}Z-${from}`;
36917
- const parseShipId = (id) => {
36918
- const [, day, h, m, s, from] = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})Z-(.+)$/.exec(id) ?? [];
36919
- if (from === void 0) return void 0;
36920
- return {
36921
- started: Date.parse(`${day}T${h}:${m}:${s}Z`),
36922
- from
36923
- };
36924
- };
36925
- const lineFormat = (hidden) => make(({ message, logLevel, date, cause, fiber }) => {
36926
- const { machine, phase } = fiber.getRef(CurrentLogAnnotations);
36927
- const scope = [machine, phase].filter((s) => typeof s === "string").join(" ");
36928
- const prefix = `${date.toISOString()} ${logLevel.toUpperCase().padEnd(5)}${scope === "" ? "" : ` [${scope}]`}`;
36929
- const parts = (Array.isArray(message) ? message : [message]).map((m) => typeof m === "string" ? m : JSON.stringify(m));
36930
- if (cause.reasons.length > 0) parts.push(pretty(cause));
36931
- let text = parts.join(" ");
36932
- for (const secret of hidden) text = text.replaceAll(secret, "[hidden]");
36933
- return text.split("\n").map((line) => `${prefix} ${line}`).join("\n");
36934
- });
36935
- const machineOf = (line) => /^\S+ +\S+ +\[([^\] ]+)/.exec(line)?.[1];
36936
- const toPrune = (names) => {
36937
- const logs = names.filter((name) => name.endsWith(".log")).toSorted();
36938
- return logs.slice(0, Math.max(0, logs.length - 19));
36939
- };
36940
- const logsDir = () => LOGS_DIR.replace(/^~/, process.env.HOME ?? "~");
36941
- const openShipLog = fn("openShipLog")(function* (id, hidden) {
36942
- const fs = yield* FileSystem;
36943
- const dir = logsDir();
36944
- yield* fs.makeDirectory(dir, { recursive: true });
36945
- for (const name of toPrune(yield* fs.readDirectory(dir))) yield* fs.remove(`${dir}/${name}`);
36946
- return yield* toFile(lineFormat(hidden), `${dir}/${id}.log`);
36947
- });
36948
- const read = fn("read")(function* (target, script, onLine) {
36949
- const code = yield* runScript(target, script, [], onLine);
36950
- if (!target.local && code === 255) return yield* fail$1(new LogsError(`could not reach ${target.name} over tailscale ssh`));
36951
- });
36952
- const shipIds = fn("shipIds")(function* (target) {
36953
- const ids = [];
36954
- yield* read(target, `ls -1 ${quotePath(LOGS_DIR)} 2>/dev/null`, (line) => sync(() => {
36955
- if (line.endsWith(".log")) ids.push(line.slice(0, -4));
36956
- }));
36957
- return ids.toSorted();
36958
- });
36959
- const MISSING = "ff-missing";
36960
- const logs = fn("logs")(function* (flags) {
36961
- const here = {
36962
- name: "this Machine",
36963
- local: true
36964
- };
36965
- const local = yield* shipIds(here);
36966
- const self = parseShipId(local.at(-1) ?? "")?.from;
36967
- const tailnet = yield* cached(readTailnet());
36968
- const targetOf = fn("targetOf")(function* (name) {
36969
- if (name === self) return here;
36970
- const { self: me, fleet } = yield* tailnet;
36971
- if (name === me.name) return here;
36972
- const machine = fleet.find((m) => m.name === name);
36973
- if (machine === void 0) return yield* fail$1(new LogsError(`no Machine ${name} on the tailnet`));
36974
- if (!machine.online) return yield* fail$1(new LogsError(`${name} is offline; its logs are there`));
36975
- return {
36976
- name,
36977
- local: false
36978
- };
36979
- });
36980
- if (flags.list) {
36981
- if (local.length === 0) return yield* log("no Ship Log on this Machine yet");
36982
- for (const id of local.toReversed()) yield* log(id);
36983
- return;
36984
- }
36985
- const id = flags.id ?? (yield* latestShip(local, self));
36986
- const ship = parseShipId(id);
36987
- if (ship === void 0) return yield* fail$1(new LogsError(`not a Ship id: ${id} (fizz logs --list shows them)`));
36988
- let missing = false;
36989
- yield* read(local.includes(id) ? here : yield* targetOf(ship.from), `ff_log=${quotePath(LOGS_DIR)}/${quote(`${id}.log`)}\nif [ -f "$ff_log" ]; then cat "$ff_log"; else echo ${MISSING}; fi`, (line) => {
36990
- if (line === MISSING) return sync(() => void (missing = true));
36991
- if (flags.machine !== void 0 && machineOf(line) !== flags.machine) return void_;
36992
- return log(line);
36993
- });
36994
- if (missing) return yield* fail$1(new LogsError(`no log for ${id} on ${ship.from}; a Machine keeps its last 20`));
36995
- });
36996
- const latestShip = fn("latestShip")(function* (local, self) {
36997
- const newest = local.at(-1);
36998
- const record = yield* readConfigRecord;
36999
- const from = parseShipId(record?.ship ?? "")?.from;
37000
- const at = self === void 0 ? Infinity : Date.parse(record?.reached[self] ?? "");
37001
- const newestStarted = parseShipId(newest ?? "")?.started ?? -Infinity;
37002
- if (record !== void 0 && from !== void 0 && from !== self && at > newestStarted) return record.ship;
37003
- if (newest === void 0) return yield* fail$1(new LogsError("no Ship Log on this Machine yet"));
37004
- return newest;
37005
- });
37006
- //#endregion
37007
37699
  //#region ../../node_modules/.pnpm/react@19.3.0/node_modules/react/cjs/react.production.js
37008
37700
  /**
37009
37701
  * @license React
@@ -65125,679 +65817,54 @@ const useWindowSize = () => {
65125
65817
  return size;
65126
65818
  };
65127
65819
  //#endregion
65128
- //#region ../../packages/core/src/targeting.ts
65129
- const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
65130
- const isVariantList = (entry) => Array.isArray(entry);
65131
- const variantFor = (entry, machine) => {
65132
- if (!isVariantList(entry)) return targets(entry, machine) ? { variant: entry } : void 0;
65133
- const index = entry.findIndex((variant) => targets(variant, machine));
65134
- const variant = entry[index];
65135
- return variant === void 0 ? void 0 : {
65136
- variant,
65137
- index
65138
- };
65139
- };
65140
- //#endregion
65141
- //#region ../../packages/core/src/engine.ts
65142
- const dependenciesOf = (provider, config) => [.../* @__PURE__ */ new Set([...provider.dependsOn, ...config[provider.name]?.dependsOn ?? []])];
65143
- const withoutTargeting = ({ platforms: _platforms, machines: _machines, ...def }) => def;
65144
- const targetedItems = (provider, config, machine) => {
65145
- const section = config[provider.name];
65146
- if (section === void 0 || !targets(section, machine)) return provider.items(void 0, []);
65147
- const listed = Object.entries(section.items ?? {}).flatMap(([name, entry]) => {
65148
- const chosen = variantFor(entry, machine);
65149
- if (chosen === void 0) return [];
65150
- const at = chosen.index === void 0 ? [] : [chosen.index];
65151
- return [{
65152
- name,
65153
- def: withoutTargeting(chosen.variant),
65154
- path: [
65155
- provider.name,
65156
- "items",
65157
- name,
65158
- ...at
65159
- ]
65160
- }];
65161
- });
65162
- return provider.items(section, listed);
65163
- };
65164
- const machineNames = (value) => {
65165
- if (Array.isArray(value)) return value.flatMap(machineNames);
65166
- if (typeof value !== "object" || value === null) return [];
65167
- return Object.entries(value).flatMap(([key, v]) => key === "machines" && Array.isArray(v) && v.every((name) => typeof name === "string") ? v : machineNames(v));
65168
- };
65169
- const machinesNamed = (provider, config) => machineNames(config[provider.name]);
65170
- const hold = (decisions, by) => decisions.map((d) => d.kind === "change" || d.kind === "ask" ? {
65171
- kind: "skip",
65172
- item: d.item,
65173
- ...d.kind === "change" && d.from !== void 0 ? { at: d.from } : {},
65174
- ...d.kind === "ask" && d.at !== void 0 ? { at: d.at } : {},
65175
- reason: `depends on ${by}`
65176
- } : d);
65177
- const planMachine = (input) => {
65178
- const ctx = {
65179
- ...input.ctx,
65180
- home: input.state.home ?? "~"
65181
- };
65182
- const plans = /* @__PURE__ */ new Map();
65183
- const held = /* @__PURE__ */ new Set();
65184
- for (const provider of input.providers) {
65185
- const dependsOn = dependenciesOf(provider, ctx.config);
65186
- const items = targetedItems(provider, ctx.config, ctx.machine);
65187
- const decisions = provider.decide({
65188
- items,
65189
- observed: provider.parse(input.state.facts[provider.name] ?? [], input.state.records[provider.name] ?? {}),
65190
- resolved: input.resolved[provider.name],
65191
- ctx
65192
- });
65193
- const by = dependsOn.find((name) => {
65194
- const dep = plans.get(name);
65195
- return dep !== void 0 && dep.decisions.length > 0 && (held.has(name) || dep.decisions.some((d) => d.kind === "fail"));
65196
- });
65197
- if (by !== void 0) held.add(provider.name);
65198
- plans.set(provider.name, {
65199
- name: provider.name,
65200
- rule: provider.rule,
65201
- dependsOn,
65202
- decisions: by === void 0 ? decisions : hold(decisions, by),
65203
- steps: by === void 0 ? provider.act(decisions, ctx) : []
65204
- });
65205
- }
65206
- return {
65207
- machine: ctx.machine,
65208
- providers: [...plans.values()]
65209
- };
65210
- };
65211
- //#endregion
65212
- //#region ../../packages/core/src/range.ts
65213
- const newest = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
65214
- const decideRange = (range, installed, available) => {
65215
- if (range === "latest") {
65216
- const version = newest(available);
65217
- if (version === void 0) return { kind: "unavailable" };
65218
- return installed === version ? { kind: "leave" } : {
65219
- kind: "install",
65220
- version
65221
- };
65222
- }
65223
- if (range === "*") {
65224
- if (installed !== void 0) return { kind: "leave" };
65225
- const version = newest(available);
65226
- return version === void 0 ? { kind: "unavailable" } : {
65227
- kind: "install",
65228
- version
65229
- };
65230
- }
65231
- if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
65232
- if (installed !== void 0 && import_semver.gtr(installed, range)) return {
65233
- kind: "ask",
65234
- installed
65235
- };
65236
- const version = import_semver.maxSatisfying([...available], range);
65237
- return version === null ? { kind: "unavailable" } : {
65238
- kind: "install",
65239
- version
65240
- };
65241
- };
65242
- //#endregion
65243
- //#region ../../packages/core/src/provider.ts
65244
- const address = (provider, item) => `${provider}:${item}`;
65245
- const parseAddress = (text) => {
65246
- const at = text.indexOf(":");
65247
- return at <= 0 || at === text.length - 1 ? void 0 : {
65248
- provider: text.slice(0, at),
65249
- item: text.slice(at + 1)
65250
- };
65251
- };
65252
- const QUESTION_CHOICES = {
65253
- range: [
65254
- "bump",
65255
- "skip",
65256
- "downgrade"
65257
- ],
65258
- config: ["overwrite", "stop"],
65259
- mode: ["managed", "skip"],
65260
- file: ["overwrite", "skip"]
65261
- };
65262
- const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
65263
- var Hidden = class {
65264
- #value;
65265
- constructor(value) {
65266
- this.#value = value;
65267
- }
65268
- reveal() {
65269
- return this.#value;
65270
- }
65271
- toJSON() {
65272
- return "[hidden]";
65273
- }
65274
- toString() {
65275
- return "[hidden]";
65276
- }
65277
- };
65278
- const rangeQuestion = (subject, installed, range, bumpPath) => ({
65279
- kind: "range",
65280
- subject,
65281
- found: installed,
65282
- problem: `is above ${range}`,
65283
- choices: [
65284
- {
65285
- value: "bump",
65286
- title: "bump the config"
65287
- },
65288
- {
65289
- value: "skip",
65290
- title: "skip on these Machines (leave as is, shown as skipped)"
65291
- },
65292
- {
65293
- value: "downgrade",
65294
- title: "downgrade into the range"
65295
- }
65296
- ],
65297
- default: "skip",
65298
- bump: {
65299
- path: bumpPath,
65300
- range
65301
- }
65820
+ //#region src/configRecord.ts
65821
+ const recordPath = () => CONFIG_RECORD.replace(/^~/, process.env.HOME ?? "~");
65822
+ const readConfigRecord = gen(function* () {
65823
+ const text = yield* (yield* FileSystem).readFileString(recordPath()).pipe(option);
65824
+ return getOrUndefined$1(flatMap$3(text, decodeConfigRecordJson));
65825
+ });
65826
+ const writeConfigRecord = fn("writeConfigRecord")(function* (json) {
65827
+ const fs = yield* FileSystem;
65828
+ const path = recordPath();
65829
+ yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
65830
+ yield* fs.writeFileString(`${path}.tmp`, `${json}\n`);
65831
+ yield* fs.rename(`${path}.tmp`, path);
65302
65832
  });
65303
- const decideRangeItem = (input) => {
65304
- const { item, range, installed, available } = input;
65305
- const at = installed === void 0 ? {} : { at: installed };
65306
- const unavailable = {
65307
- kind: "fail",
65308
- item,
65309
- ...at,
65310
- reason: `no published version satisfies ${range}`
65311
- };
65312
- const install = (version) => ({
65313
- kind: "change",
65314
- item,
65315
- ...installed === void 0 ? {} : { from: installed },
65316
- to: version,
65317
- change: version
65318
- });
65319
- const decision = decideRange(range, installed, available);
65320
- switch (decision.kind) {
65321
- case "leave": return {
65322
- kind: "leave",
65323
- item,
65324
- ...at
65325
- };
65326
- case "install": return install(decision.version);
65327
- case "unavailable": return unavailable;
65328
- case "ask": {
65329
- const subject = address(input.provider, item);
65330
- const answer = input.ctx.answer("range", subject);
65331
- if (answer === "skip") return {
65332
- kind: "skip",
65333
- item,
65334
- ...at,
65335
- reason: `is above ${range}`
65336
- };
65337
- if (answer === "downgrade") {
65338
- const target = decideRange(range, void 0, available);
65339
- return target.kind === "install" ? install(target.version) : unavailable;
65340
- }
65341
- return {
65342
- kind: "ask",
65343
- item,
65344
- ...at,
65345
- question: rangeQuestion(subject, decision.installed, range, input.bumpPath)
65346
- };
65347
- }
65348
- }
65349
- };
65350
- const orderProviders = (all, dependsOn) => {
65351
- const byName = new Map(all.map((p) => [p.name, p]));
65352
- const order = [];
65353
- const done = /* @__PURE__ */ new Set();
65354
- const visit = (p, path) => {
65355
- if (done.has(p.name)) return void 0;
65356
- if (path.includes(p.name)) return [...path.slice(path.indexOf(p.name)), p.name];
65357
- for (const name of dependsOn(p)) {
65358
- const dep = byName.get(name);
65359
- const cycle = dep === void 0 ? void 0 : visit(dep, [...path, p.name]);
65360
- if (cycle !== void 0) return cycle;
65361
- }
65362
- done.add(p.name);
65363
- order.push(p);
65364
- };
65365
- for (const p of all) {
65366
- const cycle = visit(p, []);
65367
- if (cycle !== void 0) return { cycle };
65368
- }
65369
- return { order };
65370
- };
65371
- //#endregion
65372
- //#region ../../packages/core/src/record.ts
65373
- const decideInstall = (record, now, rebuild) => {
65374
- if (rebuild) return {
65375
- run: true,
65376
- reason: "--rebuild"
65377
- };
65378
- if (record === void 0) return {
65379
- run: true,
65380
- reason: "never installed"
65381
- };
65382
- if (record.commit !== now.commit) return {
65383
- run: true,
65384
- reason: `new commit ${now.commit.slice(0, 7)}`
65385
- };
65386
- if (record.path !== now.path) return {
65387
- run: true,
65388
- reason: "clone moved"
65389
- };
65390
- if (record.install !== now.install) return {
65391
- run: true,
65392
- reason: "install command changed"
65393
- };
65394
- for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
65395
- run: true,
65396
- reason: `drift: ${path}`
65397
- };
65398
- return { run: false };
65399
- };
65400
65833
  //#endregion
65401
- //#region ../../packages/core/src/state.ts
65402
- const EngineFact = Union([
65403
- Struct({
65404
- fact: Literal("home"),
65405
- path: String$6
65406
- }),
65407
- Struct({ fact: Literal("end") }),
65408
- Struct({
65409
- fact: Literal("config"),
65410
- present: Boolean$4,
65411
- sha: optionalKey(String$6),
65412
- base64: optionalKey(String$6)
65413
- }),
65414
- Struct({
65415
- fact: Literal("configRecord"),
65416
- record: Unknown
65417
- }),
65418
- Struct({
65419
- fact: Literal("record"),
65420
- provider: String$6,
65421
- item: String$6,
65422
- record: Unknown
65423
- })
65424
- ]);
65425
- const ProviderFact = Struct({ provider: String$6 });
65426
- const AtFact = Struct({
65427
- provider: String$6,
65428
- fact: Literal("at"),
65429
- item: String$6,
65430
- at: String$6
65834
+ //#region src/local.ts
65835
+ const runLocal = fn("runLocal")(function* (command, args, env) {
65836
+ const spawner = yield* ChildProcessSpawner;
65837
+ return yield* scoped(gen(function* () {
65838
+ const handle = yield* spawner.spawn(make$13(command, [...args], {
65839
+ stdin: "ignore",
65840
+ stdout: "pipe",
65841
+ stderr: "pipe",
65842
+ ...env === void 0 ? {} : { env: {
65843
+ ...process.env,
65844
+ ...env
65845
+ } }
65846
+ }));
65847
+ const [stdout, , code] = yield* all([
65848
+ runCollect(decodeText(handle.stdout)),
65849
+ runDrain(handle.stderr),
65850
+ handle.exitCode
65851
+ ], { concurrency: "unbounded" });
65852
+ return {
65853
+ code,
65854
+ stdout: stdout.join("")
65855
+ };
65856
+ })).pipe(orElseSucceed(() => ({
65857
+ code: 127,
65858
+ stdout: ""
65859
+ })));
65431
65860
  });
65432
- const decodeEngineFact = decodeUnknownOption(EngineFact);
65433
- const decodeProviderFact = decodeUnknownOption(ProviderFact);
65434
- const emptyState = {
65435
- complete: false,
65436
- records: {},
65437
- facts: {}
65438
- };
65439
- const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
65440
- const assembleState = (facts) => {
65441
- let state = emptyState;
65442
- for (const raw of facts) {
65443
- const fact = decodeEngineFact(raw);
65444
- if (fact._tag === "Some") {
65445
- const f = fact.value;
65446
- switch (f.fact) {
65447
- case "home":
65448
- state = {
65449
- ...state,
65450
- home: f.path
65451
- };
65452
- break;
65453
- case "end":
65454
- state = {
65455
- ...state,
65456
- complete: true
65457
- };
65458
- break;
65459
- case "config":
65460
- if (f.present && f.base64 !== void 0 && f.sha !== void 0) state = {
65461
- ...state,
65462
- config: {
65463
- text: decodeBase64(f.base64),
65464
- sha: f.sha
65465
- }
65466
- };
65467
- break;
65468
- case "configRecord":
65469
- state = {
65470
- ...state,
65471
- configRecord: f.record
65472
- };
65473
- break;
65474
- case "record": state = {
65475
- ...state,
65476
- records: {
65477
- ...state.records,
65478
- [f.provider]: {
65479
- ...state.records[f.provider],
65480
- [f.item]: f.record
65481
- }
65482
- }
65483
- };
65484
- }
65485
- continue;
65486
- }
65487
- const owned = decodeProviderFact(raw);
65488
- if (owned._tag === "Some") {
65489
- const { provider } = owned.value;
65490
- state = {
65491
- ...state,
65492
- facts: {
65493
- ...state.facts,
65494
- [provider]: [...state.facts[provider] ?? [], raw]
65495
- }
65496
- };
65497
- }
65498
- }
65499
- return state;
65500
- };
65501
- //#endregion
65502
- //#region src/ledger.ts
65503
- const Note = Struct({
65504
- kind: Literals$2([
65505
- "ask",
65506
- "answer",
65507
- "info"
65508
- ]),
65509
- text: String$6
65510
- });
65511
- const Item = Struct({
65512
- name: String$6,
65513
- kind: Literals$2([
65514
- "range",
65515
- "record",
65516
- "config"
65517
- ]),
65518
- from: optionalKey(String$6),
65519
- to: optionalKey(String$6),
65520
- reason: optionalKey(String$6),
65521
- changes: Boolean$4,
65522
- status: Literals$2([
65523
- "idle",
65524
- "running",
65525
- "ok",
65526
- "failed",
65527
- "skipped"
65528
- ]),
65529
- doing: optionalKey(String$6),
65530
- detail: optionalKey(String$6),
65531
- output: optionalKey(ArraySchema(String$6)),
65532
- steps: ArraySchema(Struct({
65533
- id: String$6,
65534
- doing: String$6
65535
- })),
65536
- started: optionalKey(Number$2),
65537
- ended: optionalKey(Number$2)
65538
- });
65539
- const MachineEntry = Struct({
65540
- name: String$6,
65541
- status: Literals$2([
65542
- "checking",
65543
- "waiting",
65544
- "planned",
65545
- "running",
65546
- "ok",
65547
- "skipped",
65548
- "failed"
65549
- ]),
65550
- detail: optionalKey(String$6),
65551
- output: optionalKey(ArraySchema(String$6)),
65552
- items: ArraySchema(Item),
65553
- connected: optionalKey(Boolean$4),
65554
- checked: optionalKey(Number$2),
65555
- started: optionalKey(Number$2),
65556
- ended: optionalKey(Number$2)
65557
- });
65558
- const LedgerState = Struct({
65559
- from: String$6,
65560
- dryRun: Boolean$4,
65561
- notes: ArraySchema(Note),
65562
- machines: ArraySchema(MachineEntry),
65563
- resolving: optionalKey(Struct({
65564
- started: Number$2,
65565
- items: Number$2
65566
- })),
65567
- started: Number$2,
65568
- ended: optionalKey(Number$2)
65569
- });
65570
- const itemsOf = (p, config) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
65571
- const base = {
65572
- name: address(pp.name, d.item),
65573
- kind: pp.rule,
65574
- steps: pp.steps.filter((step) => step.items.includes(d.item)).map(({ id, doing }) => ({
65575
- id,
65576
- doing
65577
- }))
65578
- };
65579
- switch (d.kind) {
65580
- case "change": return {
65581
- ...base,
65582
- ...d.from === void 0 ? {} : { from: d.from },
65583
- to: d.to,
65584
- ...d.reason === void 0 ? {} : { reason: d.reason },
65585
- changes: true,
65586
- status: "idle"
65587
- };
65588
- case "leave": return {
65589
- ...base,
65590
- ...d.at === void 0 ? {} : { from: d.at },
65591
- changes: false,
65592
- status: "idle"
65593
- };
65594
- case "skip":
65595
- case "fail": return {
65596
- ...base,
65597
- ...d.at === void 0 ? {} : { from: d.at },
65598
- changes: d.kind === "fail",
65599
- status: d.kind === "fail" ? "failed" : "skipped",
65600
- detail: d.reason
65601
- };
65602
- case "ask": return {
65603
- ...base,
65604
- ...d.at === void 0 ? {} : { from: d.at },
65605
- changes: false,
65606
- status: "skipped",
65607
- detail: d.question.problem
65608
- };
65609
- }
65610
- })), {
65611
- name: CONFIG_STEP,
65612
- kind: "config",
65613
- ...config,
65614
- changes: config.from !== config.to,
65615
- status: "idle",
65616
- steps: [{
65617
- id: CONFIG_STEP,
65618
- doing: "copying"
65619
- }]
65620
- }];
65621
- const outcomeOf = (m, dryRun) => m.status === "skipped" ? "skipped" : m.status === "failed" || m.items.some((i) => i.status === "failed") ? "failed" : m.status === "ok" || m.status === "planned" && dryRun ? "ok" : void 0;
65622
- const anyFailed = (state) => state.machines.some((m) => outcomeOf(m, state.dryRun) === "failed");
65623
- const makeLedger = fn("makeLedger")(function* (input) {
65624
- const started = yield* currentTimeMillis;
65625
- let snapshot = {
65626
- state: {
65627
- from: input.from,
65628
- dryRun: input.dryRun,
65629
- notes: [],
65630
- machines: input.machines.map((name) => ({
65631
- name,
65632
- status: "checking",
65633
- items: []
65634
- })),
65635
- started
65636
- },
65637
- now: started
65638
- };
65639
- const listeners = /* @__PURE__ */ new Set();
65640
- const update = (change) => gen(function* () {
65641
- const now = yield* currentTimeMillis;
65642
- snapshot = {
65643
- state: change(snapshot.state, now),
65644
- now
65645
- };
65646
- for (const listener of listeners) listener();
65647
- });
65648
- const machine = (name, change) => update((state, now) => ({
65649
- ...state,
65650
- machines: state.machines.map((m) => m.name === name ? change(m, now) : m)
65651
- }));
65652
- const items = (name, id, change) => machine(name, (m, now) => ({
65653
- ...m,
65654
- items: m.items.map((i) => i.steps.some((step) => step.id === id) ? change(i, now) : i)
65655
- }));
65656
- const store = {
65657
- get: () => snapshot,
65658
- subscribe: (listener) => {
65659
- listeners.add(listener);
65660
- return () => listeners.delete(listener);
65661
- },
65662
- tick: update((state) => state)
65663
- };
65664
- const log = (name, entry) => annotateLogs(entry, { machine: name });
65665
- return {
65666
- ledger: {
65667
- note: fn("ledger.note")(function* (kind, text) {
65668
- yield* update((state) => ({
65669
- ...state,
65670
- notes: [...state.notes, {
65671
- kind,
65672
- text
65673
- }]
65674
- }));
65675
- yield* input.print({
65676
- kind,
65677
- text
65678
- });
65679
- yield* logInfo(`${kind}: ${text}`);
65680
- }),
65681
- connected: (name) => machine(name, (m) => ({
65682
- ...m,
65683
- connected: true
65684
- })).pipe(andThen(log(name, logInfo("connected")))),
65685
- checked: (name) => machine(name, (m, now) => ({
65686
- ...m,
65687
- status: "waiting",
65688
- checked: now
65689
- })).pipe(andThen(log(name, logInfo("checked")))),
65690
- skip: (name, reason) => machine(name, (m) => ({
65691
- ...m,
65692
- status: "skipped",
65693
- detail: reason
65694
- })).pipe(andThen(log(name, logWarning(`skipped: ${reason}`)))),
65695
- fail: (name, [reason = "failed", ...output]) => machine(name, (m, now) => ({
65696
- ...m,
65697
- status: "failed",
65698
- detail: reason,
65699
- ...output.length === 0 ? {} : { output },
65700
- ended: now
65701
- })).pipe(andThen(log(name, logError(`failed: ${reason}`)))),
65702
- resolving: (n) => update((state, now) => ({
65703
- ...state,
65704
- resolving: {
65705
- started: now,
65706
- items: n
65707
- }
65708
- })).pipe(andThen(logInfo(`resolving ${n} items`))),
65709
- resolved: update(({ resolving: _, ...state }) => state).pipe(andThen(logInfo("resolved"))),
65710
- plan: (name, p, config) => machine(name, (m) => ({
65711
- ...m,
65712
- status: "planned",
65713
- items: itemsOf(p, config)
65714
- })).pipe(andThen(forEach$1(p.providers.flatMap((pp) => pp.decisions.map((d) => ({
65715
- pp,
65716
- d
65717
- }))), ({ pp, d }) => log(name, logInfo(`plan ${address(pp.name, d.item)}:`, d)), { discard: true }))),
65718
- start: (name) => machine(name, (m, now) => ({
65719
- ...m,
65720
- status: "running",
65721
- connected: false,
65722
- started: now
65723
- })).pipe(andThen(log(name, logInfo("applying")))),
65724
- stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" || item.status === "skipped" ? item : {
65725
- ...item,
65726
- status: "running",
65727
- doing: item.steps.find((step) => step.id === id)?.doing ?? "running",
65728
- started: item.started ?? now
65729
- }).pipe(andThen(log(name, logInfo(`step ${id}`)))),
65730
- stepDone: (name, id, result) => items(name, id, (item, now) => {
65731
- if (item.status === "failed" || item.status === "skipped") return item;
65732
- if (!result.ok) return {
65733
- ...item,
65734
- changes: true,
65735
- status: "failed",
65736
- detail: result.detail,
65737
- ...result.output.length === 0 ? {} : { output: result.output },
65738
- ended: now
65739
- };
65740
- if (item.steps.at(-1)?.id !== id) return item;
65741
- const { doing: _, ...rest } = item;
65742
- return {
65743
- ...rest,
65744
- status: "ok",
65745
- ended: now
65746
- };
65747
- }).pipe(andThen(log(name, result.ok ? logInfo(`step ${id}: done`) : logWarning(`step ${id}: ${result.detail}`)))),
65748
- stepSkipped: (name, id, reason) => items(name, id, (item, now) => {
65749
- if (item.status === "failed") return item;
65750
- const { doing: _, ...rest } = item;
65751
- return {
65752
- ...rest,
65753
- status: "skipped",
65754
- detail: reason,
65755
- ended: now
65756
- };
65757
- }).pipe(andThen(log(name, logInfo(`step ${id}: skipped, ${reason}`)))),
65758
- reached: (name, item, at) => machine(name, (m) => ({
65759
- ...m,
65760
- items: m.items.map((i) => i.name !== item ? i : {
65761
- ...i,
65762
- to: at,
65763
- changes: i.status === "failed" || i.from !== at
65764
- })
65765
- })).pipe(andThen(log(name, logInfo(`${item} reached ${at}`)))),
65766
- finish: (name, outcome) => machine(name, (m, now) => ({
65767
- ...m,
65768
- status: m.status === "failed" ? "failed" : outcome,
65769
- ended: m.ended ?? now
65770
- })).pipe(andThen(log(name, logInfo(`finished: ${outcome}`)))),
65771
- end: gen(function* () {
65772
- yield* update(({ resolving: _, ...state }, now) => ({
65773
- ...state,
65774
- ended: now,
65775
- machines: state.machines.map((m) => m.status === "running" || m.status === "checking" ? {
65776
- ...m,
65777
- status: "failed",
65778
- detail: "stopped",
65779
- ended: now,
65780
- items: m.items.map((i) => i.status === "running" ? {
65781
- ...i,
65782
- status: "failed",
65783
- detail: "stopped",
65784
- ended: now
65785
- } : i)
65786
- } : m)
65787
- }));
65788
- return snapshot.state;
65789
- })
65790
- },
65791
- store
65792
- };
65793
- });
65794
- const duration = (ms) => {
65795
- const s = ms / 1e3;
65796
- if (s < 60) return `${s.toFixed(1)}s`;
65797
- const m = Math.floor(s / 60);
65798
- if (m < 60) return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`;
65799
- return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
65861
+ const interactive = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
65862
+ const homeRelative = (path) => {
65863
+ const home = process.env.HOME;
65864
+ if (home === void 0 || home === "") return path;
65865
+ return path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
65800
65866
  };
65867
+ const expandHome$1 = (path) => path === "~" || path.startsWith("~/") ? `${process.env.HOME ?? "~"}${path.slice(1)}` : path;
65801
65868
  //#endregion
65802
65869
  //#region ../../node_modules/.pnpm/cli-spinners@3.4.0/node_modules/cli-spinners/spinners.json
65803
65870
  var spinners_default = {
@@ -67460,6 +67527,318 @@ var spinners_default = {
67460
67527
  var cli_spinners_default = spinners_default;
67461
67528
  Object.keys(spinners_default);
67462
67529
  //#endregion
67530
+ //#region src/ledger.ts
67531
+ const Note = Struct({
67532
+ kind: Literals$2([
67533
+ "ask",
67534
+ "answer",
67535
+ "info"
67536
+ ]),
67537
+ text: String$6
67538
+ });
67539
+ const Item = Struct({
67540
+ name: String$6,
67541
+ kind: Literals$2([
67542
+ "range",
67543
+ "record",
67544
+ "config"
67545
+ ]),
67546
+ from: optionalKey(String$6),
67547
+ to: optionalKey(String$6),
67548
+ reason: optionalKey(String$6),
67549
+ changes: Boolean$4,
67550
+ status: Literals$2([
67551
+ "idle",
67552
+ "running",
67553
+ "ok",
67554
+ "failed",
67555
+ "skipped"
67556
+ ]),
67557
+ doing: optionalKey(String$6),
67558
+ detail: optionalKey(String$6),
67559
+ asked: optionalKey(Boolean$4),
67560
+ output: optionalKey(ArraySchema(String$6)),
67561
+ steps: ArraySchema(Struct({
67562
+ id: String$6,
67563
+ doing: String$6
67564
+ })),
67565
+ started: optionalKey(Number$2),
67566
+ ended: optionalKey(Number$2)
67567
+ });
67568
+ const MachineEntry = Struct({
67569
+ name: String$6,
67570
+ status: Literals$2([
67571
+ "checking",
67572
+ "waiting",
67573
+ "planned",
67574
+ "running",
67575
+ "ok",
67576
+ "skipped",
67577
+ "failed"
67578
+ ]),
67579
+ detail: optionalKey(String$6),
67580
+ output: optionalKey(ArraySchema(String$6)),
67581
+ items: ArraySchema(Item),
67582
+ connected: optionalKey(Boolean$4),
67583
+ checked: optionalKey(Number$2),
67584
+ started: optionalKey(Number$2),
67585
+ ended: optionalKey(Number$2)
67586
+ });
67587
+ const LedgerState = Struct({
67588
+ from: String$6,
67589
+ dryRun: Boolean$4,
67590
+ notes: ArraySchema(Note),
67591
+ machines: ArraySchema(MachineEntry),
67592
+ resolving: optionalKey(Struct({
67593
+ started: Number$2,
67594
+ items: Number$2
67595
+ })),
67596
+ started: Number$2,
67597
+ ended: optionalKey(Number$2)
67598
+ });
67599
+ const itemsOf = (p, config) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
67600
+ const base = {
67601
+ name: address(pp.name, d.item),
67602
+ kind: pp.rule,
67603
+ steps: pp.steps.filter((step) => step.items.includes(d.item)).map(({ id, doing }) => ({
67604
+ id,
67605
+ doing
67606
+ }))
67607
+ };
67608
+ switch (d.kind) {
67609
+ case "change": return {
67610
+ ...base,
67611
+ ...d.from === void 0 ? {} : { from: d.from },
67612
+ to: d.to,
67613
+ ...d.reason === void 0 ? {} : { reason: d.reason },
67614
+ changes: true,
67615
+ status: "idle"
67616
+ };
67617
+ case "leave": return {
67618
+ ...base,
67619
+ ...d.at === void 0 ? {} : { from: d.at },
67620
+ changes: false,
67621
+ status: "idle"
67622
+ };
67623
+ case "skip":
67624
+ case "fail": return {
67625
+ ...base,
67626
+ ...d.at === void 0 ? {} : { from: d.at },
67627
+ changes: d.kind === "fail",
67628
+ status: d.kind === "fail" ? "failed" : "skipped",
67629
+ detail: d.reason
67630
+ };
67631
+ case "ask": return {
67632
+ ...base,
67633
+ ...d.at === void 0 ? {} : { from: d.at },
67634
+ changes: false,
67635
+ status: "skipped",
67636
+ detail: d.question.problem,
67637
+ asked: true
67638
+ };
67639
+ }
67640
+ })), (() => {
67641
+ const { asked, ...change } = config;
67642
+ return {
67643
+ name: CONFIG_STEP,
67644
+ kind: "config",
67645
+ ...change,
67646
+ ...asked === void 0 ? {
67647
+ changes: change.from !== change.to,
67648
+ status: "idle"
67649
+ } : {
67650
+ changes: false,
67651
+ status: "skipped",
67652
+ asked: true,
67653
+ detail: asked
67654
+ },
67655
+ steps: [{
67656
+ id: CONFIG_STEP,
67657
+ doing: "copying"
67658
+ }]
67659
+ };
67660
+ })()];
67661
+ const outcomeOf = (m, dryRun) => m.status === "skipped" ? "skipped" : m.status === "failed" || m.items.some((i) => i.status === "failed") ? "failed" : m.status === "ok" || m.status === "planned" && dryRun ? "ok" : void 0;
67662
+ const anyFailed = (state) => state.machines.some((m) => outcomeOf(m, state.dryRun) === "failed");
67663
+ const makeLedger = fn("makeLedger")(function* (input) {
67664
+ const started = yield* currentTimeMillis;
67665
+ let snapshot = {
67666
+ state: {
67667
+ from: input.from,
67668
+ dryRun: input.dryRun,
67669
+ notes: [],
67670
+ machines: input.machines.map((name) => ({
67671
+ name,
67672
+ status: "checking",
67673
+ items: []
67674
+ })),
67675
+ started
67676
+ },
67677
+ now: started
67678
+ };
67679
+ const listeners = /* @__PURE__ */ new Set();
67680
+ const update = (change) => gen(function* () {
67681
+ const now = yield* currentTimeMillis;
67682
+ snapshot = {
67683
+ state: change(snapshot.state, now),
67684
+ now
67685
+ };
67686
+ for (const listener of listeners) listener();
67687
+ });
67688
+ const machine = (name, change) => update((state, now) => ({
67689
+ ...state,
67690
+ machines: state.machines.map((m) => m.name === name ? change(m, now) : m)
67691
+ }));
67692
+ const items = (name, id, change) => machine(name, (m, now) => ({
67693
+ ...m,
67694
+ items: m.items.map((i) => i.steps.some((step) => step.id === id) ? change(i, now) : i)
67695
+ }));
67696
+ const store = {
67697
+ get: () => snapshot,
67698
+ subscribe: (listener) => {
67699
+ listeners.add(listener);
67700
+ return () => listeners.delete(listener);
67701
+ },
67702
+ tick: update((state) => state)
67703
+ };
67704
+ const log = (name, entry) => annotateLogs(entry, { machine: name });
67705
+ return {
67706
+ ledger: {
67707
+ note: fn("ledger.note")(function* (kind, text) {
67708
+ yield* update((state) => ({
67709
+ ...state,
67710
+ notes: [...state.notes, {
67711
+ kind,
67712
+ text
67713
+ }]
67714
+ }));
67715
+ yield* input.print({
67716
+ kind,
67717
+ text
67718
+ });
67719
+ yield* logInfo(`${kind}: ${text}`);
67720
+ }),
67721
+ connected: (name) => machine(name, (m) => ({
67722
+ ...m,
67723
+ connected: true
67724
+ })).pipe(andThen(log(name, logInfo("connected")))),
67725
+ checked: (name) => machine(name, (m, now) => ({
67726
+ ...m,
67727
+ status: "waiting",
67728
+ checked: now
67729
+ })).pipe(andThen(log(name, logInfo("checked")))),
67730
+ skip: (name, reason) => machine(name, (m) => ({
67731
+ ...m,
67732
+ status: "skipped",
67733
+ detail: reason
67734
+ })).pipe(andThen(log(name, logWarning(`skipped: ${reason}`)))),
67735
+ fail: (name, [reason = "failed", ...output]) => machine(name, (m, now) => ({
67736
+ ...m,
67737
+ status: "failed",
67738
+ detail: reason,
67739
+ ...output.length === 0 ? {} : { output },
67740
+ ended: now
67741
+ })).pipe(andThen(log(name, logError(`failed: ${reason}`)))),
67742
+ resolving: (n) => update((state, now) => ({
67743
+ ...state,
67744
+ resolving: {
67745
+ started: now,
67746
+ items: n
67747
+ }
67748
+ })).pipe(andThen(logInfo(`resolving ${n} items`))),
67749
+ resolved: update(({ resolving: _, ...state }) => state).pipe(andThen(logInfo("resolved"))),
67750
+ plan: (name, p, config) => machine(name, (m) => ({
67751
+ ...m,
67752
+ status: "planned",
67753
+ items: itemsOf(p, config)
67754
+ })).pipe(andThen(forEach$1(p.providers.flatMap((pp) => pp.decisions.map((d) => ({
67755
+ pp,
67756
+ d
67757
+ }))), ({ pp, d }) => log(name, logInfo(`plan ${address(pp.name, d.item)}:`, d)), { discard: true }))),
67758
+ start: (name) => machine(name, (m, now) => ({
67759
+ ...m,
67760
+ status: "running",
67761
+ connected: false,
67762
+ started: now
67763
+ })).pipe(andThen(log(name, logInfo("applying")))),
67764
+ stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" || item.status === "skipped" ? item : {
67765
+ ...item,
67766
+ status: "running",
67767
+ doing: item.steps.find((step) => step.id === id)?.doing ?? "running",
67768
+ started: item.started ?? now
67769
+ }).pipe(andThen(log(name, logInfo(`step ${id}`)))),
67770
+ stepDone: (name, id, result) => items(name, id, (item, now) => {
67771
+ if (item.status === "failed" || item.status === "skipped") return item;
67772
+ if (!result.ok) return {
67773
+ ...item,
67774
+ changes: true,
67775
+ status: "failed",
67776
+ detail: result.detail,
67777
+ ...result.output.length === 0 ? {} : { output: result.output },
67778
+ ended: now
67779
+ };
67780
+ if (item.steps.at(-1)?.id !== id) return item;
67781
+ const { doing: _, ...rest } = item;
67782
+ return {
67783
+ ...rest,
67784
+ status: "ok",
67785
+ ended: now
67786
+ };
67787
+ }).pipe(andThen(log(name, result.ok ? logInfo(`step ${id}: done`) : logWarning(`step ${id}: ${result.detail}`)))),
67788
+ stepSkipped: (name, id, reason) => items(name, id, (item, now) => {
67789
+ if (item.status === "failed") return item;
67790
+ const { doing: _, ...rest } = item;
67791
+ return {
67792
+ ...rest,
67793
+ status: "skipped",
67794
+ detail: reason,
67795
+ ended: now
67796
+ };
67797
+ }).pipe(andThen(log(name, logInfo(`step ${id}: skipped, ${reason}`)))),
67798
+ reached: (name, item, at) => machine(name, (m) => ({
67799
+ ...m,
67800
+ items: m.items.map((i) => i.name !== item ? i : {
67801
+ ...i,
67802
+ to: at,
67803
+ changes: i.status === "failed" || i.from !== at
67804
+ })
67805
+ })).pipe(andThen(log(name, logInfo(`${item} reached ${at}`)))),
67806
+ finish: (name, outcome) => machine(name, (m, now) => ({
67807
+ ...m,
67808
+ status: m.status === "failed" ? "failed" : outcome,
67809
+ ended: m.ended ?? now
67810
+ })).pipe(andThen(log(name, logInfo(`finished: ${outcome}`)))),
67811
+ end: gen(function* () {
67812
+ yield* update(({ resolving: _, ...state }, now) => ({
67813
+ ...state,
67814
+ ended: now,
67815
+ machines: state.machines.map((m) => m.status === "running" || m.status === "checking" ? {
67816
+ ...m,
67817
+ status: "failed",
67818
+ detail: "stopped",
67819
+ ended: now,
67820
+ items: m.items.map((i) => i.status === "running" ? {
67821
+ ...i,
67822
+ status: "failed",
67823
+ detail: "stopped",
67824
+ ended: now
67825
+ } : i)
67826
+ } : m)
67827
+ }));
67828
+ return snapshot.state;
67829
+ })
67830
+ },
67831
+ store
67832
+ };
67833
+ });
67834
+ const duration = (ms) => {
67835
+ const s = ms / 1e3;
67836
+ if (s < 60) return `${s.toFixed(1)}s`;
67837
+ const m = Math.floor(s / 60);
67838
+ if (m < 60) return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`;
67839
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
67840
+ };
67841
+ //#endregion
67463
67842
  //#region ../../node_modules/.pnpm/react@19.3.0/node_modules/react/cjs/react-jsx-runtime.production.js
67464
67843
  /**
67465
67844
  * @license React
@@ -67763,7 +68142,7 @@ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
67763
68142
  })
67764
68143
  })
67765
68144
  });
67766
- const value = (i) => i.to === void 0 ? i.from ?? "" : i.from === void 0 ? `+ ${i.to}` : i.from === i.to ? i.to : `${i.from} → ${i.to}`;
68145
+ const value$1 = (i) => i.to === void 0 ? i.from ?? "" : i.from === void 0 ? `+ ${i.to}` : i.from === i.to ? i.to : `${i.from} → ${i.to}`;
67767
68146
  const SPINNER = cli_spinners_default.dots;
67768
68147
  const runGlyph = (live, now) => (live ? SPINNER.frames[Math.floor(now / SPINNER.interval) % SPINNER.frames.length] : void 0) ?? "▸";
67769
68148
  const UNCHANGED = {
@@ -67829,7 +68208,7 @@ const upToDate = (m, dryRun) => {
67829
68208
  const c = countsOf(m);
67830
68209
  return c.changes === 0 && c.skipped === 0 && settled(m, dryRun);
67831
68210
  };
67832
- const summaryOf = (m, dryRun, glyph) => {
68211
+ const summaryOf$1 = (m, dryRun, glyph) => {
67833
68212
  if (m.status === "checking") return [{
67834
68213
  text: `${glyph} ${m.connected === true ? "checking" : "connecting"}`,
67835
68214
  tone: "run"
@@ -67955,7 +68334,7 @@ const MachineLine = (props) => {
67955
68334
  tone: "dim",
67956
68335
  children: upToDate(m, props.dryRun) ? "" : machineTime(m, props.shipStarted, props.now)
67957
68336
  }),
67958
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: summaryOf(m, props.dryRun, props.glyph) })
68337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: summaryOf$1(m, props.dryRun, props.glyph) })
67959
68338
  ]
67960
68339
  });
67961
68340
  };
@@ -67979,7 +68358,7 @@ const ItemLine = (props) => {
67979
68358
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
67980
68359
  width: w.value,
67981
68360
  tone: quiet,
67982
- children: value(i)
68361
+ children: value$1(i)
67983
68362
  }),
67984
68363
  w.time > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
67985
68364
  width: w.time,
@@ -68018,7 +68397,7 @@ const LedgerView = (props) => {
68018
68397
  const w = {
68019
68398
  status,
68020
68399
  item,
68021
- value: Math.max(width(visible.map(value)), width(state.machines.map((m) => m.name)) - (status + GAP + INDENT + item + GAP)),
68400
+ value: Math.max(width(visible.map(value$1)), width(state.machines.map((m) => m.name)) - (status + GAP + INDENT + item + GAP)),
68022
68401
  time: width([...visible.map((i) => elapsed(i.started, i.ended, now)), ...state.machines.map((m) => machineTime(m, state.started, now))])
68023
68402
  };
68024
68403
  const blocks = state.machines.map((m) => ({
@@ -68158,44 +68537,928 @@ const hintText = (text, color) => toText(/* @__PURE__ */ (0, import_jsx_runtime.
68158
68537
  children: text
68159
68538
  }), color);
68160
68539
  //#endregion
68161
- //#region src/report.tsx
68162
- const reportPath = () => `${STATE_DIR.replace(/^~/, process.env.HOME ?? "~")}/last-ship.json`;
68163
- const LedgerJson = fromJsonString(LedgerState);
68164
- const encode = encodeEffect(LedgerJson);
68165
- const decode = decodeEffect(LedgerJson);
68166
- const saveReport = fn("saveReport")(function* (state) {
68540
+ //#region src/configView.tsx
68541
+ const SHOWN = [
68542
+ "vp",
68543
+ "brew",
68544
+ "cask",
68545
+ "repos",
68546
+ "files"
68547
+ ];
68548
+ const whereOf = (targeting) => {
68549
+ const parts = [...targeting.platforms ?? [], ...targeting.machines ?? []];
68550
+ return parts.length === 0 ? void 0 : parts.join(", ");
68551
+ };
68552
+ const rowsOf$1 = (items, wants) => Object.entries(items ?? {}).flatMap(([name, entry]) => {
68553
+ const one = (e, variant) => {
68554
+ const where = whereOf(e);
68555
+ return {
68556
+ name,
68557
+ wants: wants(e),
68558
+ ...variant === void 0 ? {} : { variant },
68559
+ ...where === void 0 ? {} : { where }
68560
+ };
68561
+ };
68562
+ return Array.isArray(entry) ? entry.map((e, i) => one(e, i)) : [one(entry)];
68563
+ });
68564
+ const sectionsOf = (config) => SHOWN.flatMap((provider) => {
68565
+ const section = config[provider];
68566
+ if (section === void 0) return [];
68567
+ const where = whereOf(section);
68568
+ const rows = (() => {
68569
+ switch (provider) {
68570
+ case "vp": {
68571
+ const vp = config.vp ?? {};
68572
+ return [...[
68573
+ ["vp", vp.version],
68574
+ ["node", vp.node],
68575
+ ...PACKAGE_MANAGERS.map((pm) => [pm, vp[pm]])
68576
+ ].flatMap(([name, range]) => range === void 0 ? [] : [{
68577
+ name,
68578
+ wants: range
68579
+ }]), ...rowsOf$1(vp.items, (g) => g.range)];
68580
+ }
68581
+ case "brew": return rowsOf$1(config.brew?.items, (f) => f.range);
68582
+ case "cask": return rowsOf$1(config.cask?.items, (c) => c.range);
68583
+ case "repos": return rowsOf$1(config.repos?.items, (r) => isPin(r.ref) ? r.ref.slice(0, 7) : r.ref);
68584
+ case "files": return rowsOf$1(config.files?.items, (f) => f.path);
68585
+ }
68586
+ })();
68587
+ return [{
68588
+ provider,
68589
+ ...where === void 0 ? {} : { where },
68590
+ rows
68591
+ }];
68592
+ });
68593
+ const WANTS_WIDTH = 32;
68594
+ const shorten = (text, width) => text.length <= width ? text : `${text.slice(0, Math.ceil((width - 1) / 2))}…${text.slice(text.length - Math.floor((width - 1) / 2))}`;
68595
+ const pad = (text, width) => text.padEnd(width);
68596
+ const Sections = (props) => {
68597
+ const rows = props.sections.flatMap((s) => s.rows);
68598
+ const variants = rows.some((r) => r.variant !== void 0);
68599
+ const nameWidth = Math.max(0, ...rows.map((r) => r.name.length)) + 2;
68600
+ const wantsWidth = Math.min(WANTS_WIDTH, Math.max(0, ...rows.map((r) => r.wants.length))) + (variants ? 2 : 0) + 2;
68601
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: props.sections.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
68602
+ flexDirection: "column",
68603
+ marginTop: 1,
68604
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68605
+ tone: "bold",
68606
+ children: pad(s.provider, 2 + nameWidth + wantsWidth)
68607
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68608
+ tone: "skip",
68609
+ children: s.where ?? ""
68610
+ })] }), s.rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68611
+ tone: "dim",
68612
+ children: " no Items"
68613
+ }) }) : s.rows.map((r, i) => {
68614
+ const repeated = r.variant !== void 0 && r.variant > 0;
68615
+ const index = r.variant === void 0 ? variants ? " " : "" : `${r.variant} `;
68616
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
68617
+ ` ${pad(repeated ? "" : r.name, nameWidth)}`,
68618
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68619
+ tone: "ask",
68620
+ children: index
68621
+ }),
68622
+ pad(shorten(r.wants, WANTS_WIDTH), wantsWidth - (variants ? 2 : 0)),
68623
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68624
+ tone: "skip",
68625
+ children: r.where ?? ""
68626
+ })
68627
+ ] }, i);
68628
+ })]
68629
+ }, s.provider)) });
68630
+ };
68631
+ const Machines = (props) => {
68632
+ const width = Math.max(...props.config.machines.map((m) => m.length)) + 2;
68633
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
68634
+ flexDirection: "column",
68635
+ marginTop: 1,
68636
+ children: props.config.machines.map((name, i) => {
68637
+ const machine = props.tailnet.fleet.find((m) => m.name === name);
68638
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
68639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68640
+ tone: "bold",
68641
+ children: i === 0 ? "machines " : " "
68642
+ }),
68643
+ pad(name, width),
68644
+ machine === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68645
+ tone: "fail",
68646
+ children: "not on the tailnet"
68647
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [machine.platform, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68648
+ tone: "dim",
68649
+ children: [name === props.tailnet.self.name ? " this Machine" : "", machine.online ? "" : " offline"].join("")
68650
+ })] })
68651
+ ] }, name);
68652
+ })
68653
+ });
68654
+ };
68655
+ const ago = (ms) => {
68656
+ const minutes = Math.floor(ms / 6e4);
68657
+ if (minutes < 1) return "just now";
68658
+ if (minutes < 60) return `${minutes}m ago`;
68659
+ const hours = Math.floor(minutes / 60);
68660
+ return hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
68661
+ };
68662
+ const shippedLine = (record, machines, sha, now) => {
68663
+ const times = machines.map((m) => {
68664
+ const at = record?.reached[m];
68665
+ return {
68666
+ m,
68667
+ at: at === void 0 ? void 0 : Date.parse(at)
68668
+ };
68669
+ });
68670
+ const last = Math.max(...times.flatMap((t) => t.at === void 0 ? [] : [t.at]));
68671
+ if (record === void 0 || !Number.isFinite(last)) return "No Ship has reached it yet.";
68672
+ const missed = times.filter((t) => t.at !== last);
68673
+ const reached = machines.length - missed.length;
68674
+ const others = missed.map((t) => `${t.m} ${t.at === void 0 ? "never" : ago(now - t.at)}`).join(", ");
68675
+ return [`Last Ship ${ago(now - last)} reached ${reached} of ${machines.length} Machine${machines.length === 1 ? "" : "s"}${others === "" ? "" : `; ${others}`}.`, ...record.sha === sha ? [] : ["Edited since."]].join(" ");
68676
+ };
68677
+ const configText = (input) => toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
68678
+ flexDirection: "column",
68679
+ children: [
68680
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68681
+ tone: "dim",
68682
+ children: homeRelative(input.path)
68683
+ }),
68684
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Machines, {
68685
+ config: input.config,
68686
+ tailnet: input.tailnet
68687
+ }),
68688
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sections, { sections: sectionsOf(input.config) }),
68689
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
68690
+ flexDirection: "column",
68691
+ marginTop: 1,
68692
+ children: input.footer.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68693
+ tone: "dim",
68694
+ children: line
68695
+ }, i))
68696
+ })
68697
+ ]
68698
+ }));
68699
+ const showConfig = fn("showConfig")(function* (flags) {
68700
+ const path = flags.config ?? configPath(process.env);
68167
68701
  const fs = yield* FileSystem;
68168
- const path = reportPath();
68169
- yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
68170
- yield* fs.writeFileString(path, yield* encode(state));
68702
+ const config = yield* loadConfig(path);
68703
+ const tailnet = yield* readTailnet();
68704
+ const record = flags.config === void 0 ? yield* readConfigRecord : void 0;
68705
+ const sha = yield* shaOf(yield* fs.readFileString(path));
68706
+ const now$3 = toEpochMillis(yield* now);
68707
+ yield* log(configText({
68708
+ path,
68709
+ config,
68710
+ tailnet,
68711
+ footer: [shippedLine(record, config.machines, sha, now$3), "fizz status for where each Machine stands."]
68712
+ }));
68171
68713
  });
68172
- const report = fn("report")(function* () {
68714
+ const changedLines = (before, after, context = 3) => {
68715
+ const a = before.split("\n");
68716
+ const b = after.split("\n");
68717
+ let start = 0;
68718
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
68719
+ let end = 0;
68720
+ while (end < a.length - start && end < b.length - start && a[a.length - 1 - end] === b[b.length - 1 - end]) end++;
68721
+ const line = (sign) => (text) => ({
68722
+ sign,
68723
+ text
68724
+ });
68725
+ return [
68726
+ ...a.slice(Math.max(0, start - context), start).map(line(" ")),
68727
+ ...a.slice(start, a.length - end).map(line("-")),
68728
+ ...b.slice(start, b.length - end).map(line("+")),
68729
+ ...b.slice(b.length - end, b.length - end + 1).map(line(" "))
68730
+ ];
68731
+ };
68732
+ const changeText = (path, before, after, summary) => toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
68733
+ flexDirection: "column",
68734
+ children: [
68735
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68736
+ tone: "dim",
68737
+ children: homeRelative(path)
68738
+ }),
68739
+ changedLines(before, after).map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68740
+ tone: line.sign === "+" ? "ok" : line.sign === "-" ? "fail" : "dim",
68741
+ children: `${line.sign} ${line.text}`
68742
+ }, i)),
68743
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: summary })
68744
+ ]
68745
+ }));
68746
+ //#endregion
68747
+ //#region src/configAdd.tsx
68748
+ var ConfigAddError = class extends TaggedError()("ConfigAddError", { reason: String$6 }) {
68749
+ get message() {
68750
+ return this.reason;
68751
+ }
68752
+ };
68753
+ const targetingOf = (flags) => ({
68754
+ ...flags.platforms.length > 0 ? { platforms: [...new Set(flags.platforms)] } : {},
68755
+ ...flags.machines.length > 0 ? { machines: [...new Set(flags.machines)] } : {}
68756
+ });
68757
+ const untargeted = (targeting) => targeting.platforms === void 0 && targeting.machines === void 0;
68758
+ const rangeEntry = (range, targeting) => untargeted(targeting) ? range : {
68759
+ range,
68760
+ ...targeting
68761
+ };
68762
+ const fileEntry = (path, exclude, targeting) => untargeted(targeting) && exclude.length === 0 ? path : {
68763
+ path,
68764
+ ...exclude.length > 0 ? { exclude } : {},
68765
+ ...targeting
68766
+ };
68767
+ const parseSpec = (spec) => {
68768
+ const at = spec.lastIndexOf("@");
68769
+ return at > 0 ? {
68770
+ name: spec.slice(0, at),
68771
+ range: spec.slice(at + 1)
68772
+ } : { name: spec };
68773
+ };
68774
+ const fileName = (path) => (path.replace(/\/+$/, "").split("/").at(-1) ?? "").replace(/^\.+/, "").replace(/[^A-Za-z0-9._-]/g, "-");
68775
+ const openConfig = fn("openConfig")(function* (flag) {
68776
+ const path = flag ?? configPath(process.env);
68173
68777
  const fs = yield* FileSystem;
68174
- const path = reportPath();
68175
- if (!(yield* fs.exists(path))) return yield* log("no Ship from this Machine yet");
68176
- const decoded = yield* decode(yield* fs.readFileString(path)).pipe(option);
68177
- if (isNone(decoded)) return yield* log(`the last report (${path}) is from another version of fleetfizz; the next Ship replaces it`);
68178
- const state = decoded.value;
68179
- const when = formatLocal(makeUnsafe(state.started), {
68180
- dateStyle: "medium",
68181
- timeStyle: "short"
68778
+ if (!(yield* fs.exists(path))) return yield* new ConfigAddError({ reason: `no config at ${homeRelative(path)}; run fizz init to write one, or pass --config` });
68779
+ const text = yield* fs.readFileString(path);
68780
+ return {
68781
+ path,
68782
+ text,
68783
+ config: yield* parseConfig(path, text)
68784
+ };
68785
+ });
68786
+ const reach = (config, tailnet, provider, own) => {
68787
+ const section = config[provider] ?? {};
68788
+ const names = config.machines.filter((name) => {
68789
+ const machine = tailnet.fleet.find((m) => m.name === name);
68790
+ return machine !== void 0 && targets(section, machine) && targets(own, machine);
68182
68791
  });
68183
- yield* log(toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
68184
- flexDirection: "column",
68185
- children: [
68186
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68187
- tone: "dim",
68188
- children: `last Ship ${when} from ${state.from}`
68189
- }),
68190
- state.notes.map((note, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoteLine, { note }, i)),
68191
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LedgerView, {
68192
- state,
68193
- full: true,
68194
- now: state.ended ?? state.started
68195
- })
68196
- ]
68197
- })));
68792
+ return names.length === config.machines.length ? "every Machine" : names.length === 0 ? void 0 : names.join(", ");
68793
+ };
68794
+ const addItem = fn("addItem")(function* (input) {
68795
+ const { path, text, config } = yield* openConfig(input.config);
68796
+ const where = address(input.provider, input.name);
68797
+ if (config[input.provider]?.items?.[input.name] !== void 0) return yield* new ConfigAddError({ reason: `${where} is already in the config; change it with fizz config edit` });
68798
+ const unknown = (input.targeting.machines ?? []).filter((m) => !config.machines.includes(m));
68799
+ if (unknown.length > 0) return yield* new ConfigAddError({ reason: `not in machines: ${unknown.join(", ")} (add it with fizz config add machines)` });
68800
+ const tailnet = yield* readTailnet();
68801
+ const linux = config.machines.some((m) => tailnet.fleet.find((f) => f.name === m)?.platform === "linux");
68802
+ const start = input.provider === "cask" && linux && config.cask?.platforms === void 0 ? insertAt(text, ["cask", "platforms"], ["darwin"]) : { text };
68803
+ if ("reason" in start) return yield* new ConfigAddError({ reason: start.reason });
68804
+ const at = [
68805
+ input.provider,
68806
+ "items",
68807
+ input.name
68808
+ ];
68809
+ const result = insertAt(start.text, at, input.entry);
68810
+ if ("reason" in result) return yield* new ConfigAddError({ reason: result.reason });
68811
+ const saved = yield* saveConfig(path, result.text);
68812
+ const to = reach(saved, tailnet, input.provider, input.targeting);
68813
+ yield* log(changeText(path, text, result.text, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
68814
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68815
+ tone: "ok",
68816
+ children: "added"
68817
+ }),
68818
+ ` ${where}`,
68819
+ to === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68820
+ tone: "skip",
68821
+ children: " · no Machine in the Fleet matches its Targeting yet"
68822
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [` for ${to}`, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68823
+ tone: "dim",
68824
+ children: " · fizz ship to install it"
68825
+ })] })
68826
+ ] })));
68827
+ for (const note of input.notes ?? []) yield* log(note);
68828
+ });
68829
+ const needs = (what) => new ConfigAddError({ reason: `${what} is needed (no terminal to ask at)` });
68830
+ const addGlobal = fn("addGlobal")(function* (input) {
68831
+ const { name, range: given } = parseSpec(input.spec);
68832
+ const range = given ?? (yield* gen(function* () {
68833
+ const listed = yield* runLocal("vp", [
68834
+ "list",
68835
+ "--global",
68836
+ "--json"
68837
+ ]);
68838
+ const installed = decodeOption(fromJsonString(ArraySchema(Struct({
68839
+ name: String$6,
68840
+ version: String$6
68841
+ }))))(listed.stdout);
68842
+ const here = installed._tag === "Some" ? installed.value.find((g) => g.name === name)?.version : void 0;
68843
+ const published = here === void 0 ? (yield* resolveVersions([name]))[name] ?? [] : [];
68844
+ const range = addedRange(here, published);
68845
+ if (range === void 0) return yield* new ConfigAddError({ reason: `${name} is not installed here and npm has no version of it; check the name, or give a Range (${name}@^1.0.0)` });
68846
+ return range;
68847
+ }));
68848
+ const targeting = targetingOf(input.targeting);
68849
+ yield* addItem({
68850
+ config: input.config,
68851
+ provider: "vp",
68852
+ name,
68853
+ entry: rangeEntry(range, targeting),
68854
+ targeting
68855
+ });
68856
+ });
68857
+ const addHomebrew = fn("addHomebrew")(function* (input) {
68858
+ const noun = input.provider === "brew" ? "Formula" : "Cask";
68859
+ if (input.provider === "cask" && input.targeting.platforms.includes("linux")) return yield* new ConfigAddError({ reason: "a Cask is macOS only; leave out --platform linux" });
68860
+ const notes = [];
68861
+ if (input.name.includes("/")) notes.push(`${input.name} is a tapped name, so it was not checked`);
68862
+ else {
68863
+ const info = yield* runLocal("brew", [
68864
+ "info",
68865
+ input.provider === "brew" ? "--formula" : "--cask",
68866
+ input.name
68867
+ ]);
68868
+ if (info.code === 127) notes.push(`Homebrew is not installed here, so ${input.name} was not checked`);
68869
+ else if (info.code !== 0) return yield* new ConfigAddError({ reason: `Homebrew has no ${noun} named ${input.name}` });
68870
+ }
68871
+ const targeting = targetingOf(input.targeting);
68872
+ yield* addItem({
68873
+ config: input.config,
68874
+ provider: input.provider,
68875
+ name: input.name,
68876
+ entry: rangeEntry(input.latest ? "latest" : "*", targeting),
68877
+ targeting,
68878
+ notes
68879
+ });
68880
+ });
68881
+ const defaultBranch = fn("defaultBranch")(function* (origin) {
68882
+ const remote = yield* runLocal("git", [
68883
+ "ls-remote",
68884
+ "--symref",
68885
+ `https://github.com/${origin}`,
68886
+ "HEAD"
68887
+ ], { GIT_TERMINAL_PROMPT: "0" });
68888
+ const head = /^ref: refs\/heads\/(\S+)\s+HEAD/m.exec(remote.stdout)?.[1];
68889
+ if (head !== void 0) return head;
68890
+ const gh = yield* runLocal("gh", [
68891
+ "repo",
68892
+ "view",
68893
+ origin,
68894
+ "--json",
68895
+ "defaultBranchRef",
68896
+ "--jq",
68897
+ ".defaultBranchRef.name"
68898
+ ]);
68899
+ return gh.code === 0 && gh.stdout.trim() !== "" ? gh.stdout.trim() : void 0;
68900
+ });
68901
+ const addRepo = fn("addRepo")(function* (input) {
68902
+ const name = input.name ?? input.origin.split("/").at(-1) ?? input.origin;
68903
+ const ask = interactive();
68904
+ const found = input.ref === void 0 ? yield* defaultBranch(input.origin) : void 0;
68905
+ const ref = input.ref ?? (ask ? yield* String$4({
68906
+ message: "Ref",
68907
+ ...found === void 0 ? {} : { default: found },
68908
+ validate: (v) => v.trim() === "" ? fail$1("a Ref is needed") : succeed$3(v.trim())
68909
+ }) : found ?? (yield* new ConfigAddError({ reason: `could not find ${input.origin}'s default branch; pass --ref` })));
68910
+ const install = input.install ?? (ask ? yield* String$4({
68911
+ message: "Install Command (runs in the Managed Clone)",
68912
+ validate: (v) => v.trim() === "" ? fail$1("an Install Command is needed") : succeed$3(v.trim())
68913
+ }) : yield* needs("--install"));
68914
+ const outputs = (input.outputs.length > 0 || input.install !== void 0 || !ask ? input.outputs : (yield* String$4({
68915
+ message: "Outputs (paths outside the clone it produces, comma-separated, optional)",
68916
+ default: ""
68917
+ })).split(",").map((o) => o.trim()).filter((o) => o !== "")).map(homeRelative);
68918
+ const targeting = targetingOf(input.targeting);
68919
+ yield* addItem({
68920
+ config: input.config,
68921
+ provider: "repos",
68922
+ name,
68923
+ entry: {
68924
+ origin: input.origin,
68925
+ ref,
68926
+ install,
68927
+ ...outputs.length > 0 ? { outputs } : {},
68928
+ ...targeting
68929
+ },
68930
+ targeting
68931
+ });
68932
+ });
68933
+ const addFile = fn("addFile")(function* (input) {
68934
+ const fs = yield* FileSystem;
68935
+ const local = (yield* Path$1).resolve(expandHome$1(input.path));
68936
+ if (!(yield* fs.exists(local))) return yield* new ConfigAddError({ reason: `${input.path} does not exist here; a Ship copies it from this Machine` });
68937
+ const path = homeRelative(local);
68938
+ const name = input.name ?? fileName(path);
68939
+ const targeting = targetingOf(input.targeting);
68940
+ yield* addItem({
68941
+ config: input.config,
68942
+ provider: "files",
68943
+ name,
68944
+ entry: fileEntry(path, input.exclude, targeting),
68945
+ targeting
68946
+ });
68947
+ });
68948
+ const addMachines = fn("addMachines")(function* (input) {
68949
+ const { path, text, config } = yield* openConfig(input.config);
68950
+ const tailnet = yield* readTailnet();
68951
+ const listed = new Set(config.machines);
68952
+ const candidates = tailnet.fleet.filter((m) => !listed.has(m.name));
68953
+ const names = input.names.length > 0 ? input.names : !interactive() ? yield* needs("a Machine name") : candidates.length === 0 ? yield* new ConfigAddError({ reason: "every macOS and linux Machine on the tailnet is already listed" }) : yield* MultiSelect({
68954
+ message: "Which Machines?",
68955
+ choices: candidates.map((m) => {
68956
+ const note = machineNote(m, tailnet);
68957
+ return {
68958
+ title: `${m.name} ${m.platform}`,
68959
+ value: m.name,
68960
+ ...note === void 0 ? {} : { description: note }
68961
+ };
68962
+ }),
68963
+ min: 1
68964
+ });
68965
+ const again = names.filter((n) => listed.has(n));
68966
+ if (again.length > 0) return yield* new ConfigAddError({ reason: `already in machines: ${again.join(", ")}` });
68967
+ const unknown = names.filter((n) => !tailnet.fleet.some((m) => m.name === n));
68968
+ if (unknown.length > 0) return yield* new ConfigAddError({ reason: `not a macOS or linux Machine on the tailnet: ${unknown.join(", ")} (it has: ${tailnet.fleet.map((m) => m.name).join(", ")})` });
68969
+ let next = text;
68970
+ for (const [i, name] of names.entries()) {
68971
+ const result = insertAt(next, ["machines", config.machines.length + i], name);
68972
+ if ("reason" in result) return yield* new ConfigAddError({ reason: result.reason });
68973
+ next = result.text;
68974
+ }
68975
+ yield* saveConfig(path, next);
68976
+ const notes = names.flatMap((n) => {
68977
+ const m = tailnet.fleet.find((f) => f.name === n);
68978
+ const note = m === void 0 ? void 0 : machineNote(m, tailnet);
68979
+ return note === void 0 ? [] : [`${n}: ${note}`];
68980
+ });
68981
+ yield* log(changeText(path, text, next, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
68982
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68983
+ tone: "ok",
68984
+ children: "added"
68985
+ }),
68986
+ ` ${names.join(", ")} to the Fleet`,
68987
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
68988
+ tone: "dim",
68989
+ children: " · fizz ship to set it up"
68990
+ })
68991
+ ] })));
68992
+ for (const note of notes) yield* log(note);
68993
+ });
68994
+ const machineNote = (m, tailnet) => m.name === tailnet.self.name ? void 0 : !m.online ? "offline; a Ship skips it until it is online" : !m.ssh ? "Tailscale SSH is off there: run sudo tailscale set --ssh on it" : void 0;
68995
+ //#endregion
68996
+ //#region src/configEdit.tsx
68997
+ const pathOf = (provider, item) => {
68998
+ if (provider === "vp" && item === "vp") return ["vp", "version"];
68999
+ if (provider === "vp" && (item === "node" || PACKAGE_MANAGERS.some((pm) => pm === item))) return ["vp", item];
69000
+ return PROVIDER_NAMES.some((p) => p === provider) ? [
69001
+ provider,
69002
+ "items",
69003
+ item
69004
+ ] : void 0;
69005
+ };
69006
+ const owner = (path) => path.slice(0, -1).join(".");
69007
+ const removeMachine = fn("removeMachine")(function* (flag, name) {
69008
+ const { path, text, config } = yield* openConfig(flag);
69009
+ const index = config.machines.indexOf(name);
69010
+ if (index < 0) return yield* new ConfigAddError({ reason: `${name} is not in machines` });
69011
+ const lists = targetingLists(text, name);
69012
+ const only = lists.filter((l) => l.size === 1);
69013
+ if (only.length > 0) return yield* new ConfigAddError({ reason: `${name} is the only Machine ${only.map((l) => owner(l.path)).join(", ")} targets; change ${only.length === 1 ? "it" : "them"} with fizz config edit first` });
69014
+ let next = text;
69015
+ for (const at of [...lists.map((l) => [...l.path, l.index]), ["machines", index]].toReversed()) {
69016
+ const result = removeAt(next, at);
69017
+ if ("reason" in result) return yield* new ConfigAddError({ reason: result.reason });
69018
+ next = result.text;
69019
+ }
69020
+ yield* saveConfig(path, next);
69021
+ yield* log(changeText(path, text, next, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
69022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
69023
+ tone: "ok",
69024
+ children: "removed"
69025
+ }),
69026
+ ` ${name} from the Fleet`,
69027
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
69028
+ tone: "dim",
69029
+ children: " · a Ship no longer checks or changes it"
69030
+ })
69031
+ ] })));
69032
+ });
69033
+ const removeItem = fn("removeItem")(function* (input) {
69034
+ const at = parseAddress(input.address);
69035
+ if (at === void 0) return yield* new ConfigAddError({ reason: `${input.address}: expected provider:item, as the Ledger writes it (brew:tmux)` });
69036
+ if (at.provider === "machines") return yield* removeMachine(input.config, at.item);
69037
+ const path = pathOf(at.provider, at.item);
69038
+ if (path === void 0) return yield* new ConfigAddError({ reason: `${at.provider}: expected machines or a Provider (${PROVIDER_NAMES.join(", ")})` });
69039
+ const { path: file, text } = yield* openConfig(input.config);
69040
+ const result = removeAt(text, path);
69041
+ if ("reason" in result) return yield* new ConfigAddError({ reason: `${input.address} is not in the config` });
69042
+ yield* saveConfig(file, result.text);
69043
+ yield* log(changeText(file, text, result.text, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
69044
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
69045
+ tone: "ok",
69046
+ children: "removed"
69047
+ }),
69048
+ ` ${input.address}`,
69049
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
69050
+ tone: "dim",
69051
+ children: " · a Ship never uninstalls it, so it stays where it is"
69052
+ })
69053
+ ] })));
69054
+ });
69055
+ const runEditor = fn("runEditor")(function* (file) {
69056
+ const spawner = yield* ChildProcessSpawner;
69057
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
69058
+ return yield* scoped(gen(function* () {
69059
+ return yield* (yield* spawner.spawn(make$13("/bin/sh", [
69060
+ "-c",
69061
+ `${editor} "$1"`,
69062
+ "fizz",
69063
+ file
69064
+ ], {
69065
+ stdin: "inherit",
69066
+ stdout: "inherit",
69067
+ stderr: "inherit"
69068
+ }))).exitCode;
69069
+ }));
69070
+ });
69071
+ const editConfig = fn("editConfig")(function* (flags) {
69072
+ if (!interactive()) return yield* new ConfigAddError({ reason: "fizz config edit needs a terminal" });
69073
+ const { path, text } = yield* openConfig(flags.config);
69074
+ const fs = yield* FileSystem;
69075
+ const dir = yield* fs.makeTempDirectoryScoped();
69076
+ const copy = `${dir}/${path.split("/").at(-1) ?? "config.jsonc"}`;
69077
+ yield* fs.writeFileString(copy, text);
69078
+ const paths = yield* Path$1;
69079
+ const schema = paths.join(paths.dirname(path), SCHEMA_FILE);
69080
+ if (yield* fs.exists(schema)) yield* fs.copyFile(schema, `${dir}/${SCHEMA_FILE}`);
69081
+ for (;;) {
69082
+ const code = yield* runEditor(copy);
69083
+ if (code !== 0) return yield* log(`the editor exited with ${code}; the config is unchanged`);
69084
+ const edited = yield* fs.readFileString(copy);
69085
+ if (edited === text) return yield* log("no changes");
69086
+ const parsed = yield* result(parseConfig(homeRelative(path), edited));
69087
+ if (parsed._tag === "Success") {
69088
+ if ((yield* fs.readFileString(path)) !== text) {
69089
+ yield* log(edited);
69090
+ return yield* new ConfigAddError({ reason: `${homeRelative(path)} changed while the editor was open, so it is unchanged; your edit is printed above` });
69091
+ }
69092
+ yield* saveConfig(path, edited);
69093
+ return yield* log(`saved ${homeRelative(path)} · fizz ship to ship it`);
69094
+ }
69095
+ yield* error(parsed.failure.message);
69096
+ if (!(yield* Select({
69097
+ message: "The config does not load",
69098
+ choices: [{
69099
+ title: "Edit it again",
69100
+ value: true
69101
+ }, {
69102
+ title: "Discard the changes",
69103
+ value: false
69104
+ }]
69105
+ }))) return yield* log("discarded; the config is unchanged");
69106
+ }
69107
+ }, scoped);
69108
+ const printPath = (flags) => log(flags.config ?? configPath(process.env));
69109
+ //#endregion
69110
+ //#region src/prerequisites.ts
69111
+ const SERVICE_MACOS = "brew install --formula tailscale && sudo brew services start tailscale && sudo tailscale up && sudo tailscale set --ssh";
69112
+ const checksOf = (facts) => {
69113
+ const darwin = facts.platform === "darwin";
69114
+ const vp = facts.vp === void 0 ? {
69115
+ ok: false,
69116
+ required: true,
69117
+ text: "vp is not installed",
69118
+ fix: "curl -fsSL https://vite.plus | bash"
69119
+ } : {
69120
+ ok: true,
69121
+ required: true,
69122
+ text: `vp ${facts.vp}`
69123
+ };
69124
+ const ts = facts.tailscale;
69125
+ const tailscale = ts === void 0 ? [{
69126
+ ok: false,
69127
+ required: true,
69128
+ text: "Tailscale is not installed",
69129
+ fix: darwin ? SERVICE_MACOS : "curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up && sudo tailscale set --ssh"
69130
+ }] : ts.app && !ts.ssh ? [{
69131
+ ok: false,
69132
+ required: true,
69133
+ text: "Tailscale is the macOS app, which cannot accept Tailscale SSH. Quit and remove it, then use Homebrew's tailscaled",
69134
+ fix: SERVICE_MACOS
69135
+ }] : ts.state === void 0 ? [{
69136
+ ok: false,
69137
+ required: true,
69138
+ text: "Tailscale is installed, but tailscaled is not running",
69139
+ fix: darwin ? "sudo brew services start tailscale && sudo tailscale up" : "sudo systemctl enable --now tailscaled && sudo tailscale up"
69140
+ }] : ts.state !== "Running" ? [{
69141
+ ok: false,
69142
+ required: true,
69143
+ text: `Tailscale is not connected (${ts.state})`,
69144
+ fix: "sudo tailscale up"
69145
+ }] : [{
69146
+ ok: true,
69147
+ required: true,
69148
+ text: "Tailscale, connected"
69149
+ }, ts.ssh ? {
69150
+ ok: true,
69151
+ required: true,
69152
+ text: "Tailscale SSH is on"
69153
+ } : {
69154
+ ok: false,
69155
+ required: true,
69156
+ text: "Tailscale SSH is off",
69157
+ fix: "sudo tailscale set --ssh"
69158
+ }];
69159
+ const brewForTailscale = darwin && (ts === void 0 || ts.app && !ts.ssh);
69160
+ const brew = !darwin ? [] : facts.brew ? [{
69161
+ ok: true,
69162
+ required: false,
69163
+ text: "Homebrew"
69164
+ }] : [{
69165
+ ok: false,
69166
+ required: brewForTailscale,
69167
+ text: brewForTailscale ? "Homebrew is not installed (Tailscale's fix needs it)" : "Homebrew is not installed (only brew and cask Items need it)",
69168
+ fix: "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
69169
+ }];
69170
+ const gh = facts.gh === "logged in" ? {
69171
+ ok: true,
69172
+ required: false,
69173
+ text: "gh, logged in"
69174
+ } : {
69175
+ ok: false,
69176
+ required: false,
69177
+ text: `gh is ${facts.gh === "missing" ? "not installed" : "not logged in"} (only private Tracked Repos need it)`,
69178
+ fix: facts.gh === "logged out" ? "gh auth login" : darwin ? "brew install gh && gh auth login" : "install gh from https://cli.github.com, then gh auth login"
69179
+ };
69180
+ return [
69181
+ vp,
69182
+ ...brew,
69183
+ ...tailscale,
69184
+ gh
69185
+ ];
69186
+ };
69187
+ const Status = fromJsonString(Struct({ BackendState: optionalKey(String$6) }));
69188
+ const Prefs = fromJsonString(Struct({ RunSSH: optionalKey(Boolean$4) }));
69189
+ const readFacts = fn("readFacts")(function* () {
69190
+ const fs = yield* FileSystem;
69191
+ const vp = yield* runLocal("vp", ["--version"]);
69192
+ const version = /^vp v?(\S+)/m.exec(vp.stdout)?.[1];
69193
+ const status = yield* runLocal("tailscale", ["status", "--json"]);
69194
+ const prefs = yield* runLocal("tailscale", ["debug", "prefs"]);
69195
+ const state = decodeOption(Status)(status.stdout);
69196
+ const ssh = decodeOption(Prefs)(prefs.stdout);
69197
+ const brew = yield* runLocal("brew", ["--version"]);
69198
+ const gh = yield* runLocal("gh", ["auth", "status"]);
69199
+ const app = process.platform === "darwin" && (yield* fs.exists("/Applications/Tailscale.app"));
69200
+ return {
69201
+ platform: process.platform,
69202
+ vp: vp.code === 0 ? version : void 0,
69203
+ tailscale: status.code === 127 && !app ? void 0 : {
69204
+ app,
69205
+ state: state._tag === "Some" ? state.value.BackendState : void 0,
69206
+ ssh: ssh._tag === "Some" && ssh.value.RunSSH === true
69207
+ },
69208
+ brew: brew.code === 0,
69209
+ gh: gh.code === 127 ? "missing" : gh.code === 0 ? "logged in" : "logged out"
69210
+ };
68198
69211
  });
69212
+ //#endregion
69213
+ //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Logger.js
69214
+ const TypeId = "~effect/Logger";
69215
+ /**
69216
+ * Returns `true` if the specified value is a `Logger`, otherwise returns `false`.
69217
+ *
69218
+ * **Example** (Checking logger values)
69219
+ *
69220
+ * ```ts import.meta.vitest
69221
+ * import { Logger } from "effect"
69222
+ *
69223
+ * const myLogger = Logger.make(() => undefined)
69224
+ *
69225
+ * Logger.isLogger(myLogger) // => true
69226
+ * Logger.isLogger("not a logger") // => false
69227
+ * Logger.isLogger({ log: () => {} }) // => false
69228
+ * ```
69229
+ *
69230
+ * @stability stable
69231
+ * @category guards
69232
+ * @since 4.0.0
69233
+ */
69234
+ const isLogger = (u) => hasProperty(u, TypeId);
69235
+ /**
69236
+ * Context reference containing the active loggers for the current fiber.
69237
+ *
69238
+ * **Details**
69239
+ *
69240
+ * By default this set includes the default logger and the tracer logger.
69241
+ * Providing `Logger.layer` replaces or merges with this set depending on its
69242
+ * options.
69243
+ *
69244
+ * **Example** (Accessing current loggers)
69245
+ *
69246
+ * ```ts import.meta.vitest
69247
+ * import { Effect, Logger } from "effect"
69248
+ *
69249
+ * const messages: Array<unknown> = []
69250
+ * const customLogger = Logger.make((options) => {
69251
+ * messages.push(options.message)
69252
+ * })
69253
+ * const program = Effect.gen(function*() {
69254
+ * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers)
69255
+ * yield* Effect.log("Hello from custom logger").pipe(
69256
+ * Effect.provide(Logger.layer([customLogger]))
69257
+ * )
69258
+ * return currentLoggers.has(Logger.defaultLogger)
69259
+ * })
69260
+ *
69261
+ * Effect.runSync(program) // => true
69262
+ * messages // => [["Hello from custom logger"]]
69263
+ * ```
69264
+ *
69265
+ * @stability stable
69266
+ * @category services
69267
+ * @since 4.0.0
69268
+ */
69269
+ const CurrentLoggers = CurrentLoggers$1;
69270
+ /**
69271
+ * Creates a new `Logger` from a log function.
69272
+ *
69273
+ * **Details**
69274
+ *
69275
+ * The log function receives an options object containing the message, log level,
69276
+ * cause, fiber information, and timestamp, and should return the desired output.
69277
+ *
69278
+ * **Example** (Creating loggers from functions)
69279
+ *
69280
+ * ```ts import.meta.vitest
69281
+ * import { Effect, Logger } from "effect"
69282
+ *
69283
+ * const outputs: Array<string> = []
69284
+ * const textLogger = Logger.make((options) =>
69285
+ * `${options.logLevel}: ${options.message}`
69286
+ * )
69287
+ * const collector = Logger.make((options) => outputs.push(textLogger.log(options)))
69288
+ *
69289
+ * const program = Effect.log("Hello World").pipe(
69290
+ * Effect.provide(Logger.layer([collector]))
69291
+ * )
69292
+ * Effect.runSync(program)
69293
+ * outputs // => ["Info: Hello World"]
69294
+ * ```
69295
+ *
69296
+ * @stability stable
69297
+ * @category constructors
69298
+ * @since 2.0.0
69299
+ */
69300
+ const make = loggerMake;
69301
+ /**
69302
+ * Creates a scoped logger that batches the output of another logger.
69303
+ *
69304
+ * **Details**
69305
+ *
69306
+ * The returned effect starts a scoped background process that periodically
69307
+ * passes buffered outputs to `flush`. When the scope closes, the background
69308
+ * process is interrupted and any remaining buffered entries are flushed.
69309
+ *
69310
+ * **Example** (Batching logger output)
69311
+ *
69312
+ * ```ts import.meta.vitest
69313
+ * import { Effect, Logger } from "effect"
69314
+ *
69315
+ * const flushed: Array<ReadonlyArray<string>> = []
69316
+ * const messageLogger = Logger.make((options) => String(options.message))
69317
+ * const batchedLogger = Logger.batched(messageLogger, {
69318
+ * window: "1 hour",
69319
+ * flush: (messages) =>
69320
+ * Effect.sync(() => {
69321
+ * flushed.push(messages)
69322
+ * })
69323
+ * })
69324
+ *
69325
+ * const program = Effect.scoped(Effect.gen(function*() {
69326
+ * const logger = yield* batchedLogger
69327
+ * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger])))
69328
+ * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger])))
69329
+ * }))
69330
+ * await Effect.runPromise(program)
69331
+ * flushed // => [["Event 1", "Event 2"]]
69332
+ * ```
69333
+ *
69334
+ * @stability stable
69335
+ * @category constructors
69336
+ * @since 2.0.0
69337
+ */
69338
+ const batched = /*#__PURE__*/ dual(2, (self, options) => suspend$3(() => {
69339
+ let buffer = [];
69340
+ const flush = uninterruptible$1(suspend$3(() => {
69341
+ if (buffer.length === 0) return void_$2;
69342
+ const arr = buffer;
69343
+ buffer = [];
69344
+ return options.flush(arr);
69345
+ }));
69346
+ return uninterruptibleMask$1((restore) => restore(sleep$1(options.window).pipe(andThen$1(flush), forever$2)).pipe(forkDetach, flatMap$2((fiber) => addFinalizer$2(() => andThen$1(fiberInterrupt(fiber), flush))), as$1(loggerMake((options) => {
69347
+ buffer.push(self.log(options));
69348
+ }))));
69349
+ }));
69350
+ /**
69351
+ * Creates a `Layer` which will overwrite the current set of loggers with the
69352
+ * specified array of `loggers`.
69353
+ *
69354
+ * **Details**
69355
+ *
69356
+ * If the specified array of `loggers` should be _merged_ with the current set
69357
+ * of loggers (instead of overwriting them), set `mergeWithExisting` to `true`.
69358
+ *
69359
+ * **Example** (Providing logger layers)
69360
+ *
69361
+ * ```ts import.meta.vitest
69362
+ * import { Effect, Logger } from "effect"
69363
+ *
69364
+ * const messages: Array<unknown> = []
69365
+ * const customLogger = Logger.make((options) => {
69366
+ * messages.push(options.message)
69367
+ * })
69368
+ * const CustomLoggerLayer = Logger.layer([customLogger])
69369
+ *
69370
+ * const program = Effect.log("Application started").pipe(
69371
+ * Effect.provide(CustomLoggerLayer)
69372
+ * )
69373
+ * Effect.runSync(program)
69374
+ * messages // => [["Application started"]]
69375
+ * ```
69376
+ *
69377
+ * @stability stable
69378
+ * @category layers
69379
+ * @since 4.0.0
69380
+ */
69381
+ const layer = (loggers, options) => effect(CurrentLoggers, withFiber$1(fnUntraced$1(function* (fiber) {
69382
+ const currentLoggers = new Set(options?.mergeWithExisting === true ? fiber.getRef(CurrentLoggers$1) : []);
69383
+ for (const logger of loggers) currentLoggers.add(isEffect$1(logger) ? yield* logger : logger);
69384
+ return currentLoggers;
69385
+ })));
69386
+ /**
69387
+ * Creates a scoped logger that writes string logger output to a file.
69388
+ *
69389
+ * **Details**
69390
+ *
69391
+ * The returned effect requires `FileSystem` and `Scope`. The file logger batches
69392
+ * string output, writes each batch to the specified path, and flushes remaining
69393
+ * entries when the scope closes.
69394
+ *
69395
+ * **Example** (Writing JSON logs to a file)
69396
+ *
69397
+ * ```ts import.meta.vitest
69398
+ * import { Effect, FileSystem, Logger } from "effect"
69399
+ *
69400
+ * const writes: Array<string> = []
69401
+ * const file = {
69402
+ * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
69403
+ * writes.push(new TextDecoder().decode(buffer).trim())
69404
+ * })
69405
+ * } as unknown as FileSystem.File
69406
+ * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
69407
+ * const messageLogger = Logger.make((options) => String(options.message))
69408
+ *
69409
+ * const program = Effect.scoped(Effect.gen(function*() {
69410
+ * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt")
69411
+ * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger])))
69412
+ * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger])))
69413
+ * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger])))
69414
+ * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
69415
+ *
69416
+ * await Effect.runPromise(program)
69417
+ * writes // => ["a\nb\nc"]
69418
+ * ```
69419
+ *
69420
+ * **Example** (Writing logs to files)
69421
+ *
69422
+ * ```ts import.meta.vitest
69423
+ * import { Effect, FileSystem, Logger } from "effect"
69424
+ *
69425
+ * const writes: Array<string> = []
69426
+ * const file = {
69427
+ * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
69428
+ * writes.push(new TextDecoder().decode(buffer).trim())
69429
+ * })
69430
+ * } as unknown as FileSystem.File
69431
+ * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
69432
+ * const messageLogger = Logger.make((options) => String(options.message))
69433
+ *
69434
+ * const program = Effect.scoped(Effect.gen(function*() {
69435
+ * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", {
69436
+ * batchWindow: "1 hour"
69437
+ * })
69438
+ * yield* Effect.log("Application started").pipe(
69439
+ * Effect.provide(Logger.layer([fileLogger]))
69440
+ * )
69441
+ * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
69442
+ *
69443
+ * await Effect.runPromise(program)
69444
+ * writes // => ["Application started"]
69445
+ * ```
69446
+ *
69447
+ * @stability unstable
69448
+ * @category logging
69449
+ * @since 4.0.0
69450
+ */
69451
+ const toFile = /*#__PURE__*/ dual((args) => isLogger(args[0]), (self, path, options) => gen$1(function* () {
69452
+ const logFile = yield* (yield* FileSystem).open(path, {
69453
+ flag: "a+",
69454
+ ...options
69455
+ });
69456
+ const encoder = new TextEncoder();
69457
+ return yield* batched(self, {
69458
+ window: options?.batchWindow ?? 1e3,
69459
+ flush: (output) => ignore$1(logFile.writeAll(encoder.encode(output.join("\n") + "\n")))
69460
+ });
69461
+ }));
68199
69462
  const decodeFact$3 = decodeUnknownOption(Union([Struct({
68200
69463
  fact: Literal("brew"),
68201
69464
  version: NullOr(String$6)
@@ -69662,6 +70925,99 @@ const noLive = {
69662
70925
  aside: (effect) => effect
69663
70926
  };
69664
70927
  const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
70928
+ var LogsError = class extends Error {
70929
+ name = "LogsError";
70930
+ };
70931
+ const shipId = (started, from) => `${formatIso(started).slice(0, 19).replaceAll(":", "-")}Z-${from}`;
70932
+ const parseShipId = (id) => {
70933
+ const [, day, h, m, s, from] = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})Z-(.+)$/.exec(id) ?? [];
70934
+ if (from === void 0) return void 0;
70935
+ return {
70936
+ started: Date.parse(`${day}T${h}:${m}:${s}Z`),
70937
+ from
70938
+ };
70939
+ };
70940
+ const lineFormat = (hidden) => make(({ message, logLevel, date, cause, fiber }) => {
70941
+ const { machine, phase } = fiber.getRef(CurrentLogAnnotations);
70942
+ const scope = [machine, phase].filter((s) => typeof s === "string").join(" ");
70943
+ const prefix = `${date.toISOString()} ${logLevel.toUpperCase().padEnd(5)}${scope === "" ? "" : ` [${scope}]`}`;
70944
+ const parts = (Array.isArray(message) ? message : [message]).map((m) => typeof m === "string" ? m : JSON.stringify(m));
70945
+ if (cause.reasons.length > 0) parts.push(pretty(cause));
70946
+ let text = parts.join(" ");
70947
+ for (const secret of hidden) text = text.replaceAll(secret, "[hidden]");
70948
+ return text.split("\n").map((line) => `${prefix} ${line}`).join("\n");
70949
+ });
70950
+ const machineOf = (line) => /^\S+ +\S+ +\[([^\] ]+)/.exec(line)?.[1];
70951
+ const toPrune = (names) => {
70952
+ const logs = names.filter((name) => name.endsWith(".log")).toSorted();
70953
+ return logs.slice(0, Math.max(0, logs.length - 19));
70954
+ };
70955
+ const logsDir = () => LOGS_DIR.replace(/^~/, process.env.HOME ?? "~");
70956
+ const openShipLog = fn("openShipLog")(function* (id, hidden) {
70957
+ const fs = yield* FileSystem;
70958
+ const dir = logsDir();
70959
+ yield* fs.makeDirectory(dir, { recursive: true });
70960
+ for (const name of toPrune(yield* fs.readDirectory(dir))) yield* fs.remove(`${dir}/${name}`);
70961
+ return yield* toFile(lineFormat(hidden), `${dir}/${id}.log`);
70962
+ });
70963
+ const read = fn("read")(function* (target, script, onLine) {
70964
+ const code = yield* runScript(target, script, [], onLine);
70965
+ if (!target.local && code === 255) return yield* fail$1(new LogsError(`could not reach ${target.name} over tailscale ssh`));
70966
+ });
70967
+ const shipIds = fn("shipIds")(function* (target) {
70968
+ const ids = [];
70969
+ yield* read(target, `ls -1 ${quotePath(LOGS_DIR)} 2>/dev/null`, (line) => sync(() => {
70970
+ if (line.endsWith(".log")) ids.push(line.slice(0, -4));
70971
+ }));
70972
+ return ids.toSorted();
70973
+ });
70974
+ const MISSING = "ff-missing";
70975
+ const logs = fn("logs")(function* (flags) {
70976
+ const here = {
70977
+ name: "this Machine",
70978
+ local: true
70979
+ };
70980
+ const local = yield* shipIds(here);
70981
+ const self = parseShipId(local.at(-1) ?? "")?.from;
70982
+ const tailnet = yield* cached(readTailnet());
70983
+ const targetOf = fn("targetOf")(function* (name) {
70984
+ if (name === self) return here;
70985
+ const { self: me, fleet } = yield* tailnet;
70986
+ if (name === me.name) return here;
70987
+ const machine = fleet.find((m) => m.name === name);
70988
+ if (machine === void 0) return yield* fail$1(new LogsError(`no Machine ${name} on the tailnet`));
70989
+ if (!machine.online) return yield* fail$1(new LogsError(`${name} is offline; its logs are there`));
70990
+ return {
70991
+ name,
70992
+ local: false
70993
+ };
70994
+ });
70995
+ if (flags.list) {
70996
+ if (local.length === 0) return yield* log("no Ship Log on this Machine yet");
70997
+ for (const id of local.toReversed()) yield* log(id);
70998
+ return;
70999
+ }
71000
+ const id = flags.id ?? (yield* latestShip(local, self));
71001
+ const ship = parseShipId(id);
71002
+ if (ship === void 0) return yield* fail$1(new LogsError(`not a Ship id: ${id} (fizz logs --list shows them)`));
71003
+ let missing = false;
71004
+ yield* read(local.includes(id) ? here : yield* targetOf(ship.from), `ff_log=${quotePath(LOGS_DIR)}/${quote(`${id}.log`)}\nif [ -f "$ff_log" ]; then cat "$ff_log"; else echo ${MISSING}; fi`, (line) => {
71005
+ if (line === MISSING) return sync(() => void (missing = true));
71006
+ if (flags.machine !== void 0 && machineOf(line) !== flags.machine) return void_;
71007
+ return log(line);
71008
+ });
71009
+ if (missing) return yield* fail$1(new LogsError(`no log for ${id} on ${ship.from}; a Machine keeps its last 20`));
71010
+ });
71011
+ const latestShip = fn("latestShip")(function* (local, self) {
71012
+ const newest = local.at(-1);
71013
+ const record = yield* readConfigRecord;
71014
+ const from = parseShipId(record?.ship ?? "")?.from;
71015
+ const at = self === void 0 ? Infinity : Date.parse(record?.reached[self] ?? "");
71016
+ const newestStarted = parseShipId(newest ?? "")?.started ?? -Infinity;
71017
+ if (record !== void 0 && from !== void 0 && from !== self && at > newestStarted) return record.ship;
71018
+ if (newest === void 0) return yield* fail$1(new LogsError("no Ship Log on this Machine yet"));
71019
+ return newest;
71020
+ });
69665
71021
  //#endregion
69666
71022
  //#region src/machine.ts
69667
71023
  const decodeAt = decodeUnknownOption(AtFact);
@@ -69838,6 +71194,45 @@ const askQuestions = fn("askQuestions")(function* (input) {
69838
71194
  return answered;
69839
71195
  });
69840
71196
  //#endregion
71197
+ //#region src/report.tsx
71198
+ const reportPath = () => `${STATE_DIR.replace(/^~/, process.env.HOME ?? "~")}/last-ship.json`;
71199
+ const LedgerJson = fromJsonString(LedgerState);
71200
+ const encode = encodeEffect(LedgerJson);
71201
+ const decode = decodeEffect(LedgerJson);
71202
+ const saveReport = fn("saveReport")(function* (state) {
71203
+ const fs = yield* FileSystem;
71204
+ const path = reportPath();
71205
+ yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
71206
+ yield* fs.writeFileString(path, yield* encode(state));
71207
+ });
71208
+ const report = fn("report")(function* () {
71209
+ const fs = yield* FileSystem;
71210
+ const path = reportPath();
71211
+ if (!(yield* fs.exists(path))) return yield* log("no Ship from this Machine yet");
71212
+ const decoded = yield* decode(yield* fs.readFileString(path)).pipe(option);
71213
+ if (isNone(decoded)) return yield* log(`the last report (${path}) is from another version of fleetfizz; the next Ship replaces it`);
71214
+ const state = decoded.value;
71215
+ const when = formatLocal(makeUnsafe(state.started), {
71216
+ dateStyle: "medium",
71217
+ timeStyle: "short"
71218
+ });
71219
+ yield* log(toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71220
+ flexDirection: "column",
71221
+ children: [
71222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71223
+ tone: "dim",
71224
+ children: `last Ship ${when} from ${state.from}`
71225
+ }),
71226
+ state.notes.map((note, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoteLine, { note }, i)),
71227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LedgerView, {
71228
+ state,
71229
+ full: true,
71230
+ now: state.ended ?? state.started
71231
+ })
71232
+ ]
71233
+ })));
71234
+ });
71235
+ //#endregion
69841
71236
  //#region src/ship.ts
69842
71237
  var ShipError = class extends Error {
69843
71238
  name = "ShipError";
@@ -69899,7 +71294,6 @@ const staleConfigs = fn("staleConfigs")(function* (text, base, own, checked, led
69899
71294
  }];
69900
71295
  });
69901
71296
  });
69902
- const schemaJson = encodeSync(fromJsonString(Unknown, { space: 2 }));
69903
71297
  const ship = fn("ship")(function* (flags) {
69904
71298
  const tailnet = yield* readTailnet();
69905
71299
  const id = shipId(yield* now, tailnet.self.name);
@@ -69911,9 +71305,9 @@ const ship = fn("ship")(function* (flags) {
69911
71305
  yield* logInfo(`fizz ${process.argv.slice(2).join(" ")}`);
69912
71306
  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)" : ""}`);
69913
71307
  while ((yield* shipPass(flags, tailnet, hidden, id)) === "adopted");
69914
- }).pipe(onExit((exit) => isSuccess(exit) ? logInfo("the Ship ended") : logError("the Ship stopped", exit.cause)), provide(layer$1(logger === void 0 ? [] : [logger])), provideService(MinimumLogLevel, "Debug"));
71308
+ }).pipe(onExit((exit) => isSuccess(exit) ? logInfo("the Ship ended") : logError("the Ship stopped", exit.cause)), provide(layer(logger === void 0 ? [] : [logger])), provideService(MinimumLogLevel, "Debug"));
69915
71309
  }, scoped);
69916
- const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
71310
+ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id, show = "ledger") {
69917
71311
  const fs = yield* FileSystem;
69918
71312
  const parsed = parseAnswers(flags.answers);
69919
71313
  if ("invalid" in parsed) return yield* fail$1(new ShipError(parsed.invalid));
@@ -69936,20 +71330,22 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
69936
71330
  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));
69937
71331
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
69938
71332
  yield* logInfo(`${flags.dryRun ? "dry run, " : ""}targets: ${targetsList.map((m) => m.name).join(", ")}`);
69939
- yield* log(`${flags.dryRun ? "dry run, " : ""}${count} from ${tailnet.self.name}`);
71333
+ const quiet = show !== "ledger";
71334
+ if (!quiet) yield* log(`${flags.dryRun ? "dry run, " : ""}${count} from ${tailnet.self.name}`);
69940
71335
  const color = colorful();
69941
71336
  const { ledger, store } = yield* makeLedger({
69942
71337
  from: tailnet.self.name,
69943
71338
  dryRun: flags.dryRun,
69944
71339
  machines: targetsList.map((m) => m.name),
69945
- print: (note) => log(noteText(note, color))
71340
+ print: (note) => quiet ? void_ : log(noteText(note, color))
69946
71341
  });
69947
- const redraw = canRedraw();
71342
+ const redraw = !quiet && canRedraw();
69948
71343
  let changing = false;
69949
71344
  let adopted = false;
69950
71345
  const finish = fn("finish")(function* () {
69951
71346
  const state = yield* ledger.end;
69952
71347
  if (adopted) return;
71348
+ if (show !== "ledger") return show(state);
69953
71349
  const saved = changing && (yield* saveReport(state).pipe(as(true), catch_((e) => error(`could not save the report for fizz report: ${e.message}`).pipe(as(false)))));
69954
71350
  yield* log(ledgerText(state, redraw ? {
69955
71351
  full: false,
@@ -70003,6 +71399,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
70003
71399
  phase: "check"
70004
71400
  })), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
70005
71401
  const stale = yield* staleConfigs(yield* fs.readFileString(path), base, own, checked, ledger);
71402
+ const asking = new Map(flags.dryRun ? stale.flatMap((s) => s.machines.filter((m) => given("config", m) !== "overwrite").map((m) => [m, s.description])) : []);
70006
71403
  if (stale.length > 0) {
70007
71404
  const what = stale.map((s) => s.description).join("; ");
70008
71405
  const answers = stale.flatMap((s) => s.machines).map((m) => given("config", m));
@@ -70169,10 +71566,14 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
70169
71566
  }
70170
71567
  const text = yield* fs.readFileString(path);
70171
71568
  const sha = yield* shaOf(text);
70172
- for (const { machine, state, plan: p } of planned) yield* ledger.plan(machine.name, p, {
70173
- ...state.config === void 0 ? {} : { from: state.config.sha.slice(0, 7) },
70174
- to: sha.slice(0, 7)
70175
- });
71569
+ for (const { machine, state, plan: p } of planned) {
71570
+ const asked = asking.get(machine.name);
71571
+ yield* ledger.plan(machine.name, p, {
71572
+ ...state.config === void 0 ? {} : { from: state.config.sha.slice(0, 7) },
71573
+ to: sha.slice(0, 7),
71574
+ ...asked === void 0 ? {} : { asked }
71575
+ });
71576
+ }
70176
71577
  if (flags.dryRun) return;
70177
71578
  const at = formatIso(yield* now);
70178
71579
  const recordFor = (reached) => encodeConfigRecord({
@@ -70184,7 +71585,7 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
70184
71585
  const record = recordFor(planned.map((g) => g.machine.name));
70185
71586
  if (own !== void 0) yield* writeConfigRecord(record);
70186
71587
  yield* logInfo(`the config's record: ${record}`);
70187
- const schema = `${schemaJson(fleetConfigJsonSchema())}\n`;
71588
+ const schema = fleetConfigSchemaText();
70188
71589
  const delivered = yield* forEach$1(planned, fn("applyOne")(function* ({ machine, local, plan: p }) {
70189
71590
  return (yield* apply({
70190
71591
  name: machine.name,
@@ -70198,6 +71599,402 @@ const shipPass = fn("shipPass")(function* (flags, tailnet, hidden, id) {
70198
71599
  if (own !== void 0) yield* writeConfigRecord(recordFor(delivered));
70199
71600
  }, scoped);
70200
71601
  //#endregion
71602
+ //#region src/init.tsx
71603
+ const DOTFILES = [
71604
+ "~/.gitconfig",
71605
+ "~/.zshrc",
71606
+ "~/.bashrc",
71607
+ "~/.profile",
71608
+ "~/.vimrc"
71609
+ ];
71610
+ const draftText = (picks) => {
71611
+ const edits = [
71612
+ ...picks.vp === void 0 ? [] : [[["vp", "version"], picks.vp.version], ...picks.vp.node === void 0 ? [] : [[["vp", "node"], picks.vp.node]]],
71613
+ ...picks.globals.map((g) => [[
71614
+ "vp",
71615
+ "items",
71616
+ g.name
71617
+ ], `^${g.version}`]),
71618
+ ...picks.formulae.map((f) => [[
71619
+ "brew",
71620
+ "items",
71621
+ f
71622
+ ], "*"]),
71623
+ ...picks.casks.length > 0 && picks.linux ? [[["cask", "platforms"], ["darwin"]]] : [],
71624
+ ...picks.casks.map((c) => [[
71625
+ "cask",
71626
+ "items",
71627
+ c
71628
+ ], "*"]),
71629
+ ...picks.files.map((f) => [[
71630
+ "files",
71631
+ "items",
71632
+ fileName(f)
71633
+ ], f])
71634
+ ];
71635
+ const start = `{\n "$schema": "./${SCHEMA_FILE}",\n "machines": [],\n}\n`;
71636
+ return [...picks.machines.map((m, i) => [["machines", i], m]), ...edits].reduce((text, [path, value]) => {
71637
+ const result = insertAt(text, path, value);
71638
+ return "text" in result ? result.text : text;
71639
+ }, start);
71640
+ };
71641
+ const CheckLine = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71642
+ tone: props.check.ok ? "ok" : props.check.required ? "fail" : "skip",
71643
+ children: props.check.ok ? "✓" : props.check.required ? "✗" : "!"
71644
+ }), ` ${props.check.text}`] });
71645
+ const checksText = (checks) => {
71646
+ const required = checks.filter((c) => !c.ok && c.required && c.fix !== void 0);
71647
+ const optional = checks.filter((c) => !c.ok && !c.required && c.fix !== void 0);
71648
+ return toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71649
+ flexDirection: "column",
71650
+ children: [
71651
+ checks.map((check, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CheckLine, { check }, i)),
71652
+ required.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71653
+ flexDirection: "column",
71654
+ marginTop: 1,
71655
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: "Fix these, then run fizz init again:" }), required.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: ` ${c.fix}` }, i))]
71656
+ }),
71657
+ optional.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71658
+ flexDirection: "column",
71659
+ marginTop: 1,
71660
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: required.length > 0 ? "Optional:" : "Optional, now or later:" }), optional.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: ` ${c.fix}` }, i))]
71661
+ })
71662
+ ]
71663
+ }));
71664
+ };
71665
+ const Globals = fromJsonString(ArraySchema(Struct({
71666
+ name: String$6,
71667
+ version: String$6
71668
+ })));
71669
+ const lines = fn("lines")(function* (command, args) {
71670
+ const ran = yield* runLocal(command, args);
71671
+ return ran.code === 0 ? ran.stdout.split("\n").filter((l) => l.trim() !== "") : [];
71672
+ });
71673
+ const pickSome = (message, choices) => choices.length === 0 ? succeed$3([]) : MultiSelect({
71674
+ message,
71675
+ choices,
71676
+ maxPerPage: 15
71677
+ });
71678
+ const pickMachines = (tailnet) => MultiSelect({
71679
+ message: "Which Machines are in the Fleet?",
71680
+ choices: tailnet.fleet.map((m) => {
71681
+ const self = m.name === tailnet.self.name;
71682
+ const note = self ? "this Machine" : machineNote(m, tailnet);
71683
+ return {
71684
+ title: `${m.name} ${m.platform}`,
71685
+ value: m.name,
71686
+ selected: self || note === void 0,
71687
+ ...note === void 0 ? {} : { description: note }
71688
+ };
71689
+ }),
71690
+ min: 1
71691
+ });
71692
+ const init = fn("init")(function* (flags) {
71693
+ if (!interactive()) return yield* new ConfigAddError({ reason: "fizz init asks questions, so it needs a terminal; write the config by hand instead" });
71694
+ const fs = yield* FileSystem;
71695
+ const path = flags.config ?? configPath(process.env);
71696
+ const checks = checksOf(yield* readFacts());
71697
+ yield* log(checksText(checks));
71698
+ if (checks.some((c) => !c.ok && c.required)) {
71699
+ process.exitCode = 1;
71700
+ return;
71701
+ }
71702
+ const tailnet = yield* readTailnet();
71703
+ if (yield* fs.exists(path)) {
71704
+ const config = yield* loadConfig(path);
71705
+ yield* log(`\n${configText({
71706
+ path,
71707
+ config,
71708
+ tailnet,
71709
+ footer: ["This Machine already has a config. Add to it with fizz config add, and see where each Machine stands with fizz status."]
71710
+ })}`);
71711
+ return;
71712
+ }
71713
+ yield* log("");
71714
+ const picked = yield* pickMachines(tailnet);
71715
+ const machines = picked.includes(tailnet.self.name) ? picked : [tailnet.self.name, ...picked];
71716
+ if (machines !== picked) yield* log(`${tailnet.self.name} is in the Fleet too: a Ship starts from it.`);
71717
+ const vpVersion = /^vp v?(\S+)/m.exec((yield* runLocal("vp", ["--version"])).stdout)?.[1];
71718
+ const node = /^v(\S+)/.exec((yield* runLocal("node", ["--version"])).stdout.trim())?.[1];
71719
+ const pin = vpVersion !== void 0 && (yield* Confirm({
71720
+ message: `Pin vp ${vpVersion}${node === void 0 ? "" : ` and Node ${node}`}, the versions on this Machine?`,
71721
+ initial: true
71722
+ }));
71723
+ const installed = getOrElse(decodeOption(Globals)((yield* runLocal("vp", [
71724
+ "list",
71725
+ "--global",
71726
+ "--json"
71727
+ ])).stdout), () => []).filter((g) => isGlobalName(g.name));
71728
+ const globals = yield* pickSome(`Which Globals from this Machine? (${installed.length} installed)`, installed.map((g) => ({
71729
+ title: `${g.name} ${g.version}`,
71730
+ value: g
71731
+ })));
71732
+ const leaves = yield* lines("brew", ["leaves", "--installed-on-request"]);
71733
+ const formulae = yield* pickSome(`Which Formulae from this Machine? (${leaves.length} installed on request)`, leaves.map((f) => ({
71734
+ title: f,
71735
+ value: f
71736
+ })));
71737
+ const installedCasks = process.platform === "darwin" ? yield* lines("brew", ["list", "--cask"]) : [];
71738
+ const casks = yield* pickSome(`Which Casks from this Machine? (${installedCasks.length} installed)`, installedCasks.map((c) => ({
71739
+ title: c,
71740
+ value: c
71741
+ })));
71742
+ const present = yield* filter(DOTFILES, (f) => fs.exists(expandHome$1(f)));
71743
+ const files = yield* pickSome("Which Shared Files? A Ship copies each from the Machine it starts on.", present.map((f) => ({
71744
+ title: f,
71745
+ value: f
71746
+ })));
71747
+ const text = draftText({
71748
+ machines,
71749
+ ...pin && vpVersion !== void 0 ? { vp: {
71750
+ version: vpVersion,
71751
+ node
71752
+ } } : {},
71753
+ globals,
71754
+ formulae,
71755
+ casks,
71756
+ files,
71757
+ linux: machines.some((m) => tailnet.fleet.find((f) => f.name === m)?.platform === "linux")
71758
+ });
71759
+ const config = yield* saveConfig(path, text);
71760
+ const paths = yield* Path$1;
71761
+ yield* fs.writeFileString(paths.join(paths.dirname(path), SCHEMA_FILE), fleetConfigSchemaText());
71762
+ yield* log(`\n${toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71763
+ tone: "ok",
71764
+ children: "Wrote"
71765
+ }), ` ${homeRelative(path)} and ${SCHEMA_FILE}`] }))}`);
71766
+ yield* log(configText({
71767
+ path,
71768
+ config,
71769
+ tailnet,
71770
+ footer: ["Add more with fizz config add repos|files|vp|brew|cask."]
71771
+ }));
71772
+ if (yield* Confirm({
71773
+ message: "Run fizz ship --dry-run now?",
71774
+ initial: true
71775
+ })) yield* ship({
71776
+ dryRun: true,
71777
+ only: [],
71778
+ rebuild: [],
71779
+ answers: [],
71780
+ config: flags.config,
71781
+ version: flags.version
71782
+ });
71783
+ });
71784
+ //#endregion
71785
+ //#region src/status.tsx
71786
+ const cellOf = (m, address) => {
71787
+ if (m.status === "skipped") return {
71788
+ glyph: "○",
71789
+ tone: "dim"
71790
+ };
71791
+ if (m.status === "failed") return {
71792
+ glyph: "✗",
71793
+ tone: "fail"
71794
+ };
71795
+ const item = m.items.find((i) => i.name === address);
71796
+ if (item === void 0) return {
71797
+ glyph: "·",
71798
+ tone: "dim"
71799
+ };
71800
+ if (item.status === "failed") return {
71801
+ glyph: "✗",
71802
+ tone: "fail"
71803
+ };
71804
+ if (item.asked === true) return {
71805
+ glyph: "?",
71806
+ tone: "ask"
71807
+ };
71808
+ if (item.status === "skipped") return {
71809
+ glyph: "–",
71810
+ tone: "skip"
71811
+ };
71812
+ return item.changes ? {
71813
+ glyph: "↑",
71814
+ tone: "run"
71815
+ } : {
71816
+ glyph: "✓",
71817
+ tone: "ok"
71818
+ };
71819
+ };
71820
+ const rowsOf = (state, config) => {
71821
+ const sections = sectionsOf(config);
71822
+ const planned = state.machines.flatMap((m) => m.items.map((i) => parseAddress(i.name))).filter((at) => at !== void 0);
71823
+ return SHOWN.flatMap((provider) => {
71824
+ const rows = sections.find((s) => s.provider === provider)?.rows ?? [];
71825
+ const names = [.../* @__PURE__ */ new Set([...rows.map((r) => r.name), ...planned.filter((at) => at.provider === provider).map((at) => at.item)])];
71826
+ if (names.length === 0) return [];
71827
+ return [{
71828
+ provider,
71829
+ items: names.map((name) => {
71830
+ const wants = [...new Set(rows.filter((r) => r.name === name).map((r) => r.wants))];
71831
+ return {
71832
+ name,
71833
+ address: `${provider}:${name}`,
71834
+ wants: wants.length === 1 ? wants[0] : ""
71835
+ };
71836
+ })
71837
+ }];
71838
+ });
71839
+ };
71840
+ const differs = (m) => m.items.filter((i) => i.changes || i.status === "failed" || i.status === "skipped");
71841
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
71842
+ const summaryOf = (m, record, now) => {
71843
+ if (m.status === "skipped") {
71844
+ const at = record?.reached[m.name];
71845
+ return {
71846
+ text: `${m.detail ?? "skipped"} · ${at === void 0 ? "never reached" : `last reached ${ago(now - Date.parse(at))}`}`,
71847
+ tone: "dim"
71848
+ };
71849
+ }
71850
+ if (m.status === "failed") return {
71851
+ text: `not checked: ${m.detail ?? "failed"}`,
71852
+ tone: "fail"
71853
+ };
71854
+ const items = differs(m);
71855
+ const counts = [
71856
+ [items.filter((i) => i.status === "failed").length, "failing"],
71857
+ [items.filter((i) => i.changes && i.status !== "failed").length, "to change"],
71858
+ [items.filter((i) => i.asked === true).length, "Question"],
71859
+ [items.filter((i) => i.status === "skipped" && i.asked !== true).length, "skipped"]
71860
+ ];
71861
+ const parts = counts.flatMap(([n, what]) => n === 0 ? [] : [what === "Question" ? plural(n, what) : `${n} ${what}`]);
71862
+ return parts.length === 0 ? {
71863
+ text: "in line",
71864
+ tone: "ok"
71865
+ } : {
71866
+ text: parts.join(" · "),
71867
+ tone: counts[0][0] > 0 ? "fail" : "run"
71868
+ };
71869
+ };
71870
+ const value = (i) => i.status === "failed" || i.status === "skipped" ? i.detail ?? "" : i.from === void 0 ? `+ ${i.to ?? ""}` : `${i.from} → ${i.to ?? ""}`;
71871
+ const statusText = (input) => {
71872
+ const { state } = input;
71873
+ const machines = state.machines;
71874
+ const checked = machines.filter((m) => m.status !== "skipped" && m.status !== "failed");
71875
+ const sections = rowsOf(state, input.config);
71876
+ const nameWidth = Math.max(...machines.map((m) => m.name.length)) + 2;
71877
+ const itemWidth = Math.max(0, ...sections.flatMap((s) => s.items.map((i) => i.name.length))) + 2;
71878
+ const wantsWidth = Math.min(32, Math.max(0, ...sections.flatMap((s) => s.items.map((i) => (i.wants ?? "").length)))) + 2;
71879
+ const index = (n) => String(n + 1).padEnd(String(machines.length).length + 1);
71880
+ const anything = machines.some((m) => differs(m).length > 0);
71881
+ return toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71882
+ flexDirection: "column",
71883
+ children: [
71884
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71885
+ tone: "dim",
71886
+ children: `Checked ${checked.length} of ${plural(machines.length, "Machine")} from ${state.from}`
71887
+ }),
71888
+ state.notes.filter((n) => n.kind === "info").map((n, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71889
+ tone: "dim",
71890
+ children: n.text
71891
+ }, i)),
71892
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
71893
+ flexDirection: "column",
71894
+ marginTop: 1,
71895
+ children: machines.map((m, i) => {
71896
+ const summary = summaryOf(m, input.record, input.now);
71897
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
71898
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71899
+ tone: "bold",
71900
+ children: index(i)
71901
+ }),
71902
+ m.name.padEnd(nameWidth),
71903
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71904
+ tone: summary.tone,
71905
+ children: summary.text
71906
+ })
71907
+ ] }, m.name);
71908
+ })
71909
+ }),
71910
+ sections.map((s, n) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71911
+ flexDirection: "column",
71912
+ marginTop: n === 0 ? 1 : 0,
71913
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71914
+ tone: "bold",
71915
+ children: s.provider.padEnd(2 + itemWidth + wantsWidth)
71916
+ }), n === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71917
+ tone: "bold",
71918
+ children: machines.map((_, i) => i + 1).join(" ")
71919
+ }) : ""] }), s.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
71920
+ ` ${item.name.padEnd(itemWidth)}`,
71921
+ (item.wants ?? "").slice(0, 32).padEnd(wantsWidth),
71922
+ machines.map((m, i) => {
71923
+ const cell = cellOf(m, item.address);
71924
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71925
+ tone: cell.tone,
71926
+ children: `${cell.glyph}${i < machines.length - 1 ? " ".repeat(String(i + 2).length) : ""}`
71927
+ }, m.name);
71928
+ })
71929
+ ] }, item.name))]
71930
+ }, s.provider)),
71931
+ machines.flatMap((m) => {
71932
+ const items = m.status === "skipped" || m.status === "failed" ? [] : differs(m);
71933
+ if (items.length === 0) return [];
71934
+ const width = Math.max(...items.map((i) => i.name.length)) + 2;
71935
+ return [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
71936
+ flexDirection: "column",
71937
+ marginTop: 1,
71938
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71939
+ tone: "bold",
71940
+ children: m.name
71941
+ }), items.map((i) => {
71942
+ const cell = cellOf(m, i.name);
71943
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
71944
+ " ",
71945
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71946
+ tone: cell.tone,
71947
+ children: cell.glyph
71948
+ }),
71949
+ ` ${i.name.padEnd(width)}${value(i)}`,
71950
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71951
+ tone: "dim",
71952
+ children: i.reason === void 0 ? "" : ` ${i.reason}`
71953
+ })
71954
+ ] }, i.name);
71955
+ })]
71956
+ }, m.name)];
71957
+ }),
71958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
71959
+ marginTop: 1,
71960
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
71961
+ tone: "dim",
71962
+ children: anything ? "fizz ship to bring them in line" : "Every Machine checked is in line."
71963
+ })
71964
+ })
71965
+ ]
71966
+ }));
71967
+ };
71968
+ const status = fn("status")(function* (flags) {
71969
+ const tailnet = yield* readTailnet();
71970
+ const path = flags.config ?? configPath(process.env);
71971
+ const config = yield* loadConfig(path);
71972
+ const record = flags.config === void 0 ? yield* readConfigRecord : void 0;
71973
+ const now$2 = now;
71974
+ const id = shipId(yield* now$2, tailnet.self.name);
71975
+ const tty = Boolean(process.stdout.isTTY);
71976
+ if (tty) process.stdout.write(`checking ${plural(config.machines.length, "Machine")}...`);
71977
+ let state;
71978
+ yield* shipPass({
71979
+ dryRun: true,
71980
+ only: [],
71981
+ rebuild: [],
71982
+ answers: [],
71983
+ config: flags.config,
71984
+ version: flags.version
71985
+ }, tailnet, /* @__PURE__ */ new Set(), id, (end) => {
71986
+ state = end;
71987
+ }).pipe(scoped, provide(layer([])), ensuring(sync(() => tty && process.stdout.write("\r\x1B[2K"))));
71988
+ if (state === void 0) return;
71989
+ if (anyFailed(state)) process.exitCode = 1;
71990
+ yield* log(statusText({
71991
+ state,
71992
+ config,
71993
+ record,
71994
+ now: toEpochMillis(yield* now$2)
71995
+ }));
71996
+ });
71997
+ //#endregion
70201
71998
  //#region src/command.ts
70202
71999
  const printProblems = (effect) => effect.pipe(catch_((e) => gen(function* () {
70203
72000
  yield* error(e.message);
@@ -70225,6 +72022,14 @@ const shipCommand = make$1("ship", {
70225
72022
  config: getOrUndefined$1(flags.config),
70226
72023
  version
70227
72024
  }).pipe(printProblems)));
72025
+ const initCommand = make$1("init", { config: String$1("config").pipe(optional, withMetavar("path"), withDescription$1("Where to write the config. Default: `$XDG_CONFIG_HOME/fleetfizz/config.jsonc`.")) }).pipe(withDescription("Set up this Machine to ship: check what fleetfizz needs here and print any fixes, then pick the Fleet from the tailnet and what to keep in line, and write the config."), withHandler((flags) => init({
72026
+ config: getOrUndefined$1(flags.config),
72027
+ version
72028
+ }).pipe(printProblems)));
72029
+ const statusCommand = make$1("status", { config: String$1("config").pipe(optional, withMetavar("path"), withDescription$1("The config file. Default: `$XDG_CONFIG_HOME/fleetfizz/config.jsonc`.")) }).pipe(withDescription("Check every reachable Machine and show where each Item stands: in line, to change, a Question, or failing. Changes nothing and asks nothing."), withHandler((flags) => status({
72030
+ config: getOrUndefined$1(flags.config),
72031
+ version
72032
+ }).pipe(printProblems)));
70228
72033
  const reportCommand = make$1("report", {}).pipe(withDescription("Print the full Ledger of the last Ship from this Machine."), withHandler(() => report()));
70229
72034
  const logsCommand = make$1("logs", {
70230
72035
  ship: String$2("ship").pipe(optional$1, withDescription$2("The Ship id, printed at the end of every Ship.")),
@@ -70235,12 +72040,102 @@ const logsCommand = make$1("logs", {
70235
72040
  machine: getOrUndefined$1(flags.machine),
70236
72041
  list: flags.list
70237
72042
  }).pipe(printProblems)));
72043
+ const configFlags = { config: String$1("config").pipe(optional, withMetavar("path"), withDescription$1("The config file. Default: `$XDG_CONFIG_HOME/fleetfizz/config.jsonc`.")) };
72044
+ const targetingFlags = {
72045
+ platform: Literals("platform", ["darwin", "linux"]).pipe(atLeast(0), withDescription$1("Only Machines on this Platform. Repeat the flag for both.")),
72046
+ machine: String$1("machine").pipe(atLeast(0), withMetavar("machine"), withDescription$1("Only this Machine. Repeat the flag for more."))
72047
+ };
72048
+ const latestFlag = Boolean$1("latest").pipe(withDefault(false), withDescription$1("Upgrade it on every Ship (`\"latest\"`) instead of installing it once (`\"*\"`)."));
72049
+ const configCommand = make$1("config").pipe(withSharedFlags(configFlags), withDescription("Show the config: each Provider's Items and the Machines they target, and when a Ship last reached each Machine."), withHandler((flags) => showConfig({ config: getOrUndefined$1(flags.config) }).pipe(printProblems)));
72050
+ const configFile = map$5(configCommand, (flags) => getOrUndefined$1(flags.config));
72051
+ const homebrewCommand = (provider) => make$1(provider, {
72052
+ name: String$2("name").pipe(withDescription$2(provider === "brew" ? "The Formula, or a tapped name (`owner/tap/name`)." : "The Cask, or a tapped name.")),
72053
+ latest: latestFlag,
72054
+ ...targetingFlags
72055
+ }).pipe(withDescription(provider === "brew" ? "Add a Formula." : "Add a Cask."), withHandler((flags) => flatMap$1(configFile, (config) => addHomebrew({
72056
+ config,
72057
+ provider,
72058
+ name: flags.name,
72059
+ latest: flags.latest,
72060
+ targeting: {
72061
+ platforms: flags.platform,
72062
+ machines: flags.machine
72063
+ }
72064
+ })).pipe(printProblems)));
72065
+ const addCommand = make$1("add").pipe(withDescription("Add an Item or a Machine to the config, then validate and save it."), withSubcommands([
72066
+ make$1("machines", { names: String$2("name").pipe(variadic(), withDescription$2("Machine names. Without any, pick from the tailnet.")) }).pipe(withDescription("Add Machines from the tailnet to the Fleet."), withHandler((flags) => flatMap$1(configFile, (config) => addMachines({
72067
+ config,
72068
+ names: flags.names
72069
+ })).pipe(printProblems))),
72070
+ make$1("vp", {
72071
+ spec: String$2("package").pipe(withDescription$2("An npm package, with a Range (`vercel@^59`) or without one: then `^` the version installed here, or the newest published.")),
72072
+ ...targetingFlags
72073
+ }).pipe(withDescription("Add a Global."), withHandler((flags) => flatMap$1(configFile, (config) => addGlobal({
72074
+ config,
72075
+ spec: flags.spec,
72076
+ targeting: {
72077
+ platforms: flags.platform,
72078
+ machines: flags.machine
72079
+ }
72080
+ })).pipe(printProblems))),
72081
+ homebrewCommand("brew"),
72082
+ homebrewCommand("cask"),
72083
+ make$1("repos", {
72084
+ origin: String$2("origin").pipe(withDescription$2("The GitHub `owner/repo`.")),
72085
+ name: String$1("name").pipe(optional, withDescription$1("The Item's name. Default: the repo's name.")),
72086
+ ref: String$1("ref").pipe(optional, withDescription$1("A branch or a full commit SHA. Default: the origin's default branch.")),
72087
+ install: String$1("install").pipe(optional, withDescription$1("The Install Command. Asked for when missing.")),
72088
+ output: String$1("output").pipe(atLeast(0), withMetavar("path"), withDescription$1("A path outside the clone the Install Command produces. Repeat the flag for more.")),
72089
+ ...targetingFlags
72090
+ }).pipe(withDescription("Add a Tracked Repo."), withHandler((flags) => flatMap$1(configFile, (config) => addRepo({
72091
+ config,
72092
+ origin: flags.origin,
72093
+ name: getOrUndefined$1(flags.name),
72094
+ ref: getOrUndefined$1(flags.ref),
72095
+ install: getOrUndefined$1(flags.install),
72096
+ outputs: flags.output,
72097
+ targeting: {
72098
+ platforms: flags.platform,
72099
+ machines: flags.machine
72100
+ }
72101
+ })).pipe(printProblems))),
72102
+ make$1("files", {
72103
+ path: String$2("path").pipe(withDescription$2("A file or directory on this Machine.")),
72104
+ name: String$1("name").pipe(optional, withDescription$1("The Item's name. Default: the last part of the path, without a leading dot.")),
72105
+ exclude: String$1("exclude").pipe(atLeast(0), withMetavar("glob"), withDescription$1("A glob inside a directory that is not copied. Repeat the flag for more.")),
72106
+ ...targetingFlags
72107
+ }).pipe(withDescription("Add a Shared File."), withHandler((flags) => flatMap$1(configFile, (config) => addFile({
72108
+ config,
72109
+ path: flags.path,
72110
+ name: getOrUndefined$1(flags.name),
72111
+ exclude: flags.exclude,
72112
+ targeting: {
72113
+ platforms: flags.platform,
72114
+ machines: flags.machine
72115
+ }
72116
+ })).pipe(printProblems)))
72117
+ ]));
72118
+ const removeCommand = make$1("remove", { address: String$2("item").pipe(withDescription$2("`provider:item` (`brew:tmux`, `vp:node`), or `machines:<name>` to take a Machine out of the Fleet.")) }).pipe(withDescription("Remove an Item or a Machine from the config. A Ship never uninstalls what it removes."), withHandler((flags) => flatMap$1(configFile, (config) => removeItem({
72119
+ config,
72120
+ address: flags.address
72121
+ })).pipe(printProblems)));
72122
+ const editCommand = make$1("edit").pipe(withDescription("Edit the config in `$VISUAL` or `$EDITOR`. It is saved only when it loads."), withHandler(() => flatMap$1(configFile, (config) => editConfig({ config })).pipe(printProblems)));
72123
+ const pathCommand = make$1("path").pipe(withDescription("Print the config's path."), withHandler(() => flatMap$1(configFile, (config) => printPath({ config }))));
72124
+ const configTree = configCommand.pipe(withSubcommands([
72125
+ addCommand,
72126
+ removeCommand,
72127
+ editCommand,
72128
+ pathCommand
72129
+ ]));
70238
72130
  //#endregion
70239
72131
  //#region src/bin.ts
70240
72132
  run(make$1("fizz").pipe(withDescription("Keep every Machine on your tailnet in line with one config."), withSubcommands([
72133
+ initCommand,
70241
72134
  shipCommand,
72135
+ statusCommand,
70242
72136
  reportCommand,
70243
- logsCommand
72137
+ logsCommand,
72138
+ configTree
70244
72139
  ])), { version }).pipe(provide(layer$2), runMain);
70245
72140
  //#endregion
70246
72141
  export {};