fleetfizz 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/fleetfizz.mjs +1925 -960
  2. package/package.json +2 -1
  3. package/schema.json +1340 -71
@@ -4299,6 +4299,18 @@ const findFirst$1 = /*#__PURE__*/ dual(2, (self, f) => {
4299
4299
  //#endregion
4300
4300
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Array.js
4301
4301
  /**
4302
+ * Works with JavaScript arrays, readonly arrays, and non-empty arrays.
4303
+ *
4304
+ * The helpers cover common collection work such as creating arrays, reading
4305
+ * elements, transforming values, sorting, grouping, splitting, combining, and
4306
+ * reducing many values to one result. Helpers that change contents return new
4307
+ * arrays and preserve non-empty array types when the result is guaranteed to
4308
+ * contain values.
4309
+ *
4310
+ * @stability stable
4311
+ * @since 2.0.0
4312
+ */
4313
+ /**
4302
4314
  * Exposes the global array constructor.
4303
4315
  *
4304
4316
  * **When to use**
@@ -4562,6 +4574,17 @@ const findLast = /*#__PURE__*/ dual(2, (self, f) => {
4562
4574
  }
4563
4575
  return none();
4564
4576
  });
4577
+ const hashBucketsAdd = (buckets, value) => {
4578
+ const hash$1 = hash(value);
4579
+ const bucket = buckets.get(hash$1);
4580
+ if (bucket === void 0) {
4581
+ buckets.set(hash$1, [value]);
4582
+ return true;
4583
+ }
4584
+ for (const previous of bucket) if (equals$1(previous, value)) return false;
4585
+ bucket.push(value);
4586
+ return true;
4587
+ };
4565
4588
  /**
4566
4589
  * Creates an empty array.
4567
4590
  *
@@ -4658,6 +4681,38 @@ const map$8 = /*#__PURE__*/ dual(2, (self, f) => self.map(f));
4658
4681
  * @since 2.0.0
4659
4682
  */
4660
4683
  const reduce = /*#__PURE__*/ dual(3, (self, b, f) => fromIterable$1(self).reduce((b, a, i) => f(b, a, i), b));
4684
+ /**
4685
+ * Removes duplicates using `Equal.equivalence()`, preserving the order of the
4686
+ * first occurrence.
4687
+ *
4688
+ * **When to use**
4689
+ *
4690
+ * Use to remove repeated values from an iterable when Effect's default equality
4691
+ * is the right comparison, preserving the first occurrence.
4692
+ *
4693
+ * **Example** (Removing duplicates)
4694
+ *
4695
+ * ```ts import.meta.vitest
4696
+ * import { Array } from "effect"
4697
+ *
4698
+ * Array.dedupe([1, 2, 1, 3, 2, 4]) // => [1, 2, 3, 4]
4699
+ * ```
4700
+ *
4701
+ * @see {@link dedupeWith} — use custom equality
4702
+ * @see {@link dedupeAdjacent} — only dedupes consecutive elements
4703
+ *
4704
+ * @stability stable
4705
+ * @category deduplication
4706
+ * @since 2.0.0
4707
+ */
4708
+ const dedupe = (self) => {
4709
+ const input = fromIterable$1(self);
4710
+ if (input.length < 2) return [...input];
4711
+ const buckets = /* @__PURE__ */ new Map();
4712
+ const out = [];
4713
+ for (const value of input) if (hashBucketsAdd(buckets, value)) out.push(value);
4714
+ return out;
4715
+ };
4661
4716
  //#endregion
4662
4717
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Duration.js
4663
4718
  const TypeId$31 = "~effect/Duration";
@@ -20340,13 +20395,13 @@ function canPreserveEncodingChecks(ast) {
20340
20395
  * @category transforming
20341
20396
  * @since 4.0.0
20342
20397
  */
20343
- const toType = /*#__PURE__*/ memoizeIdempotent((ast) => {
20398
+ const toType$1 = /*#__PURE__*/ memoizeIdempotent((ast) => {
20344
20399
  const owner = getContextOwner(ast);
20345
20400
  if (owner !== ast) {
20346
- const type = toType(owner);
20401
+ const type = toType$1(owner);
20347
20402
  return type === owner && ast.encoding === void 0 ? ast : replaceContext(type, ast.context);
20348
20403
  }
20349
- const type = "recur" in ast ? ast.recur(toType) : ast;
20404
+ const type = "recur" in ast ? ast.recur(toType$1) : ast;
20350
20405
  if ("encodingChecks" in type && type.encodingChecks) {
20351
20406
  const checks = canPreserveEncodingChecks(ast) ? type.encodingChecks : isArrays(type) || isObjects(type) || isDeclaration(type) && type.typeParameters.length > 0 ? extractStructuralChecks(type.encodingChecks) : void 0;
20352
20407
  return copy$1(type, {
@@ -20384,7 +20439,7 @@ const toType = /*#__PURE__*/ memoizeIdempotent((ast) => {
20384
20439
  * @since 4.0.0
20385
20440
  */
20386
20441
  const toEncoded = /*#__PURE__*/ memoizeIdempotent((ast) => {
20387
- return toType(flip(ast));
20442
+ return toType$1(flip(ast));
20388
20443
  });
20389
20444
  function flipEncoding(ast, encoding) {
20390
20445
  const links = encoding;
@@ -22738,7 +22793,7 @@ function makeEffect(schema) {
22738
22793
  const ast = schema.ast;
22739
22794
  let parser;
22740
22795
  return (input, options) => {
22741
- return (parser ??= runWithCompiler(constructorCompiler, toType(ast)))(input, options?.disableChecks ? options?.parseOptions ? {
22796
+ return (parser ??= runWithCompiler(constructorCompiler, toType$1(ast)))(input, options?.disableChecks ? options?.parseOptions ? {
22742
22797
  ...options.parseOptions,
22743
22798
  disableChecks: true
22744
22799
  } : { disableChecks: true } : options?.parseOptions);
@@ -22799,7 +22854,7 @@ function makeOption(schema) {
22799
22854
  * @since 4.0.0
22800
22855
  */
22801
22856
  function make$5(schema) {
22802
- return makeConstructorSync(toType(schema.ast));
22857
+ return makeConstructorSync(toType$1(schema.ast));
22803
22858
  }
22804
22859
  /**
22805
22860
  * Creates a type guard that checks whether an input satisfies the schema's decoded
@@ -22841,7 +22896,7 @@ function makeIs(ast) {
22841
22896
  }
22842
22897
  /** @internal */
22843
22898
  function _is(ast) {
22844
- const typeAST = toType(ast);
22899
+ const typeAST = toType$1(ast);
22845
22900
  let guard = (input) => {
22846
22901
  guard = makeIs(typeAST);
22847
22902
  return guard(input);
@@ -22876,6 +22931,10 @@ function decodeUnknownEffect$1(schema, options) {
22876
22931
  const parser = run$3(schema.ast);
22877
22932
  return options === void 0 ? parser : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions));
22878
22933
  }
22934
+ /** @internal */
22935
+ function decodeUnknownOption$1(schema, options) {
22936
+ return asOption(decodeUnknownEffect$1(schema, options));
22937
+ }
22879
22938
  /**
22880
22939
  * Creates a decoder for `unknown` input that reports failure safely as a
22881
22940
  * `Result`.
@@ -22961,6 +23020,16 @@ function runWithCompiler(compiler, ast) {
22961
23020
  function asExit(parser) {
22962
23021
  return (input, options) => runSyncExit(parser(input, options));
22963
23022
  }
23023
+ /** @internal */
23024
+ function asOption(parser) {
23025
+ const parserExit = asExit(parser);
23026
+ return (input, options) => {
23027
+ const exit = parserExit(input, options);
23028
+ if (isSuccess(exit)) return some(exit.value);
23029
+ getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues");
23030
+ return none();
23031
+ };
23032
+ }
22964
23033
  function asResult(parser) {
22965
23034
  const parserExit = asExit(parser);
22966
23035
  return (input, options) => {
@@ -24086,7 +24155,7 @@ function toRepresentations(asts, options) {
24086
24155
  }
24087
24156
  function visitChecks(checks) {
24088
24157
  checks?.forEach((check) => {
24089
- check.annotations?.representation?.schemas?.forEach((schema) => visit(toType(schema)));
24158
+ check.annotations?.representation?.schemas?.forEach((schema) => visit(toType$1(schema)));
24090
24159
  if (check._tag === "FilterGroup") visitChecks(check.checks);
24091
24160
  });
24092
24161
  }
@@ -24226,7 +24295,7 @@ function toRepresentations(asts, options) {
24226
24295
  const { representation, ...ordinary } = annotations;
24227
24296
  const projected = representation === void 0 ? void 0 : representation.schemas === void 0 ? representation : {
24228
24297
  ...representation,
24229
- schemas: representation.schemas.map((schema) => recur(toType(schema)))
24298
+ schemas: representation.schemas.map((schema) => recur(toType$1(schema)))
24230
24299
  };
24231
24300
  return {
24232
24301
  ...projected === void 0 ? void 0 : { representation: projected },
@@ -24282,6 +24351,20 @@ function isSchemaError$1(u) {
24282
24351
  }
24283
24352
  //#endregion
24284
24353
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Schema.js
24354
+ /**
24355
+ * Describes data shapes and how unknown input becomes trusted values.
24356
+ *
24357
+ * A schema can validate input, decode it into an application type, and encode
24358
+ * that value back to another representation. This module contains the main
24359
+ * schema, codec, decoder, and encoder APIs, together with schemas for common
24360
+ * JavaScript values and Effect data types. It also supports refinements,
24361
+ * transformations, defaults, classes, JSON Schema generation, test data
24362
+ * generation, formatting, equivalence, optics, and differs derived from schema
24363
+ * definitions.
24364
+ *
24365
+ * @stability stable
24366
+ * @since 4.0.0
24367
+ */
24285
24368
  const TypeId$5 = TypeId$6;
24286
24369
  /**
24287
24370
  * Error thrown or returned when schema decoding or encoding fails.
@@ -24468,6 +24551,34 @@ function decodeUnknownEffect(schema, options) {
24468
24551
  */
24469
24552
  const decodeEffect = decodeUnknownEffect;
24470
24553
  /**
24554
+ * Decodes an `unknown` input against a schema, returning an `Option` that is
24555
+ * `Some` with the decoded value on success or `None` for schema mismatches.
24556
+ *
24557
+ * **When to use**
24558
+ *
24559
+ * Use when you do not know the input type statically and only need to know
24560
+ * whether decoding succeeded.
24561
+ *
24562
+ * **Details**
24563
+ *
24564
+ * Prefer this over {@link decodeUnknownExit} or {@link decodeUnknownEffect}
24565
+ * when you don't need error details. For input already typed as the schema's
24566
+ * `Encoded` type use {@link decodeOption}.
24567
+ * Options may be provided either when creating the decoder or when applying it;
24568
+ * application options override creation options.
24569
+ *
24570
+ * **Gotchas**
24571
+ *
24572
+ * Only causes made entirely of schema issues are converted to `None`. Causes
24573
+ * that contain defects, interruptions, or other non-schema reasons throw
24574
+ * instead.
24575
+ *
24576
+ * @stability stable
24577
+ * @category decoding
24578
+ * @since 3.10.0
24579
+ */
24580
+ const decodeUnknownOption = decodeUnknownOption$1;
24581
+ /**
24471
24582
  * Decodes an `unknown` input against a schema, returning a `Result` that
24472
24583
  * succeeds with the decoded value or fails with a {@link SchemaError} for schema
24473
24584
  * mismatches.
@@ -24711,6 +24822,15 @@ const optional$2 = /*#__PURE__*/ lambda((self) => {
24711
24822
  return make$3(optional$3(self.ast), { schema });
24712
24823
  });
24713
24824
  /**
24825
+ * Extracts the type-side schema: sets `Encoded` to equal the decoded `Type`,
24826
+ * discarding the encoding transformation path.
24827
+ *
24828
+ * @stability stable
24829
+ * @category transforming
24830
+ * @since 4.0.0
24831
+ */
24832
+ const toType = /*#__PURE__*/ lambda((schema) => make$3(toType$1(schema.ast), { schema }));
24833
+ /**
24714
24834
  * Creates a schema for a single literal value (string, number, bigint, boolean, or null).
24715
24835
  *
24716
24836
  * **Example** (Defining a string literal)
@@ -25244,6 +25364,45 @@ function isPattern(regExp, annotations) {
25244
25364
  ...annotations
25245
25365
  });
25246
25366
  }
25367
+ /**
25368
+ * Validates that a value has at least the specified length. Works with strings
25369
+ * and arrays.
25370
+ *
25371
+ * **Details**
25372
+ *
25373
+ * JSON Schema:
25374
+ *
25375
+ * The bound must be finite; it is rounded down and clamped to zero. For arrays,
25376
+ * this check corresponds to `minItems`. JavaScript counts UTF-16 code units
25377
+ * while JSON Schema counts Unicode code points, so strings use
25378
+ * `minLength: Math.ceil(minLength / 2)`, the tightest lower bound that cannot
25379
+ * reject a string accepted by this check.
25380
+ *
25381
+ * Arbitrary:
25382
+ *
25383
+ * During arbitrary generation, this applies a `minLength`
25384
+ * constraint to ensure generated strings or arrays have at least the required
25385
+ * length.
25386
+ *
25387
+ * **Example** (Checking minimum length)
25388
+ *
25389
+ * ```ts import.meta.vitest
25390
+ * import { Schema } from "effect"
25391
+ *
25392
+ * const NonEmptyStringSchema = Schema.String.check(Schema.isMinLength(1))
25393
+ * const NonEmptyArraySchema = Schema.Array(Schema.Number).check(Schema.isMinLength(1))
25394
+ * Schema.is(NonEmptyStringSchema)("a") // => true
25395
+ * Schema.is(NonEmptyArraySchema)([1]) // => true
25396
+ * ```
25397
+ *
25398
+ * @stability stable
25399
+ * @category validation
25400
+ * @since 4.0.0
25401
+ */
25402
+ function isMinLength(minLength, annotations) {
25403
+ minLength = normalizeCardinality(minLength);
25404
+ return makeIsMinLength(minLength, Math.ceil(minLength / 2), annotations);
25405
+ }
25247
25406
  function makeIsMinLength(minLength, minCodePoints, annotations) {
25248
25407
  return makeFilter((input) => input.length >= minLength, {
25249
25408
  expected: `a value with a length of at least ${minLength}`,
@@ -25284,6 +25443,39 @@ function makeIsMinLength(minLength, minCodePoints, annotations) {
25284
25443
  function isNonEmpty(annotations) {
25285
25444
  return makeIsMinLength(1, 1, annotations);
25286
25445
  }
25446
+ function normalizeCardinality(value) {
25447
+ if (!globalThis.Number.isFinite(value)) throw new globalThis.RangeError(`Expected a finite number, got ${value}`);
25448
+ return Math.max(0, Math.floor(value));
25449
+ }
25450
+ /**
25451
+ * Validates that all items in an array are unique according to Effect equality.
25452
+ *
25453
+ * **Details**
25454
+ *
25455
+ * JSON Schema:
25456
+ * This check corresponds to the `uniqueItems: true` constraint in JSON Schema.
25457
+ *
25458
+ * Arbitrary:
25459
+ * During arbitrary generation, this applies a node-local identity selector
25460
+ * for constructive uniqueness using Effect equality.
25461
+ *
25462
+ * @stability stable
25463
+ * @category validation
25464
+ * @since 4.0.0
25465
+ */
25466
+ function isUnique(annotations) {
25467
+ return makeFilter((input) => dedupe(input).length === input.length, {
25468
+ expected: "an array with unique items",
25469
+ representation: {
25470
+ id: "effect/schema/isUnique",
25471
+ payload: null
25472
+ },
25473
+ toJsonSchema: () => ({ uniqueItems: true }),
25474
+ toCode: () => ({ runtime: "Schema.isUnique()" }),
25475
+ arbitraryConstraint: { uniqueBy: identity },
25476
+ ...annotations
25477
+ });
25478
+ }
25287
25479
  /**
25288
25480
  * Schema for non-empty strings. Validates that a string has at least one
25289
25481
  * character.
@@ -25893,145 +26085,6 @@ function defaultShouldQuit(input) {
25893
26085
  */
25894
26086
  const layer = /*#__PURE__*/ provideMerge(layer$11, /*#__PURE__*/ mergeAll(layer$7, layer$9, layer$5, layer$3, layer$2));
25895
26087
  //#endregion
25896
- //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Console.js
25897
- /**
25898
- * Context reference for the current console service in the Effect system, allowing access to the active console implementation from within the Effect context.
25899
- *
25900
- * **When to use**
25901
- *
25902
- * Use when you need an effect to run against a provided console implementation,
25903
- * such as tests or alternate runtimes, rather than the default console.
25904
- *
25905
- * **Details**
25906
- *
25907
- * When no override is provided, the reference resolves to `globalThis.console`.
25908
- *
25909
- * **Example** (Accessing the current console)
25910
- *
25911
- * ```ts import.meta.vitest
25912
- * import { Console, Effect } from "effect"
25913
- *
25914
- * const messages: Array<unknown> = []
25915
- * const testConsole: Console.Console = Object.assign(Object.create(console), {
25916
- * log: (...args: ReadonlyArray<unknown>) => messages.push(...args)
25917
- * })
25918
- * const program = Console.consoleWith((console) =>
25919
- * Effect.sync(() => {
25920
- * console.log("Hello from current console!")
25921
- * })
25922
- * )
25923
- *
25924
- * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
25925
- * messages // => ["Hello from current console!"]
25926
- * ```
25927
- *
25928
- * @see {@link consoleWith} for using the current console service inside an effect
25929
- *
25930
- * @stability stable
25931
- * @category services
25932
- * @since 2.0.0
25933
- */
25934
- const Console = ConsoleRef;
25935
- /**
25936
- * Creates an Effect that provides access to the current console service and lets you perform operations with it within an Effect context.
25937
- *
25938
- * **Example** (Accessing the current console service)
25939
- *
25940
- * ```ts import.meta.vitest
25941
- * import { Console, Effect } from "effect"
25942
- *
25943
- * const messages: Array<unknown> = []
25944
- * const testConsole: Console.Console = Object.assign(Object.create(console), {
25945
- * log: (...args: ReadonlyArray<unknown>) => messages.push(...args),
25946
- * error: (...args: ReadonlyArray<unknown>) => messages.push(...args)
25947
- * })
25948
- * const program = Console.consoleWith((console) =>
25949
- * Effect.sync(() => {
25950
- * console.log("Hello, world!")
25951
- * console.error("This is an error message")
25952
- * })
25953
- * )
25954
- *
25955
- * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
25956
- * messages // => ["Hello, world!", "This is an error message"]
25957
- * ```
25958
- *
25959
- * @stability stable
25960
- * @category constructors
25961
- * @since 2.0.0
25962
- */
25963
- const consoleWith = (f) => withFiber$1((fiber) => f(fiber.getRef(Console)));
25964
- /**
25965
- * Writes an error-level message to the console, typically displayed with error
25966
- * styling by the active console implementation.
25967
- *
25968
- * **Example** (Writing error messages)
25969
- *
25970
- * ```ts import.meta.vitest
25971
- * import { Console, Effect } from "effect"
25972
- *
25973
- * const messages: Array<ReadonlyArray<unknown>> = []
25974
- * const testConsole: Console.Console = Object.assign(Object.create(console), {
25975
- * error: (...args: ReadonlyArray<unknown>) => messages.push(args)
25976
- * })
25977
- * const program = Effect.gen(function*() {
25978
- * yield* Console.error("Something went wrong!")
25979
- * yield* Console.error("Error details:", {
25980
- * code: 500,
25981
- * message: "Internal Server Error"
25982
- * })
25983
- * })
25984
- *
25985
- * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
25986
- * const expected = [
25987
- * ["Something went wrong!"],
25988
- * ["Error details:", { code: 500, message: "Internal Server Error" }]
25989
- * ]
25990
- * messages // => expected
25991
- * ```
25992
- *
25993
- * @stability stable
25994
- * @category accessors
25995
- * @since 2.0.0
25996
- */
25997
- const error = (...args) => consoleWith((console) => sync$1(() => {
25998
- console.error(...args);
25999
- }));
26000
- /**
26001
- * Logs a general-purpose message to the console.
26002
- *
26003
- * **Example** (Writing log messages)
26004
- *
26005
- * ```ts import.meta.vitest
26006
- * import { Console, Effect } from "effect"
26007
- *
26008
- * const messages: Array<ReadonlyArray<unknown>> = []
26009
- * const testConsole: Console.Console = Object.assign(Object.create(console), {
26010
- * log: (...args: ReadonlyArray<unknown>) => messages.push(args)
26011
- * })
26012
- * const program = Effect.gen(function*() {
26013
- * yield* Console.log("Hello, world!")
26014
- * yield* Console.log("User data:", { name: "John", age: 30 })
26015
- * yield* Console.log("Processing", 42, "items")
26016
- * })
26017
- *
26018
- * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
26019
- * const expected = [
26020
- * ["Hello, world!"],
26021
- * ["User data:", { name: "John", age: 30 }],
26022
- * ["Processing", 42, "items"]
26023
- * ]
26024
- * messages // => expected
26025
- * ```
26026
- *
26027
- * @stability stable
26028
- * @category accessors
26029
- * @since 2.0.0
26030
- */
26031
- const log = (...args) => consoleWith((console) => sync$1(() => {
26032
- console.log(...args);
26033
- }));
26034
- //#endregion
26035
26088
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/cli/CliError.js
26036
26089
  /**
26037
26090
  * Defines structured errors for the unstable CLI parser and runner.
@@ -29124,6 +29177,145 @@ const getParamMetadata = (param) => {
29124
29177
  });
29125
29178
  };
29126
29179
  //#endregion
29180
+ //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Console.js
29181
+ /**
29182
+ * Context reference for the current console service in the Effect system, allowing access to the active console implementation from within the Effect context.
29183
+ *
29184
+ * **When to use**
29185
+ *
29186
+ * Use when you need an effect to run against a provided console implementation,
29187
+ * such as tests or alternate runtimes, rather than the default console.
29188
+ *
29189
+ * **Details**
29190
+ *
29191
+ * When no override is provided, the reference resolves to `globalThis.console`.
29192
+ *
29193
+ * **Example** (Accessing the current console)
29194
+ *
29195
+ * ```ts import.meta.vitest
29196
+ * import { Console, Effect } from "effect"
29197
+ *
29198
+ * const messages: Array<unknown> = []
29199
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29200
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(...args)
29201
+ * })
29202
+ * const program = Console.consoleWith((console) =>
29203
+ * Effect.sync(() => {
29204
+ * console.log("Hello from current console!")
29205
+ * })
29206
+ * )
29207
+ *
29208
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29209
+ * messages // => ["Hello from current console!"]
29210
+ * ```
29211
+ *
29212
+ * @see {@link consoleWith} for using the current console service inside an effect
29213
+ *
29214
+ * @stability stable
29215
+ * @category services
29216
+ * @since 2.0.0
29217
+ */
29218
+ const Console = ConsoleRef;
29219
+ /**
29220
+ * Creates an Effect that provides access to the current console service and lets you perform operations with it within an Effect context.
29221
+ *
29222
+ * **Example** (Accessing the current console service)
29223
+ *
29224
+ * ```ts import.meta.vitest
29225
+ * import { Console, Effect } from "effect"
29226
+ *
29227
+ * const messages: Array<unknown> = []
29228
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29229
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(...args),
29230
+ * error: (...args: ReadonlyArray<unknown>) => messages.push(...args)
29231
+ * })
29232
+ * const program = Console.consoleWith((console) =>
29233
+ * Effect.sync(() => {
29234
+ * console.log("Hello, world!")
29235
+ * console.error("This is an error message")
29236
+ * })
29237
+ * )
29238
+ *
29239
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29240
+ * messages // => ["Hello, world!", "This is an error message"]
29241
+ * ```
29242
+ *
29243
+ * @stability stable
29244
+ * @category constructors
29245
+ * @since 2.0.0
29246
+ */
29247
+ const consoleWith = (f) => withFiber$1((fiber) => f(fiber.getRef(Console)));
29248
+ /**
29249
+ * Writes an error-level message to the console, typically displayed with error
29250
+ * styling by the active console implementation.
29251
+ *
29252
+ * **Example** (Writing error messages)
29253
+ *
29254
+ * ```ts import.meta.vitest
29255
+ * import { Console, Effect } from "effect"
29256
+ *
29257
+ * const messages: Array<ReadonlyArray<unknown>> = []
29258
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29259
+ * error: (...args: ReadonlyArray<unknown>) => messages.push(args)
29260
+ * })
29261
+ * const program = Effect.gen(function*() {
29262
+ * yield* Console.error("Something went wrong!")
29263
+ * yield* Console.error("Error details:", {
29264
+ * code: 500,
29265
+ * message: "Internal Server Error"
29266
+ * })
29267
+ * })
29268
+ *
29269
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29270
+ * const expected = [
29271
+ * ["Something went wrong!"],
29272
+ * ["Error details:", { code: 500, message: "Internal Server Error" }]
29273
+ * ]
29274
+ * messages // => expected
29275
+ * ```
29276
+ *
29277
+ * @stability stable
29278
+ * @category accessors
29279
+ * @since 2.0.0
29280
+ */
29281
+ const error = (...args) => consoleWith((console) => sync$1(() => {
29282
+ console.error(...args);
29283
+ }));
29284
+ /**
29285
+ * Logs a general-purpose message to the console.
29286
+ *
29287
+ * **Example** (Writing log messages)
29288
+ *
29289
+ * ```ts import.meta.vitest
29290
+ * import { Console, Effect } from "effect"
29291
+ *
29292
+ * const messages: Array<ReadonlyArray<unknown>> = []
29293
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29294
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(args)
29295
+ * })
29296
+ * const program = Effect.gen(function*() {
29297
+ * yield* Console.log("Hello, world!")
29298
+ * yield* Console.log("User data:", { name: "John", age: 30 })
29299
+ * yield* Console.log("Processing", 42, "items")
29300
+ * })
29301
+ *
29302
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29303
+ * const expected = [
29304
+ * ["Hello, world!"],
29305
+ * ["User data:", { name: "John", age: 30 }],
29306
+ * ["Processing", 42, "items"]
29307
+ * ]
29308
+ * messages // => expected
29309
+ * ```
29310
+ *
29311
+ * @stability stable
29312
+ * @category accessors
29313
+ * @since 2.0.0
29314
+ */
29315
+ const log = (...args) => consoleWith((console) => sync$1(() => {
29316
+ console.log(...args);
29317
+ }));
29318
+ //#endregion
29127
29319
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/cli/CliOutput.js
29128
29320
  /**
29129
29321
  * Formats CLI help and errors as text.
@@ -32440,7 +32632,7 @@ const runWith = (command, config) => {
32440
32632
  };
32441
32633
  //#endregion
32442
32634
  //#region package.json
32443
- var version = "0.5.2";
32635
+ var version = "0.7.0";
32444
32636
  //#endregion
32445
32637
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
32446
32638
  /**
@@ -35097,51 +35289,48 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
35097
35289
  })))(), 1);
35098
35290
  var BumpError = class extends TaggedError()("BumpError", {
35099
35291
  path: String$5,
35100
- package: String$5,
35292
+ at: String$5,
35101
35293
  reason: String$5
35102
35294
  }) {
35103
35295
  get message() {
35104
- return `${this.path}: cannot bump ${this.package}: ${this.reason}`;
35296
+ return `${this.path}: cannot bump ${this.at}: ${this.reason}`;
35105
35297
  }
35106
35298
  };
35107
- const rangePath = (pkg) => pkg === "vite-plus" ? ["vp", "version"] : pkg === "node" ? ["vp", "node"] : [
35108
- "vp",
35109
- "globals",
35110
- pkg
35111
- ];
35112
35299
  const bumpedRange = (current, installed) => import_semver.valid(current) !== null ? installed : `^${installed}`;
35113
- const bumpText = (text, pkg, installed) => {
35114
- const path = rangePath(pkg);
35115
- const node = findNodeAtLocation(parseTree(text) ?? {
35300
+ const bumpText = (text, path, installed) => {
35301
+ const tree = parseTree(text) ?? {
35116
35302
  type: "null",
35117
35303
  offset: 0,
35118
35304
  length: 0
35119
- }, path);
35305
+ };
35306
+ const shorthand = path.slice(0, -1);
35307
+ const at = findNodeAtLocation(tree, [...path]) === void 0 && findNodeAtLocation(tree, shorthand)?.type === "string" ? shorthand : path;
35308
+ const node = findNodeAtLocation(tree, [...at]);
35120
35309
  if (node === void 0 || node.type !== "string" || typeof node.value !== "string") return { reason: `${path.join(".")} is not a string literal; change it by hand` };
35121
35310
  const range = bumpedRange(node.value, installed);
35122
35311
  return {
35123
- text: applyEdits(text, modify(text, path, range, { formattingOptions: {
35312
+ text: applyEdits(text, modify(text, [...at], range, { formattingOptions: {
35124
35313
  insertSpaces: true,
35125
35314
  tabSize: 2
35126
35315
  } })),
35127
35316
  range
35128
35317
  };
35129
35318
  };
35130
- const bumpConfig = fn("bumpConfig")(function* (configPath, pkg, installed) {
35319
+ const bumpConfig = fn("bumpConfig")(function* (configPath, path, installed) {
35131
35320
  const fs = yield* FileSystem;
35132
35321
  const text = yield* fs.readFileString(configPath);
35133
- const result = bumpText(text, pkg, installed);
35322
+ const result = bumpText(text, path, installed);
35134
35323
  if ("reason" in result) return yield* new BumpError({
35135
35324
  path: configPath,
35136
- package: pkg,
35325
+ at: path.join("."),
35137
35326
  reason: result.reason
35138
35327
  });
35139
35328
  yield* fs.writeFileString(configPath, result.text);
35140
35329
  return result.range;
35141
35330
  });
35142
35331
  const Targeting = {
35143
- platforms: optionalKey(NonEmptyArray(Literals$2(["darwin", "linux"]))),
35144
- machines: optionalKey(NonEmptyArray(NonEmptyString))
35332
+ platforms: optionalKey(NonEmptyArray(Literals$2(["darwin", "linux"]))).annotateKey({ description: "`darwin`, `linux`, or both." }),
35333
+ machines: optionalKey(NonEmptyArray(NonEmptyString)).annotateKey({ description: "Machine names from `machines`." })
35145
35334
  };
35146
35335
  const Origin = String$5.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35147
35336
  title: "GitHub origin",
@@ -35149,39 +35338,102 @@ const Origin = String$5.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35149
35338
  }));
35150
35339
  const Ref = NonEmptyString;
35151
35340
  const isPin = (ref) => /^[0-9a-f]{40}$/.test(ref);
35341
+ const ProviderName = Literals$2([
35342
+ "vp",
35343
+ "brew",
35344
+ "cask",
35345
+ "repos"
35346
+ ]);
35152
35347
  const Range = NonEmptyString.check(makeFilter((range) => range === "latest" || import_semver.validRange(range) !== null, {
35153
35348
  title: "Range",
35154
- description: "a semver range, \"*\", or \"latest\""
35349
+ 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)"
35155
35350
  }));
35156
35351
  const TrackedRepo = Struct({
35157
35352
  ...Targeting,
35158
- origin: Origin,
35159
- ref: Ref,
35160
- install: NonEmptyString,
35161
- name: optionalKey(NonEmptyString),
35162
- outputs: optionalKey(ArraySchema(NonEmptyString)),
35163
- files: optionalKey(Record(NonEmptyString, String$5))
35353
+ origin: Origin.annotateKey({ description: "The GitHub `owner/repo`." }),
35354
+ ref: Ref.annotateKey({ description: "A branch, or a full 40-character commit SHA to pin it." }),
35355
+ install: NonEmptyString.annotateKey({ description: "The Install Command, run in a login shell inside the Managed Clone." }),
35356
+ outputs: optionalKey(ArraySchema(NonEmptyString)).annotateKey({ description: "Paths outside the clone the Install Command produces. If one changes by hand, the repo reinstalls." }),
35357
+ files: optionalKey(Record(NonEmptyString, String$5)).annotateKey({ description: "Gitignored files (Clone Files) written into the clone before every install, keyed by path. Never secrets." })
35358
+ });
35359
+ const withShorthand = (entry, value, shorthand) => Union([entry, value.pipe(decodeTo(toType(entry), transform$1(shorthand)))]);
35360
+ const variants = (entry) => Union([entry, NonEmptyArray(entry)]);
35361
+ const dependsOn = optionalKey(ArraySchema(ProviderName)).annotateKey({ description: "Providers whose Items must succeed on a Machine before this one changes there." });
35362
+ const section = (settings, items, description) => Struct({
35363
+ ...Targeting,
35364
+ dependsOn,
35365
+ ...settings,
35366
+ items: optionalKey(items).annotateKey({ description })
35164
35367
  });
35165
- const VpSection = Struct({
35368
+ const VARIANTS = "or a list of Variants, of which a Machine gets the first that targets it";
35369
+ const Global = withShorthand(Struct({
35166
35370
  ...Targeting,
35167
- version: optionalKey(Range),
35168
- node: optionalKey(Range),
35169
- globals: optionalKey(Record(NonEmptyString, Range))
35371
+ range: Range
35372
+ }), Range, {
35373
+ decode: (range) => ({ range }),
35374
+ encode: (global) => global.range
35170
35375
  });
35171
- const MachineSettings = Struct({ clones: optionalKey(NonEmptyString) });
35376
+ const PACKAGE_MANAGERS = [
35377
+ "npm",
35378
+ "pnpm",
35379
+ "yarn",
35380
+ "bun"
35381
+ ];
35382
+ const NOT_GLOBALS = {
35383
+ vp: "the vp Item is vp itself; set it with vp.version",
35384
+ mode: "the mode Item is vp's managed mode",
35385
+ node: "set Node with vp.node",
35386
+ ...Object.fromEntries(PACKAGE_MANAGERS.map((pm) => [pm, `set ${pm} with vp.${pm}`])),
35387
+ "vite-plus": "set vp with vp.version",
35388
+ fleetfizz: "a Ship installs the fleetfizz that ships"
35389
+ };
35390
+ const VpSection = section({
35391
+ version: optionalKey(Range).annotateKey({ description: "The vp version. Pin it exactly." }),
35392
+ node: optionalKey(Range).annotateKey({ description: "The default Node version. Pin it exactly." }),
35393
+ npm: optionalKey(Range).annotateKey({ description: "The default npm version." }),
35394
+ pnpm: optionalKey(Range).annotateKey({ description: "The default pnpm version." }),
35395
+ yarn: optionalKey(Range).annotateKey({ description: "The default Yarn version." }),
35396
+ bun: optionalKey(Range).annotateKey({ description: "The default Bun version." })
35397
+ }, Record(NonEmptyString, variants(Global)).check(makeFilter((globals) => {
35398
+ const name = Object.keys(globals).find((n) => n in NOT_GLOBALS);
35399
+ return name === void 0 || `"${name}" is not a Global: ${NOT_GLOBALS[name]}`;
35400
+ }, {
35401
+ title: "Globals",
35402
+ description: "npm packages other than vp, node, a package manager, vite-plus, and fleetfizz"
35403
+ })), `Globals, keyed by package. Each is a Range, \`{ range, platforms?, machines? }\` to target it, ${VARIANTS}.`);
35404
+ const HomebrewRange = Literals$2(["*", "latest"]).annotate({ description: "\"*\" (install when missing, never upgrade) or \"latest\" (brew update, then upgrade on every Ship)" });
35405
+ const HomebrewItems = Record(NonEmptyString, variants(withShorthand(Struct({
35406
+ ...Targeting,
35407
+ range: HomebrewRange
35408
+ }), HomebrewRange, {
35409
+ decode: (range) => ({ range }),
35410
+ encode: (entry) => entry.range
35411
+ })));
35412
+ const homebrewDescription = (noun) => `${noun}, keyed by name. Each is \`"*"\` or \`"latest"\`, \`{ range, platforms?, machines? }\` to target it, ${VARIANTS}. A tapped name (\`owner/tap/name\`) taps on install.`;
35413
+ const BrewSection = section({}, HomebrewItems.check(makeFilter((items) => !Object.hasOwn(items, "brew") || "\"brew\" is Homebrew itself", {
35414
+ title: "Formulae",
35415
+ description: "Formulae other than brew"
35416
+ })), homebrewDescription("Formulae"));
35417
+ const CaskSection = section({}, HomebrewItems, homebrewDescription("Casks"));
35418
+ const DEFAULT_CLONE_ROOT = "~/.local/share/fleetfizz/clones";
35419
+ const ReposSection = section({ cloneRoot: optionalKey(variants(withShorthand(Struct({
35420
+ ...Targeting,
35421
+ path: NonEmptyString
35422
+ }), NonEmptyString, {
35423
+ decode: (path) => ({ path }),
35424
+ encode: (root) => root.path
35425
+ }))).annotateKey({ description: `Where a Machine keeps its Managed Clones: a path, or a list of \`{ path, platforms?, machines? }\` where a Machine gets the first that targets it. \`~\` expands on that Machine. Default: \`${DEFAULT_CLONE_ROOT}\`.` }) }, Record(NonEmptyString, variants(TrackedRepo)), `Tracked Repos, keyed by name, which names the Managed Clone and the \`repos:<name>\` Item. Each is \`{ origin, ref, install, ... }\`, ${VARIANTS}.`);
35172
35426
  const FleetConfig = Struct({
35173
35427
  $schema: optionalKey(String$5),
35174
35428
  shippedAt: optionalKey(String$5),
35175
35429
  shippedFrom: optionalKey(NonEmptyString),
35176
35430
  shippedTo: optionalKey(Record(NonEmptyString, String$5)),
35177
- machines: Record(NonEmptyString, MachineSettings).check(makeFilter((machines) => Object.keys(machines).length > 0, {
35178
- title: "Fleet",
35179
- description: "at least one Machine"
35180
- })),
35181
- repos: optionalKey(ArraySchema(TrackedRepo)),
35182
- vp: optionalKey(VpSection)
35431
+ machines: ArraySchema(NonEmptyString).check(isMinLength(1, { message: "list at least one Machine" }), isUnique({ message: "list each Machine once" })).annotateKey({ description: "The Fleet: each Machine's tailnet name. A tailnet member that is not listed is not part of the Fleet." }),
35432
+ repos: optionalKey(ReposSection).annotateKey({ description: "Each Tracked Repo is cloned on every targeted Machine and installed from its origin ref." }),
35433
+ vp: optionalKey(VpSection).annotateKey({ description: "The vite-plus toolchain." }),
35434
+ brew: optionalKey(BrewSection).annotateKey({ description: "Homebrew Formulae. Needs Homebrew on the Machine; a Ship never installs it." }),
35435
+ cask: optionalKey(CaskSection).annotateKey({ description: "Homebrew Casks, macOS only. Needs Homebrew on the Machine." })
35183
35436
  });
35184
- const repoName = (repo) => repo.name ?? repo.origin.replace("/", "-");
35185
35437
  const SELF_PACKAGE = "fleetfizz";
35186
35438
  //#endregion
35187
35439
  //#region ../../packages/config/src/jsonSchema.ts
@@ -35214,17 +35466,33 @@ var ConfigNotFound = class extends TaggedError()("ConfigNotFound", { path: Strin
35214
35466
  }
35215
35467
  };
35216
35468
  const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
35217
- const semanticProblems = (config) => {
35469
+ const repeatedKeys = (text) => {
35218
35470
  const problems = [];
35219
- const seen = /* @__PURE__ */ new Set();
35220
- for (const repo of config.repos ?? []) {
35221
- const name = repoName(repo);
35222
- if (seen.has(name)) problems.push(`repos: duplicate name "${name}"`);
35223
- seen.add(name);
35224
- }
35225
- if (config.vp?.globals?.["fleetfizz"] !== void 0) problems.push(`vp.globals: ${SELF_PACKAGE} is pinned by the running version; remove it`);
35471
+ const visit = (node, path) => {
35472
+ if (node.type === "array") node.children?.forEach((child, i) => visit(child, [...path, i]));
35473
+ if (node.type !== "object") return;
35474
+ const seen = /* @__PURE__ */ new Set();
35475
+ for (const [key, value] of (node.children ?? []).map((property) => property.children ?? [])) {
35476
+ if (key === void 0) continue;
35477
+ const name = String(key.value);
35478
+ if (seen.has(name)) problems.push(`line ${lineOf(text, key.offset)}: "${name}" is repeated${path.length > 0 ? ` in ${path.join(".")}` : ""}`);
35479
+ seen.add(name);
35480
+ if (value !== void 0) visit(value, [...path, name]);
35481
+ }
35482
+ };
35483
+ const tree = parseTree(text, void 0, { allowTrailingComma: true });
35484
+ if (tree !== void 0) visit(tree, []);
35226
35485
  return problems;
35227
35486
  };
35487
+ const movedKeys = (parsed) => {
35488
+ if (typeof parsed !== "object" || parsed === null) return [];
35489
+ const config = parsed;
35490
+ return [
35491
+ ...typeof config.machines === "object" && config.machines !== null && !Array.isArray(config.machines) ? ["machines is now a list of names, and machines.<name>.clones is repos.cloneRoot"] : [],
35492
+ ...Array.isArray(config.repos) ? ["repos is now { items: { <name>: { origin, ref, install } } }, keyed by each repo's name"] : [],
35493
+ ...typeof config.vp === "object" && config.vp !== null && "globals" in config.vp ? ["vp.globals is now vp.items"] : []
35494
+ ];
35495
+ };
35228
35496
  const parseConfig = fn("parseConfig")(function* (path, text) {
35229
35497
  const parseErrors = [];
35230
35498
  const parsed = parse(text, parseErrors, { allowTrailingComma: true });
@@ -35232,16 +35500,20 @@ const parseConfig = fn("parseConfig")(function* (path, text) {
35232
35500
  path,
35233
35501
  problems: parseErrors.map((e) => `line ${lineOf(text, e.offset)}: ${printParseErrorCode(e.error)}`)
35234
35502
  });
35235
- const config = yield* decodeUnknownEffect(FleetConfig, { onExcessProperty: "error" })(parsed).pipe(mapError((error) => new ConfigError({
35503
+ const moved = movedKeys(parsed);
35504
+ if (moved.length > 0) return yield* new ConfigError({
35236
35505
  path,
35237
- problems: [formatIssue(error.issue)]
35238
- })));
35239
- const problems = semanticProblems(config);
35240
- if (problems.length > 0) return yield* new ConfigError({
35506
+ problems: moved
35507
+ });
35508
+ const repeated = repeatedKeys(text);
35509
+ if (repeated.length > 0) return yield* new ConfigError({
35241
35510
  path,
35242
- problems
35511
+ problems: repeated
35243
35512
  });
35244
- return config;
35513
+ return yield* decodeUnknownEffect(FleetConfig, { onExcessProperty: "error" })(parsed).pipe(mapError((error) => new ConfigError({
35514
+ path,
35515
+ problems: [formatIssue(error.issue)]
35516
+ })));
35245
35517
  });
35246
35518
  const formatIssue = makeFormatterDefault();
35247
35519
  const loadConfig = fn("loadConfig")(function* (path) {
@@ -35283,7 +35555,7 @@ const stampText = (text, stamp) => {
35283
35555
  const machinesOf = (config) => {
35284
35556
  if (typeof config !== "object" || config === null || !("machines" in config)) return [];
35285
35557
  const value = config.machines;
35286
- return typeof value === "object" && value !== null ? Object.keys(value) : [];
35558
+ return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
35287
35559
  };
35288
35560
  const shippedToOf = (config) => {
35289
35561
  if (typeof config !== "object" || config === null || !("shippedTo" in config)) return {};
@@ -35308,8 +35580,10 @@ const vpEnv = [
35308
35580
  ].join("\n");
35309
35581
  const prelude = [
35310
35582
  vpEnv,
35583
+ `export PATH="$PATH:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin"`,
35311
35584
  `ff_json() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' | tr -d '\\n'; }`,
35312
- `ff_state() { printf 'ff-state %s\\n' "$1"; }`
35585
+ `ff_state() { printf 'ff-state %s\\n' "$1"; }`,
35586
+ `ff_at() { ff_state "{\\"provider\\":\\"$(ff_json "$1")\\",\\"fact\\":\\"at\\",\\"item\\":\\"$(ff_json "$2")\\",\\"at\\":\\"$(ff_json "$3")\\"}"; }`
35313
35587
  ].join("\n");
35314
35588
  const fingerprintFn = [
35315
35589
  "ff_fp() {",
@@ -35324,470 +35598,52 @@ const fingerprintFn = [
35324
35598
  const STATE_DIR = "~/.local/state/fleetfizz";
35325
35599
  const RECORDS_DIR = `${STATE_DIR}/records`;
35326
35600
  const CONFIG_DIR_SH = "\"${XDG_CONFIG_HOME:-$HOME/.config}/fleetfizz\"";
35601
+ const recordDir = (provider) => `${quotePath(RECORDS_DIR)}/${quote(provider)}`;
35602
+ const recordFile = (provider, item) => `${recordDir(provider)}/${quote(`${item}.json`)}`;
35603
+ const LEGACY_RECORD_PROVIDER = "repos";
35604
+ const recordFact = (provider, item) => [
35605
+ `ff_r=${recordFile(provider, item)}`,
35606
+ ...provider === LEGACY_RECORD_PROVIDER ? [`[ -f "$ff_r" ] || ff_r=${quotePath(RECORDS_DIR)}/${quote(`${item}.json`)}`] : [],
35607
+ `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"record\\",\\"provider\\":\\"${provider}\\",\\"item\\":\\"$(ff_json ${quote(item)})\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`
35608
+ ];
35327
35609
  const checkScript = (input) => {
35328
35610
  const lines = [
35329
35611
  prelude,
35330
35612
  fingerprintFn,
35331
- `ff_state "{\\"fact\\":\\"home\\",\\"path\\":\\"$(ff_json "$HOME")\\"}"`,
35332
- "if command -v vp >/dev/null 2>&1; then",
35333
- ` ff_v=$(command vp --version 2>/dev/null | sed -n 's/^vp v\\{0,1\\}\\([^ ]*\\).*/\\1/p' | head -n 1)`,
35334
- ` ff_state "{\\"fact\\":\\"vp\\",\\"version\\":\\"$(ff_json "$ff_v")\\",\\"path\\":\\"$(ff_json "$(command -v vp)")\\"}"`,
35335
- ` ff_n=$(command vp env default 2>/dev/null | sed -n 's/^Default Node.js version: //p' | head -n 1)`,
35336
- ` if [ -n "$ff_n" ]; then ff_state "{\\"fact\\":\\"node\\",\\"version\\":\\"$(ff_json "$ff_n")\\"}"; else ff_state '{"fact":"node","version":null}'; fi`,
35337
- ` ff_g=$(command vp list --global --json 2>/dev/null | tr -d '\\n')`,
35338
- ` ff_state "{\\"fact\\":\\"globals\\",\\"list\\":\${ff_g:-[]}}"`,
35339
- "else",
35340
- ` ff_state '{"fact":"vp","version":null}'`,
35341
- "fi",
35342
- `ff_root=${quotePath(input.cloneRoot)}`,
35343
- `if [ -d "$ff_root" ]; then ff_present=true; else ff_present=false; fi`,
35344
- `ff_state "{\\"fact\\":\\"cloneRoot\\",\\"path\\":\\"$(ff_json "$ff_root")\\",\\"present\\":$ff_present}"`
35613
+ `ff_state "{\\"fact\\":\\"home\\",\\"path\\":\\"$(ff_json "$HOME")\\"}"`
35345
35614
  ];
35346
- for (const repo of input.repos) {
35347
- const name = quote(repo.name);
35348
- lines.push(`ff_d="$ff_root"/${name}`, `if [ -d "$ff_d/.git" ]; then`, ` ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${name})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$(git -C "$ff_d" rev-parse HEAD 2>/dev/null)")\\",\\"origin\\":\\"$(ff_json "$(git -C "$ff_d" remote get-url origin 2>/dev/null)")\\"}"`, "else", ` ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${name})\\",\\"present\\":false}"`, "fi");
35349
- for (const output of repo.outputs) lines.push(`ff_p=${quotePath(output)}; ff_f=$(ff_fp "$ff_p")`, `if [ -n "$ff_f" ]; then ff_state "{\\"fact\\":\\"output\\",\\"repo\\":\\"$(ff_json ${name})\\",\\"path\\":\\"$(ff_json ${quote(output)})\\",\\"fingerprint\\":\\"$(ff_json "$ff_f")\\"}"; else ff_state "{\\"fact\\":\\"output\\",\\"repo\\":\\"$(ff_json ${name})\\",\\"path\\":\\"$(ff_json ${quote(output)})\\",\\"fingerprint\\":null}"; fi`);
35350
- }
35351
- for (const repo of input.repos) {
35352
- const name = quote(repo.name);
35353
- lines.push(`ff_r=${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json`)}`, `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"record\\",\\"name\\":\\"$(ff_json ${name})\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`);
35615
+ for (const { provider, items } of input.providers) {
35616
+ lines.push(provider.observe(items, input.scope));
35617
+ if (provider.rule === "record") for (const item of items) lines.push(...recordFact(provider.name, item.name));
35354
35618
  }
35355
35619
  lines.push(`ff_c=${CONFIG_DIR_SH}/${quote(CONFIG_FILE)}`, `if [ -f "$ff_c" ]; then ff_state "{\\"fact\\":\\"config\\",\\"present\\":true,\\"base64\\":\\"$(base64 < "$ff_c" | tr -d '\\n')\\"}"; else ff_state '{"fact":"config","present":false}'; fi`);
35356
35620
  lines.push(`ff_state '{"fact":"end"}'`);
35357
35621
  return lines.join("\n");
35358
35622
  };
35359
35623
  //#endregion
35360
- //#region ../../packages/core/src/range.ts
35361
- const newest$1 = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
35362
- const decideRange = (range, installed, available) => {
35363
- if (range === "latest") {
35364
- const version = newest$1(available);
35365
- if (version === void 0) return { kind: "unavailable" };
35366
- return installed === version ? { kind: "leave" } : {
35367
- kind: "install",
35368
- version
35369
- };
35370
- }
35371
- if (range === "*") {
35372
- if (installed !== void 0) return { kind: "leave" };
35373
- const version = newest$1(available);
35374
- return version === void 0 ? { kind: "unavailable" } : {
35375
- kind: "install",
35376
- version
35377
- };
35378
- }
35379
- if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
35380
- if (installed !== void 0 && import_semver.gtr(installed, range)) return {
35381
- kind: "ask",
35382
- installed
35383
- };
35384
- const version = import_semver.maxSatisfying([...available], range);
35385
- return version === null ? { kind: "unavailable" } : {
35386
- kind: "install",
35387
- version
35388
- };
35389
- };
35390
- //#endregion
35391
- //#region ../../packages/core/src/record.ts
35392
- const sameRecord = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([k, v]) => b[k] === v);
35393
- const decideInstall = (record, now, rebuild) => {
35394
- if (rebuild) return {
35395
- run: true,
35396
- reason: "--rebuild"
35397
- };
35398
- if (record === void 0) return {
35399
- run: true,
35400
- reason: "never installed"
35401
- };
35402
- if (record.commit !== now.commit) return {
35403
- run: true,
35404
- reason: `new commit ${now.commit.slice(0, 7)}`
35405
- };
35406
- if (record.path !== now.path) return {
35407
- run: true,
35408
- reason: "clone moved"
35409
- };
35410
- if (record.install !== now.install) return {
35411
- run: true,
35412
- reason: "install command changed"
35413
- };
35414
- if (!sameRecord(record.files, now.files)) return {
35415
- run: true,
35416
- reason: "files changed"
35417
- };
35418
- for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
35419
- run: true,
35420
- reason: `drift: ${path}`
35421
- };
35422
- return { run: false };
35423
- };
35424
- //#endregion
35425
- //#region ../../packages/core/src/targeting.ts
35426
- const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
35427
- //#endregion
35428
- //#region ../../packages/core/src/plan.ts
35429
- const VP_PACKAGE = "vite-plus";
35430
- const NODE_PACKAGE = "node";
35431
- const expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? `${home}${path.slice(1)}` : path;
35432
- const isHomebrew = (path) => path !== void 0 && (path.includes("/homebrew/") || path.includes("/Cellar/"));
35433
- const planVp = (input) => {
35434
- const out = {
35435
- steps: [],
35436
- questions: [],
35437
- skipped: [],
35438
- failures: []
35439
- };
35440
- const { state, machine } = input;
35441
- const vp = input.config.vp;
35442
- const targeted = vp !== void 0 && targets(vp, machine);
35443
- const missing = state.vp === void 0 || state.vp.version === null;
35444
- if (missing) out.steps.push({ kind: "bootstrap" });
35445
- const installed = missing ? void 0 : state.vp?.version ?? void 0;
35446
- const decide = (pkg, range, current, shown = pkg) => {
35447
- if (range === void 0) return void 0;
35448
- const decision = decideRange(range, current, input.versions[pkg] ?? []);
35449
- switch (decision.kind) {
35450
- case "leave": return;
35451
- case "install": return decision.version;
35452
- case "unavailable":
35453
- out.failures.push({
35454
- what: shown,
35455
- reason: `no published version satisfies ${range}`
35456
- });
35457
- return;
35458
- case "ask": {
35459
- const answer = input.answers[pkg];
35460
- if (answer === "downgrade") {
35461
- const target = decideRange(range, void 0, input.versions[pkg] ?? []);
35462
- if (target.kind === "install") return target.version;
35463
- out.failures.push({
35464
- what: shown,
35465
- reason: `no published version satisfies ${range}`
35466
- });
35467
- return;
35468
- }
35469
- if (answer === "skip") {
35470
- out.skipped.push({
35471
- what: shown,
35472
- reason: `${decision.installed} is above ${range}`
35473
- });
35474
- return;
35475
- }
35476
- out.questions.push({
35477
- machine: machine.name,
35478
- package: pkg,
35479
- installed: decision.installed,
35480
- range
35481
- });
35482
- return;
35483
- }
35484
- }
35485
- };
35486
- const nodeNow = missing ? void 0 : state.node ?? void 0;
35487
- let nodeTarget;
35488
- if (targeted) {
35489
- const vpTarget = decide(VP_PACKAGE, vp.version, installed, "vp");
35490
- if (vpTarget !== void 0) {
35491
- if (isHomebrew(state.vp?.path)) out.failures.push({
35492
- what: "vp",
35493
- reason: `vp ${installed} was installed with Homebrew; run brew upgrade vite-plus to get ${vpTarget}`
35494
- });
35495
- else out.steps.push({
35496
- kind: "vp",
35497
- from: installed ?? null,
35498
- version: vpTarget
35499
- });
35500
- }
35501
- nodeTarget = decide(NODE_PACKAGE, vp.node, nodeNow, "node");
35502
- if (nodeTarget !== void 0) out.steps.push({
35503
- kind: "node",
35504
- from: nodeNow ?? null,
35505
- version: nodeTarget
35506
- });
35507
- }
35508
- const nodeDefault = nodeTarget ?? nodeNow;
35509
- const byName = new Map((missing ? [] : state.globals).map((g) => [g.name, g]));
35510
- const installs = [];
35511
- const wanted = Object.entries(targeted ? vp.globals ?? {} : {});
35512
- for (const [name, range] of [...wanted, [SELF_PACKAGE, input.self]]) {
35513
- const current = byName.get(name);
35514
- const version = name === "fleetfizz" ? decideSelf(current?.version, input.self, out) : decide(name, range, current?.version);
35515
- if (version !== void 0) installs.push(current === void 0 ? {
35516
- name,
35517
- version
35518
- } : {
35519
- name,
35520
- from: current.version,
35521
- version
35522
- });
35523
- else if (current !== void 0 && nodeDefault !== void 0 && current.platform?.node !== void 0 && current.platform.node !== nodeDefault) out.steps.push({
35524
- kind: "reinstall",
35525
- name,
35526
- version: current.version
35527
- });
35528
- }
35529
- if (installs.length > 0) out.steps.push({
35530
- kind: "globals",
35531
- packages: installs
35532
- });
35533
- return out;
35534
- };
35535
- const decideSelf = (installed, self, out) => {
35536
- if (installed === void 0 || import_semver.lt(installed, self)) return self;
35537
- if (import_semver.gt(installed, self)) out.failures.push({
35538
- what: SELF_PACKAGE,
35539
- reason: `runs ${SELF_PACKAGE} ${installed}, newer than this ${self}; upgrade here first`
35540
- });
35541
- };
35542
- const planRepos = (input) => {
35543
- const repos = [];
35544
- const failures = [];
35545
- const { config, machine, state } = input;
35546
- const home = state.home ?? "~";
35547
- const root = config.machines?.[machine.name]?.clones ?? "~/.local/share/fleetfizz/clones";
35548
- for (const repo of config.repos ?? []) {
35549
- if (!targets(repo, machine)) continue;
35550
- const name = repoName(repo);
35551
- if (state.cloneRoot !== void 0 && !state.cloneRoot.present && !root.startsWith("~")) {
35552
- failures.push({
35553
- what: name,
35554
- reason: `clone root not mounted: ${state.cloneRoot.path}`
35555
- });
35556
- continue;
35557
- }
35558
- const current = state.repos[name];
35559
- const expectedOrigin = `https://github.com/${repo.origin}.git`;
35560
- if (current?.present && current.origin !== void 0 && current.origin !== expectedOrigin) {
35561
- failures.push({
35562
- what: name,
35563
- reason: `managed clone has origin ${current.origin}, expected ${expectedOrigin}`
35564
- });
35565
- continue;
35566
- }
35567
- const path = `${root}/${name}`;
35568
- const expanded = expandHome(path, home);
35569
- const files = repo.files ?? {};
35570
- const outputs = repo.outputs ?? [];
35571
- const rebuild = input.rebuild === "all" || input.rebuild !== "none" && input.rebuild.includes(name);
35572
- const decision = decideInstall(state.records[name], {
35573
- commit: current?.head ?? "",
35574
- path: expanded,
35575
- install: repo.install,
35576
- files,
35577
- outputs: Object.fromEntries(outputs.map((o) => [o, state.outputs[name]?.[o] ?? void 0]))
35578
- }, rebuild);
35579
- repos.push({
35580
- name,
35581
- origin: repo.origin,
35582
- ref: repo.ref,
35583
- pin: isPin(repo.ref),
35584
- path,
35585
- expanded,
35586
- ...current?.head === void 0 ? {} : { head: current.head },
35587
- install: repo.install,
35588
- files,
35589
- outputs,
35590
- decision
35591
- });
35592
- }
35593
- return {
35594
- repos,
35595
- failures
35596
- };
35597
- };
35598
- const plan = (input) => {
35599
- const vp = planVp(input);
35600
- const repos = planRepos(input);
35601
- return {
35602
- machine: input.machine,
35603
- vp: vp.steps,
35604
- repos: repos.repos,
35605
- questions: vp.questions,
35606
- skipped: vp.skipped,
35607
- failures: [...vp.failures.map((f) => ({
35608
- ...f,
35609
- section: "vp"
35610
- })), ...repos.failures.map((f) => ({
35611
- ...f,
35612
- section: "repos"
35613
- }))]
35614
- };
35615
- };
35616
- //#endregion
35617
- //#region ../../packages/core/src/state.ts
35618
- const GlobalEntry = Struct({
35619
- name: String$5,
35620
- version: String$5,
35621
- platform: optionalKey(Struct({ node: optionalKey(String$5) }))
35622
- });
35623
- const InstallRecordSchema = Struct({
35624
- commit: String$5,
35625
- path: String$5,
35626
- install: String$5,
35627
- files: Record(String$5, String$5),
35628
- outputs: Record(String$5, String$5)
35629
- });
35630
- const Fact = Union([
35631
- Struct({
35632
- fact: Literal("home"),
35633
- path: String$5
35634
- }),
35635
- Struct({ fact: Literal("end") }),
35636
- Struct({
35637
- fact: Literal("vp"),
35638
- version: NullOr(String$5),
35639
- path: optionalKey(String$5)
35640
- }),
35641
- Struct({
35642
- fact: Literal("node"),
35643
- version: NullOr(String$5)
35644
- }),
35645
- Struct({
35646
- fact: Literal("globals"),
35647
- list: ArraySchema(GlobalEntry)
35648
- }),
35649
- Struct({
35650
- fact: Literal("cloneRoot"),
35651
- path: String$5,
35652
- present: Boolean$4
35653
- }),
35654
- Struct({
35655
- fact: Literal("repo"),
35656
- name: String$5,
35657
- present: Boolean$4,
35658
- head: optionalKey(String$5),
35659
- origin: optionalKey(String$5)
35660
- }),
35661
- Struct({
35662
- fact: Literal("record"),
35663
- name: String$5,
35664
- record: InstallRecordSchema
35665
- }),
35666
- Struct({
35667
- fact: Literal("config"),
35668
- present: Boolean$4,
35669
- base64: optionalKey(String$5)
35670
- }),
35671
- Struct({
35672
- fact: Literal("output"),
35673
- repo: String$5,
35674
- path: String$5,
35675
- fingerprint: NullOr(String$5)
35676
- })
35677
- ]);
35678
- const emptyState = {
35679
- complete: false,
35680
- globals: [],
35681
- repos: {},
35682
- records: {},
35683
- outputs: {}
35684
- };
35685
- const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
35686
- const assembleState = (facts) => {
35687
- let state = emptyState;
35688
- for (const fact of facts) switch (fact.fact) {
35689
- case "home":
35690
- state = {
35691
- ...state,
35692
- home: fact.path
35693
- };
35694
- break;
35695
- case "end":
35696
- state = {
35697
- ...state,
35698
- complete: true
35699
- };
35700
- break;
35701
- case "vp":
35702
- state = {
35703
- ...state,
35704
- vp: fact.path === void 0 ? { version: fact.version } : {
35705
- version: fact.version,
35706
- path: fact.path
35707
- }
35708
- };
35709
- break;
35710
- case "node":
35711
- state = {
35712
- ...state,
35713
- node: fact.version
35714
- };
35715
- break;
35716
- case "globals":
35717
- state = {
35718
- ...state,
35719
- globals: fact.list
35720
- };
35721
- break;
35722
- case "cloneRoot":
35723
- state = {
35724
- ...state,
35725
- cloneRoot: {
35726
- path: fact.path,
35727
- present: fact.present
35728
- }
35729
- };
35730
- break;
35731
- case "repo": {
35732
- const repo = {
35733
- present: fact.present,
35734
- ...fact.head === void 0 ? {} : { head: fact.head },
35735
- ...fact.origin === void 0 ? {} : { origin: fact.origin }
35736
- };
35737
- state = {
35738
- ...state,
35739
- repos: {
35740
- ...state.repos,
35741
- [fact.name]: repo
35742
- }
35743
- };
35744
- break;
35745
- }
35746
- case "record":
35747
- state = {
35748
- ...state,
35749
- records: {
35750
- ...state.records,
35751
- [fact.name]: fact.record
35752
- }
35753
- };
35754
- break;
35755
- case "config":
35756
- if (fact.present && fact.base64 !== void 0) state = {
35757
- ...state,
35758
- config: decodeBase64(fact.base64)
35759
- };
35760
- break;
35761
- case "output": state = {
35762
- ...state,
35763
- outputs: {
35764
- ...state.outputs,
35765
- [fact.repo]: {
35766
- ...state.outputs[fact.repo],
35767
- [fact.path]: fact.fingerprint
35768
- }
35769
- }
35770
- };
35771
- }
35772
- return state;
35773
- };
35774
- //#endregion
35775
35624
  //#region ../../packages/remote/src/protocol.ts
35625
+ const Fact = Record(String$5, Unknown);
35776
35626
  const StepStart = Struct({ id: String$5 });
35777
35627
  const StepDone = Struct({
35778
35628
  id: String$5,
35779
35629
  ok: Boolean$4,
35780
35630
  code: optionalKey(Number$2)
35781
35631
  });
35782
- const decodeFact = decodeUnknownResult(fromJsonString(Fact));
35632
+ const StepSkip = Struct({
35633
+ id: String$5,
35634
+ reason: String$5
35635
+ });
35636
+ const decodeFact$3 = decodeUnknownResult(fromJsonString(Fact));
35783
35637
  const decodeStart = decodeUnknownResult(fromJsonString(StepStart));
35784
35638
  const decodeDone = decodeUnknownResult(fromJsonString(StepDone));
35639
+ const decodeSkip = decodeUnknownResult(fromJsonString(StepSkip));
35785
35640
  const STATE = "ff-state ";
35786
35641
  const STEP = "ff-step ";
35787
35642
  const DONE = "ff-done ";
35643
+ const SKIP = "ff-skip ";
35788
35644
  const parseLine = (line) => {
35789
35645
  if (line.startsWith(STATE)) {
35790
- const result = decodeFact(line.slice(9));
35646
+ const result = decodeFact$3(line.slice(9));
35791
35647
  if (result._tag === "Success") return {
35792
35648
  kind: "state",
35793
35649
  fact: result.success
@@ -35804,6 +35660,12 @@ const parseLine = (line) => {
35804
35660
  kind: "done",
35805
35661
  done: result.success
35806
35662
  };
35663
+ } else if (line.startsWith(SKIP)) {
35664
+ const result = decodeSkip(line.slice(8));
35665
+ if (result._tag === "Success") return {
35666
+ kind: "skip",
35667
+ skip: result.success
35668
+ };
35807
35669
  }
35808
35670
  return {
35809
35671
  kind: "text",
@@ -35812,95 +35674,48 @@ const parseLine = (line) => {
35812
35674
  };
35813
35675
  const stepLine = (id) => `printf 'ff-step {"id":"%s"}\\n' '${id}'`;
35814
35676
  const doneLine = (id, ok, code) => code === void 0 ? `printf 'ff-done {"id":"%s","ok":${ok}}\\n' '${id}'` : `printf 'ff-done {"id":"%s","ok":${ok},"code":%s}\\n' '${id}' "${code}"`;
35677
+ const skipLine = (id, reason) => `printf 'ff-skip {"id":"%s","reason":"%s"}\\n' '${id}' ${reason}`;
35815
35678
  //#endregion
35816
35679
  //#region ../../packages/remote/src/apply.ts
35817
- const jsonString = encodeSync(fromJsonString(String$5));
35818
- const jsonRecord = encodeSync(fromJsonString(Record(String$5, String$5)));
35819
- const stepId = (repo, step) => `repo:${repo}:${step}`;
35820
- const isFetchStep = (id) => id.startsWith("repo:") && id.endsWith(":fetch");
35821
- const vpStepId = (step) => step.kind === "reinstall" ? `vp:reinstall:${step.name}` : step.kind === "globals" ? "vp:globals" : `vp:${step.kind}`;
35822
35680
  const CONFIG_STEP = "config";
35823
- const VP_INSTALL = [
35824
- "curl -fsSL https://vite.plus | bash",
35825
- ...vpEnv.split("\n"),
35826
- "{ hash -r 2>/dev/null || true; }"
35827
- ].join(" && ");
35828
- const marked = (id, run, then = []) => [
35681
+ const marked = (id, run, failed = []) => [
35829
35682
  stepLine(id),
35830
35683
  ...run,
35831
35684
  `ff_code=$?`,
35832
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(id, false, "$ff_code")}; else`,
35685
+ `if [ "$ff_code" -ne 0 ]; then`,
35686
+ doneLine(id, false, "$ff_code"),
35687
+ ...failed,
35688
+ "else",
35833
35689
  doneLine(id, true),
35834
- ...then,
35835
35690
  "fi"
35836
35691
  ];
35837
- const vpStep = (step) => {
35838
- const id = vpStepId(step);
35839
- const command = (() => {
35840
- switch (step.kind) {
35841
- case "bootstrap": return VP_INSTALL;
35842
- case "vp": return `command vp upgrade ${quote(step.version)}`;
35843
- case "node": return `command vp env default ${quote(step.version)}`;
35844
- case "reinstall": return `command vp install -g ${quote(`${step.name}@${step.version}`)}`;
35845
- case "globals": return `command vp install -g ${step.packages.map((p) => quote(`${p.name}@${p.version}`)).join(" ")}`;
35846
- }
35847
- })();
35848
- return marked(id, [step.kind === "bootstrap" ? command : `( ${command} )`]);
35849
- };
35850
35692
  const configStep = () => {
35851
35693
  const dir = CONFIG_DIR_SH;
35852
35694
  const write = (variable, file) => `printf '%s' "$${variable}" | base64 -d > ${dir}/${quote(`${file}.tmp`)} && mv -f ${dir}/${quote(`${file}.tmp`)} ${dir}/${quote(file)}`;
35853
35695
  return marked(CONFIG_STEP, [`( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`]);
35854
35696
  };
35855
- const writeRecord = (repo) => {
35856
- return [
35857
- `ff_outs=""`,
35858
- ...repo.outputs.map((o) => `ff_fpv=$(ff_fp ${quotePath(o)}); if [ -n "$ff_fpv" ]; then ff_outs="$ff_outs,\\"$(ff_json ${quote(o)})\\":\\"$(ff_json "$ff_fpv")\\""; fi`),
35859
- `mkdir -p ${quotePath(RECORDS_DIR)}`,
35860
- `printf '%s\\n' "{\\"commit\\":\\"$ff_after\\",\\"path\\":\\"$(ff_json "$ff_d")\\",\\"install\\":$(ff_lit ${quote(jsonString(repo.install))}),\\"files\\":$(ff_lit ${quote(jsonRecord(repo.files))}),\\"outputs\\":{\${ff_outs#,}}}" > ${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json.tmp`)}`,
35861
- `mv -f ${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json.tmp`)} ${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json`)}`
35697
+ const providerSteps = (plan) => {
35698
+ if (plan.decisions.length === 0 && plan.steps.length === 0) return [];
35699
+ const held = `ff_held_${plan.name}`;
35700
+ const items = [...new Set(plan.steps.flatMap((step) => step.items))];
35701
+ const flag = (item) => `ff_f_${plan.name}_${items.indexOf(item)}`;
35702
+ const step = (s) => [
35703
+ `if [ -n "${s.items.map((item) => `\${${flag(item)}:-}`).join("")}" ]; then`,
35704
+ skipLine(s.id, "'an earlier step failed'"),
35705
+ "else",
35706
+ ...marked(s.id, s.run, [...s.items.map((item) => `${flag(item)}=1`), `${held}=1`]),
35707
+ "fi"
35862
35708
  ];
35863
- };
35864
- const writeFiles = (repo) => Object.entries(repo.files).flatMap(([path, content]) => [`mkdir -p "$(dirname "$ff_d"/${quote(path)})"`, `printf '%s' ${quote(content)} > "$ff_d"/${quote(path)}`]);
35865
- const repoSteps = (repo, input) => {
35866
- const fetch = stepId(repo.name, "fetch");
35867
- const install = stepId(repo.name, "install");
35868
- const url = `https://github.com/${repo.origin}.git`;
35869
- const sync = repo.pin ? [
35870
- `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(repo.ref)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(repo.ref)}; fi`,
35871
- `git -C "$ff_d" checkout --quiet --detach ${quote(repo.ref)}`,
35872
- `git -C "$ff_d" reset --quiet --hard ${quote(repo.ref)}`
35873
- ] : [`ff_git -C "$ff_d" fetch --quiet origin ${quote(repo.ref)}`, `git -C "$ff_d" reset --quiet --hard ${quote(`origin/${repo.ref}`)}`];
35874
- const envPrefix = `FLEETFIZZ_MACHINE=${quote(input.env.machine)} FLEETFIZZ_PLATFORM=${quote(input.env.platform)} FLEETFIZZ_MACHINES=${quote(input.env.machines.join(","))}`;
35709
+ const why = [`ff_why=""`, ...plan.dependsOn.map((dep) => `if [ -z "$ff_why" ] && [ -n "\${ff_held_${dep}:-}" ]; then ff_why=${quote(dep)}; fi`)];
35710
+ if (plan.steps.length === 0) return [...why, `if [ -n "$ff_why" ]; then ${held}=1; fi`];
35875
35711
  return [
35876
- `ff_d="$ff_root"/${quote(repo.name)}`,
35877
- `ff_before=$(git -C "$ff_d" rev-parse HEAD 2>/dev/null || true)`,
35878
- ...marked(fetch, [
35879
- "(",
35880
- " set -e",
35881
- ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
35882
- ` if [ "$(git -C "$ff_d" remote get-url origin)" != ${quote(url)} ]; then echo "origin is $(git -C "$ff_d" remote get-url origin), expected ${url}"; exit 1; fi`,
35883
- ...sync.map((l) => ` ${l}`),
35884
- ` git -C "$ff_d" clean --quiet -fd`,
35885
- ...writeFiles(repo).map((l) => ` ${l}`),
35886
- ")"
35887
- ], [
35888
- `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
35889
- `ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${quote(repo.name)})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$ff_after")\\"}"`,
35890
- `if [ ${repo.decision.run ? "true" : "false"} = true ] || [ "$ff_after" != "$ff_before" ]; then`,
35891
- ` ${stepLine(install)}`,
35892
- ` (cd "$ff_d" && ${envPrefix} "\${SHELL:-/bin/sh}" -lc ${quote(repo.install)})`,
35893
- ` ff_code=$?`,
35894
- ` if [ "$ff_code" -ne 0 ]; then ${doneLine(install, false, "$ff_code")}; else`,
35895
- " (",
35896
- " set -e",
35897
- ...writeRecord(repo).map((l) => ` ${l}`),
35898
- " )",
35899
- ` ff_code=$?`,
35900
- ` if [ "$ff_code" -ne 0 ]; then echo "could not write the install record"; ${doneLine(install, false, "$ff_code")}; else ${doneLine(install, true)}; fi`,
35901
- " fi",
35902
- "fi"
35903
- ])
35712
+ ...why,
35713
+ `if [ -n "$ff_why" ]; then`,
35714
+ `${held}=1`,
35715
+ ...plan.steps.map((s) => skipLine(s.id, `"depends on $ff_why"`)),
35716
+ "else",
35717
+ ...plan.steps.flatMap(step),
35718
+ "fi"
35904
35719
  ];
35905
35720
  };
35906
35721
  const applyStdin = (token, config, schema) => [
@@ -35914,24 +35729,18 @@ const encodeBase64 = (text) => {
35914
35729
  for (const byte of bytes) binary += String.fromCharCode(byte);
35915
35730
  return btoa(binary);
35916
35731
  };
35917
- const applyScript = (input) => {
35918
- const lines = [
35919
- prelude,
35920
- fingerprintFn,
35921
- `ff_lit() { printf '%s' "$1"; }`,
35922
- `IFS= read -r ff_token || ff_token=""`,
35923
- `IFS= read -r ff_config || ff_config=""`,
35924
- `IFS= read -r ff_schema || ff_schema=""`,
35925
- `ff_git() { if [ -n "$ff_token" ]; then FF_TOKEN="$ff_token" git -c credential.helper= -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$FF_TOKEN"; }; f' "$@"; else git "$@"; fi; }`,
35926
- `ff_root=${quotePath(input.cloneRoot)}`,
35927
- ...input.cloneRoot.startsWith("~") ? [`mkdir -p "$ff_root"`] : []
35928
- ];
35929
- lines.push(...configStep());
35930
- for (const step of input.plan.vp) lines.push(...vpStep(step));
35931
- for (const repo of input.plan.repos) lines.push(...repoSteps(repo, input));
35932
- lines.push(doneLine("end", true));
35933
- return lines.join("\n");
35934
- };
35732
+ const applyScript = (plan) => [
35733
+ prelude,
35734
+ fingerprintFn,
35735
+ `ff_lit() { printf '%s' "$1"; }`,
35736
+ `IFS= read -r ff_token || ff_token=""`,
35737
+ `IFS= read -r ff_config || ff_config=""`,
35738
+ `IFS= read -r ff_schema || ff_schema=""`,
35739
+ `ff_git() { if [ -n "$ff_token" ]; then FF_TOKEN="$ff_token" git -c credential.helper= -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$FF_TOKEN"; }; f' "$@"; else git "$@"; fi; }`,
35740
+ ...configStep(),
35741
+ ...plan.providers.flatMap(providerSteps),
35742
+ doneLine("end", true)
35743
+ ].join("\n");
35935
35744
  //#endregion
35936
35745
  //#region ../../packages/remote/src/tailnet.ts
35937
35746
  const StatusNode = Struct({
@@ -64199,6 +64008,355 @@ const useWindowSize = () => {
64199
64008
  return size;
64200
64009
  };
64201
64010
  //#endregion
64011
+ //#region ../../packages/core/src/targeting.ts
64012
+ const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
64013
+ const isVariantList = (entry) => Array.isArray(entry);
64014
+ const variantFor = (entry, machine) => {
64015
+ if (!isVariantList(entry)) return targets(entry, machine) ? { variant: entry } : void 0;
64016
+ const index = entry.findIndex((variant) => targets(variant, machine));
64017
+ const variant = entry[index];
64018
+ return variant === void 0 ? void 0 : {
64019
+ variant,
64020
+ index
64021
+ };
64022
+ };
64023
+ //#endregion
64024
+ //#region ../../packages/core/src/engine.ts
64025
+ const dependenciesOf = (provider, config) => [.../* @__PURE__ */ new Set([...provider.dependsOn, ...config[provider.name]?.dependsOn ?? []])];
64026
+ const withoutTargeting = ({ platforms: _platforms, machines: _machines, ...def }) => def;
64027
+ const targetedItems = (provider, config, machine) => {
64028
+ const section = config[provider.name];
64029
+ if (section === void 0 || !targets(section, machine)) return provider.items(void 0, []);
64030
+ const listed = Object.entries(section.items ?? {}).flatMap(([name, entry]) => {
64031
+ const chosen = variantFor(entry, machine);
64032
+ if (chosen === void 0) return [];
64033
+ const at = chosen.index === void 0 ? [] : [chosen.index];
64034
+ return [{
64035
+ name,
64036
+ def: withoutTargeting(chosen.variant),
64037
+ path: [
64038
+ provider.name,
64039
+ "items",
64040
+ name,
64041
+ ...at
64042
+ ]
64043
+ }];
64044
+ });
64045
+ return provider.items(section, listed);
64046
+ };
64047
+ const machineNames = (value) => {
64048
+ if (Array.isArray(value)) return value.flatMap(machineNames);
64049
+ if (typeof value !== "object" || value === null) return [];
64050
+ return Object.entries(value).flatMap(([key, v]) => key === "machines" && Array.isArray(v) && v.every((name) => typeof name === "string") ? v : machineNames(v));
64051
+ };
64052
+ const machinesNamed = (provider, config) => machineNames(config[provider.name]);
64053
+ const hold = (decisions, by) => decisions.map((d) => d.kind === "change" || d.kind === "ask" ? {
64054
+ kind: "skip",
64055
+ item: d.item,
64056
+ ...d.kind === "change" && d.from !== void 0 ? { at: d.from } : {},
64057
+ ...d.kind === "ask" && d.at !== void 0 ? { at: d.at } : {},
64058
+ reason: `depends on ${by}`
64059
+ } : d);
64060
+ const planMachine = (input) => {
64061
+ const ctx = {
64062
+ ...input.ctx,
64063
+ home: input.state.home ?? "~"
64064
+ };
64065
+ const plans = /* @__PURE__ */ new Map();
64066
+ const held = /* @__PURE__ */ new Set();
64067
+ for (const provider of input.providers) {
64068
+ const dependsOn = dependenciesOf(provider, ctx.config);
64069
+ const items = targetedItems(provider, ctx.config, ctx.machine);
64070
+ const decisions = provider.decide({
64071
+ items,
64072
+ observed: provider.parse(input.state.facts[provider.name] ?? [], input.state.records[provider.name] ?? {}),
64073
+ resolved: input.resolved[provider.name],
64074
+ ctx
64075
+ });
64076
+ const by = dependsOn.find((name) => {
64077
+ const dep = plans.get(name);
64078
+ return dep !== void 0 && dep.decisions.length > 0 && (held.has(name) || dep.decisions.some((d) => d.kind === "fail"));
64079
+ });
64080
+ if (by !== void 0) held.add(provider.name);
64081
+ plans.set(provider.name, {
64082
+ name: provider.name,
64083
+ rule: provider.rule,
64084
+ dependsOn,
64085
+ decisions: by === void 0 ? decisions : hold(decisions, by),
64086
+ steps: by === void 0 ? provider.act(decisions, ctx) : []
64087
+ });
64088
+ }
64089
+ return {
64090
+ machine: ctx.machine,
64091
+ providers: [...plans.values()]
64092
+ };
64093
+ };
64094
+ //#endregion
64095
+ //#region ../../packages/core/src/range.ts
64096
+ const newest = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
64097
+ const decideRange = (range, installed, available) => {
64098
+ if (range === "latest") {
64099
+ const version = newest(available);
64100
+ if (version === void 0) return { kind: "unavailable" };
64101
+ return installed === version ? { kind: "leave" } : {
64102
+ kind: "install",
64103
+ version
64104
+ };
64105
+ }
64106
+ if (range === "*") {
64107
+ if (installed !== void 0) return { kind: "leave" };
64108
+ const version = newest(available);
64109
+ return version === void 0 ? { kind: "unavailable" } : {
64110
+ kind: "install",
64111
+ version
64112
+ };
64113
+ }
64114
+ if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
64115
+ if (installed !== void 0 && import_semver.gtr(installed, range)) return {
64116
+ kind: "ask",
64117
+ installed
64118
+ };
64119
+ const version = import_semver.maxSatisfying([...available], range);
64120
+ return version === null ? { kind: "unavailable" } : {
64121
+ kind: "install",
64122
+ version
64123
+ };
64124
+ };
64125
+ //#endregion
64126
+ //#region ../../packages/core/src/provider.ts
64127
+ const address = (provider, item) => `${provider}:${item}`;
64128
+ const parseAddress = (text) => {
64129
+ const at = text.indexOf(":");
64130
+ return at <= 0 || at === text.length - 1 ? void 0 : {
64131
+ provider: text.slice(0, at),
64132
+ item: text.slice(at + 1)
64133
+ };
64134
+ };
64135
+ const QUESTION_CHOICES = {
64136
+ range: [
64137
+ "bump",
64138
+ "skip",
64139
+ "downgrade"
64140
+ ],
64141
+ config: ["overwrite", "stop"],
64142
+ mode: ["managed", "skip"]
64143
+ };
64144
+ const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
64145
+ const rangeQuestion = (subject, installed, range, bumpPath) => ({
64146
+ kind: "range",
64147
+ subject,
64148
+ found: installed,
64149
+ problem: `is above ${range}`,
64150
+ choices: [
64151
+ {
64152
+ value: "bump",
64153
+ title: "bump the config"
64154
+ },
64155
+ {
64156
+ value: "skip",
64157
+ title: "skip on these Machines (leave as is, shown as skipped)"
64158
+ },
64159
+ {
64160
+ value: "downgrade",
64161
+ title: "downgrade into the range"
64162
+ }
64163
+ ],
64164
+ default: "skip",
64165
+ bump: {
64166
+ path: bumpPath,
64167
+ range
64168
+ }
64169
+ });
64170
+ const decideRangeItem = (input) => {
64171
+ const { item, range, installed, available } = input;
64172
+ const at = installed === void 0 ? {} : { at: installed };
64173
+ const unavailable = {
64174
+ kind: "fail",
64175
+ item,
64176
+ ...at,
64177
+ reason: `no published version satisfies ${range}`
64178
+ };
64179
+ const install = (version) => ({
64180
+ kind: "change",
64181
+ item,
64182
+ ...installed === void 0 ? {} : { from: installed },
64183
+ to: version,
64184
+ change: version
64185
+ });
64186
+ const decision = decideRange(range, installed, available);
64187
+ switch (decision.kind) {
64188
+ case "leave": return {
64189
+ kind: "leave",
64190
+ item,
64191
+ ...at
64192
+ };
64193
+ case "install": return install(decision.version);
64194
+ case "unavailable": return unavailable;
64195
+ case "ask": {
64196
+ const subject = address(input.provider, item);
64197
+ const answer = input.ctx.answer("range", subject);
64198
+ if (answer === "skip") return {
64199
+ kind: "skip",
64200
+ item,
64201
+ ...at,
64202
+ reason: `is above ${range}`
64203
+ };
64204
+ if (answer === "downgrade") {
64205
+ const target = decideRange(range, void 0, available);
64206
+ return target.kind === "install" ? install(target.version) : unavailable;
64207
+ }
64208
+ return {
64209
+ kind: "ask",
64210
+ item,
64211
+ ...at,
64212
+ question: rangeQuestion(subject, decision.installed, range, input.bumpPath)
64213
+ };
64214
+ }
64215
+ }
64216
+ };
64217
+ const orderProviders = (all, dependsOn) => {
64218
+ const byName = new Map(all.map((p) => [p.name, p]));
64219
+ const order = [];
64220
+ const done = /* @__PURE__ */ new Set();
64221
+ const visit = (p, path) => {
64222
+ if (done.has(p.name)) return void 0;
64223
+ if (path.includes(p.name)) return [...path.slice(path.indexOf(p.name)), p.name];
64224
+ for (const name of dependsOn(p)) {
64225
+ const dep = byName.get(name);
64226
+ const cycle = dep === void 0 ? void 0 : visit(dep, [...path, p.name]);
64227
+ if (cycle !== void 0) return cycle;
64228
+ }
64229
+ done.add(p.name);
64230
+ order.push(p);
64231
+ };
64232
+ for (const p of all) {
64233
+ const cycle = visit(p, []);
64234
+ if (cycle !== void 0) return { cycle };
64235
+ }
64236
+ return { order };
64237
+ };
64238
+ //#endregion
64239
+ //#region ../../packages/core/src/record.ts
64240
+ const sameRecord = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([k, v]) => b[k] === v);
64241
+ const decideInstall = (record, now, rebuild) => {
64242
+ if (rebuild) return {
64243
+ run: true,
64244
+ reason: "--rebuild"
64245
+ };
64246
+ if (record === void 0) return {
64247
+ run: true,
64248
+ reason: "never installed"
64249
+ };
64250
+ if (record.commit !== now.commit) return {
64251
+ run: true,
64252
+ reason: `new commit ${now.commit.slice(0, 7)}`
64253
+ };
64254
+ if (record.path !== now.path) return {
64255
+ run: true,
64256
+ reason: "clone moved"
64257
+ };
64258
+ if (record.install !== now.install) return {
64259
+ run: true,
64260
+ reason: "install command changed"
64261
+ };
64262
+ if (!sameRecord(record.files, now.files)) return {
64263
+ run: true,
64264
+ reason: "files changed"
64265
+ };
64266
+ for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
64267
+ run: true,
64268
+ reason: `drift: ${path}`
64269
+ };
64270
+ return { run: false };
64271
+ };
64272
+ //#endregion
64273
+ //#region ../../packages/core/src/state.ts
64274
+ const EngineFact = Union([
64275
+ Struct({
64276
+ fact: Literal("home"),
64277
+ path: String$5
64278
+ }),
64279
+ Struct({ fact: Literal("end") }),
64280
+ Struct({
64281
+ fact: Literal("config"),
64282
+ present: Boolean$4,
64283
+ base64: optionalKey(String$5)
64284
+ }),
64285
+ Struct({
64286
+ fact: Literal("record"),
64287
+ provider: String$5,
64288
+ item: String$5,
64289
+ record: Unknown
64290
+ })
64291
+ ]);
64292
+ const ProviderFact = Struct({ provider: String$5 });
64293
+ const AtFact = Struct({
64294
+ provider: String$5,
64295
+ fact: Literal("at"),
64296
+ item: String$5,
64297
+ at: String$5
64298
+ });
64299
+ const decodeEngineFact = decodeUnknownOption(EngineFact);
64300
+ const decodeProviderFact = decodeUnknownOption(ProviderFact);
64301
+ const emptyState = {
64302
+ complete: false,
64303
+ records: {},
64304
+ facts: {}
64305
+ };
64306
+ const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
64307
+ const assembleState = (facts) => {
64308
+ let state = emptyState;
64309
+ for (const raw of facts) {
64310
+ const fact = decodeEngineFact(raw);
64311
+ if (fact._tag === "Some") {
64312
+ const f = fact.value;
64313
+ switch (f.fact) {
64314
+ case "home":
64315
+ state = {
64316
+ ...state,
64317
+ home: f.path
64318
+ };
64319
+ break;
64320
+ case "end":
64321
+ state = {
64322
+ ...state,
64323
+ complete: true
64324
+ };
64325
+ break;
64326
+ case "config":
64327
+ if (f.present && f.base64 !== void 0) state = {
64328
+ ...state,
64329
+ config: decodeBase64(f.base64)
64330
+ };
64331
+ break;
64332
+ case "record": state = {
64333
+ ...state,
64334
+ records: {
64335
+ ...state.records,
64336
+ [f.provider]: {
64337
+ ...state.records[f.provider],
64338
+ [f.item]: f.record
64339
+ }
64340
+ }
64341
+ };
64342
+ }
64343
+ continue;
64344
+ }
64345
+ const owned = decodeProviderFact(raw);
64346
+ if (owned._tag === "Some") {
64347
+ const { provider } = owned.value;
64348
+ state = {
64349
+ ...state,
64350
+ facts: {
64351
+ ...state.facts,
64352
+ [provider]: [...state.facts[provider] ?? [], raw]
64353
+ }
64354
+ };
64355
+ }
64356
+ }
64357
+ return state;
64358
+ };
64359
+ //#endregion
64202
64360
  //#region src/ledger.ts
64203
64361
  const Note = Struct({
64204
64362
  kind: Literals$2([
@@ -64211,8 +64369,8 @@ const Note = Struct({
64211
64369
  const Item = Struct({
64212
64370
  name: String$5,
64213
64371
  kind: Literals$2([
64214
- "version",
64215
- "repo",
64372
+ "range",
64373
+ "record",
64216
64374
  "config"
64217
64375
  ]),
64218
64376
  from: optionalKey(String$5),
@@ -64229,7 +64387,10 @@ const Item = Struct({
64229
64387
  doing: optionalKey(String$5),
64230
64388
  detail: optionalKey(String$5),
64231
64389
  output: optionalKey(ArraySchema(String$5)),
64232
- steps: ArraySchema(String$5),
64390
+ steps: ArraySchema(Struct({
64391
+ id: String$5,
64392
+ doing: String$5
64393
+ })),
64233
64394
  started: optionalKey(Number$2),
64234
64395
  ended: optionalKey(Number$2)
64235
64396
  });
@@ -64258,88 +64419,56 @@ const LedgerState = Struct({
64258
64419
  started: Number$2,
64259
64420
  ended: optionalKey(Number$2)
64260
64421
  });
64261
- const short = (sha) => sha === void 0 ? void 0 : sha.slice(0, 7);
64262
- const doingOf = (id) => id === "config" ? "copying" : isFetchStep(id) ? "fetching" : "installing";
64263
- const itemsOf = (p, state) => {
64264
- const items = /* @__PURE__ */ new Map();
64265
- const key = (kind, name) => `${kind}:${name}`;
64266
- const base = (name, from, kind = "version") => ({
64267
- name,
64268
- kind,
64269
- ...from === void 0 ? {} : { from },
64270
- changes: false,
64271
- status: "idle",
64272
- steps: []
64273
- });
64274
- const set = (name, change, kind = "version") => items.set(key(kind, name), change(items.get(key(kind, name)) ?? base(name, void 0, kind)));
64275
- const plans = (id, to, from, reason) => ({ from: checked, ...item }) => {
64276
- const now = from === void 0 ? checked : from ?? void 0;
64277
- return {
64278
- ...item,
64279
- ...now === void 0 ? {} : { from: now },
64280
- to,
64281
- ...reason === void 0 ? {} : { reason },
64422
+ const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
64423
+ const base = {
64424
+ name: address(pp.name, d.item),
64425
+ kind: pp.rule,
64426
+ steps: pp.steps.filter((step) => step.items.includes(d.item)).map(({ id, doing }) => ({
64427
+ id,
64428
+ doing
64429
+ }))
64430
+ };
64431
+ switch (d.kind) {
64432
+ case "change": return {
64433
+ ...base,
64434
+ ...d.from === void 0 ? {} : { from: d.from },
64435
+ to: d.to,
64436
+ ...d.reason === void 0 ? {} : { reason: d.reason },
64282
64437
  changes: true,
64283
- steps: [...item.steps, id]
64438
+ status: "idle"
64439
+ };
64440
+ case "leave": return {
64441
+ ...base,
64442
+ ...d.at === void 0 ? {} : { from: d.at },
64443
+ changes: false,
64444
+ status: "idle"
64445
+ };
64446
+ case "skip":
64447
+ case "fail": return {
64448
+ ...base,
64449
+ ...d.at === void 0 ? {} : { from: d.at },
64450
+ changes: d.kind === "fail",
64451
+ status: d.kind === "fail" ? "failed" : "skipped",
64452
+ detail: d.reason
64453
+ };
64454
+ case "ask": return {
64455
+ ...base,
64456
+ ...d.at === void 0 ? {} : { from: d.at },
64457
+ changes: false,
64458
+ status: "skipped",
64459
+ detail: d.question.problem
64284
64460
  };
64285
- };
64286
- const vp = state.vp?.version ?? void 0;
64287
- items.set(key("version", "vp"), base("vp", vp));
64288
- if (state.node != null) items.set(key("version", "node"), base("node", state.node));
64289
- for (const g of state.globals) items.set(key("version", g.name), base(g.name, g.version));
64290
- for (const step of p.vp) {
64291
- const id = vpStepId(step);
64292
- switch (step.kind) {
64293
- case "bootstrap":
64294
- set("vp", plans(id, "latest", null));
64295
- break;
64296
- case "vp":
64297
- case "node":
64298
- set(step.kind, plans(id, step.version, step.from));
64299
- break;
64300
- case "reinstall":
64301
- set(step.name, plans(id, step.version, step.version, "reinstall"));
64302
- break;
64303
- case "globals": for (const pkg of step.packages) set(pkg.name, plans(id, pkg.version, pkg.from ?? null));
64304
- }
64305
- }
64306
- for (const repo of p.repos) {
64307
- const from = short(state.records[repo.name]?.commit);
64308
- const to = short(repo.head);
64309
- items.set(key("repo", repo.name), {
64310
- name: repo.name,
64311
- kind: "repo",
64312
- ...from === void 0 ? {} : { from },
64313
- ...to === void 0 ? {} : { to },
64314
- ...repo.decision.run ? { reason: repo.decision.reason } : {},
64315
- changes: repo.decision.run,
64316
- status: "idle",
64317
- steps: [stepId(repo.name, "fetch"), stepId(repo.name, "install")]
64318
- });
64319
64461
  }
64320
- for (const s of p.skipped) set(s.what, (i) => ({
64321
- ...i,
64322
- status: "skipped",
64323
- detail: s.reason
64324
- }));
64325
- for (const f of p.failures) {
64326
- const kind = f.section === "repos" ? "repo" : "version";
64327
- set(f.what, (i) => ({
64328
- ...i,
64329
- changes: true,
64330
- status: "failed",
64331
- detail: f.reason
64332
- }), kind);
64333
- }
64334
- items.set(key("config", CONFIG_STEP), {
64335
- name: CONFIG_STEP,
64336
- kind: "config",
64337
- changes: false,
64338
- status: "idle",
64339
- steps: [CONFIG_STEP]
64340
- });
64341
- return [...items.values()];
64342
- };
64462
+ })), {
64463
+ name: CONFIG_STEP,
64464
+ kind: "config",
64465
+ changes: false,
64466
+ status: "idle",
64467
+ steps: [{
64468
+ id: CONFIG_STEP,
64469
+ doing: "copying"
64470
+ }]
64471
+ }];
64343
64472
  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;
64344
64473
  const anyFailed = (state) => state.machines.some((m) => outcomeOf(m, state.dryRun) === "failed");
64345
64474
  const makeLedger = fn("makeLedger")(function* (input) {
@@ -64373,7 +64502,7 @@ const makeLedger = fn("makeLedger")(function* (input) {
64373
64502
  }));
64374
64503
  const items = (name, id, change) => machine(name, (m, now) => ({
64375
64504
  ...m,
64376
- items: m.items.map((i) => i.steps.includes(id) ? change(i, now) : i)
64505
+ items: m.items.map((i) => i.steps.some((step) => step.id === id) ? change(i, now) : i)
64377
64506
  }));
64378
64507
  const store = {
64379
64508
  get: () => snapshot,
@@ -64414,25 +64543,24 @@ const makeLedger = fn("makeLedger")(function* (input) {
64414
64543
  ...output.length === 0 ? {} : { output },
64415
64544
  ended: now
64416
64545
  })),
64417
- plan: (name, p, state) => machine(name, (m) => ({
64546
+ plan: (name, p) => machine(name, (m) => ({
64418
64547
  ...m,
64419
64548
  status: "planned",
64420
- items: itemsOf(p, state)
64549
+ items: itemsOf(p)
64421
64550
  })),
64422
64551
  start: (name) => machine(name, (m, now) => ({
64423
64552
  ...m,
64424
64553
  status: "running",
64425
64554
  started: now
64426
64555
  })),
64427
- stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" ? item : {
64556
+ stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" || item.status === "skipped" ? item : {
64428
64557
  ...item,
64429
- changes: item.changes || !(id === "config" || isFetchStep(id)),
64430
64558
  status: "running",
64431
- doing: doingOf(id),
64559
+ doing: item.steps.find((step) => step.id === id)?.doing ?? "running",
64432
64560
  started: item.started ?? now
64433
64561
  }),
64434
64562
  stepDone: (name, id, result) => items(name, id, (item, now) => {
64435
- if (item.status === "failed") return item;
64563
+ if (item.status === "failed" || item.status === "skipped") return item;
64436
64564
  if (!result.ok) return {
64437
64565
  ...item,
64438
64566
  changes: true,
@@ -64441,25 +64569,31 @@ const makeLedger = fn("makeLedger")(function* (input) {
64441
64569
  ...result.output.length === 0 ? {} : { output: result.output },
64442
64570
  ended: now
64443
64571
  };
64572
+ if (item.steps.at(-1)?.id !== id) return item;
64444
64573
  const { doing: _, ...rest } = item;
64445
- if (isFetchStep(id)) return item.changes ? item : {
64574
+ return {
64446
64575
  ...rest,
64447
- status: "idle",
64576
+ status: "ok",
64448
64577
  ended: now
64449
64578
  };
64579
+ }),
64580
+ stepSkipped: (name, id, reason) => items(name, id, (item, now) => {
64581
+ if (item.status === "failed") return item;
64582
+ const { doing: _, ...rest } = item;
64450
64583
  return {
64451
64584
  ...rest,
64452
- status: "ok",
64585
+ status: "skipped",
64586
+ detail: reason,
64453
64587
  ended: now
64454
64588
  };
64455
64589
  }),
64456
- fetched: (name, repo, head) => machine(name, (m) => ({
64590
+ reached: (name, item, at) => machine(name, (m) => ({
64457
64591
  ...m,
64458
- items: m.items.map((i) => i.kind === "repo" && i.name === repo ? {
64592
+ items: m.items.map((i) => i.name !== item ? i : {
64459
64593
  ...i,
64460
- to: head.slice(0, 7),
64461
- changes: i.changes || head.slice(0, 7) !== i.from
64462
- } : i)
64594
+ to: at,
64595
+ changes: i.status === "failed" || i.from !== at
64596
+ })
64463
64597
  })),
64464
64598
  finish: (name, outcome) => machine(name, (m, now) => ({
64465
64599
  ...m,
@@ -64801,6 +64935,10 @@ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
64801
64935
  })
64802
64936
  });
64803
64937
  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}`;
64938
+ const unchanged = (i) => ({
64939
+ text: "",
64940
+ detail: i.kind === "record" ? "up to date" : void 0
64941
+ });
64804
64942
  const statusOf = (i, dryRun) => {
64805
64943
  switch (i.status) {
64806
64944
  case "running": return {
@@ -64822,7 +64960,8 @@ const statusOf = (i, dryRun) => {
64822
64960
  text: "✓ copied",
64823
64961
  tone: "dim"
64824
64962
  };
64825
- if (i.kind === "repo") return i.from !== void 0 && i.from === i.to ? {
64963
+ if (!i.changes) return unchanged(i);
64964
+ if (i.kind === "record") return i.from !== void 0 && i.from === i.to ? {
64826
64965
  text: "✓ rebuilt",
64827
64966
  tone: "ok",
64828
64967
  detail: i.reason
@@ -64841,10 +64980,7 @@ const statusOf = (i, dryRun) => {
64841
64980
  tone: "dim",
64842
64981
  detail: i.reason
64843
64982
  };
64844
- return {
64845
- text: "",
64846
- detail: i.kind === "repo" ? "up to date" : void 0
64847
- };
64983
+ return unchanged(i);
64848
64984
  }
64849
64985
  };
64850
64986
  const countsOf = (m) => {
@@ -65187,6 +65323,743 @@ const report = fn("report")(function* () {
65187
65323
  ]
65188
65324
  })));
65189
65325
  });
65326
+ const decodeFact$2 = decodeUnknownOption(Union([Struct({
65327
+ fact: Literal("brew"),
65328
+ version: NullOr(String$5)
65329
+ }), AtFact]));
65330
+ const NO_BREW = "Homebrew is not installed; install it first";
65331
+ function homebrew(name) {
65332
+ const flag = name === "brew" ? "--formula" : "--cask";
65333
+ const lookup = name === "brew" ? "\"$ff_i\"" : "\"${ff_i##*/}\"";
65334
+ const report = (names) => `for ff_i in ${names.map(quote).join(" ")}; do ff_v=$(brew list --versions ${flag} ${lookup} 2>/dev/null | awk '{print $NF}'); [ -z "$ff_v" ] || ff_at ${name} "$ff_i" "$ff_v"; done`;
65335
+ const step = (what, batch) => {
65336
+ const names = batch.map((c) => c.name);
65337
+ const update = batch.some((c) => c.range === "latest") ? "brew update && " : "";
65338
+ return {
65339
+ id: `${name}:${name === "brew" ? "formulae" : "casks"}:${what}`,
65340
+ items: names,
65341
+ doing: what === "install" ? "installing" : "upgrading",
65342
+ run: [[
65343
+ `( ${update}NONINTERACTIVE=1 brew ${what} ${flag} ${names.map(quote).join(" ")}`,
65344
+ "ff_rc=$?",
65345
+ report(names),
65346
+ "exit $ff_rc )"
65347
+ ].join("; ")]
65348
+ };
65349
+ };
65350
+ return {
65351
+ name,
65352
+ rule: "range",
65353
+ dependsOn: name === "cask" ? ["brew"] : [],
65354
+ items(section, listed) {
65355
+ if (section === void 0) return [];
65356
+ const packages = listed.map(({ name: item, def }) => ({
65357
+ name: item,
65358
+ kind: "package",
65359
+ range: def.range
65360
+ }));
65361
+ return name === "brew" ? [{
65362
+ name: "brew",
65363
+ kind: "homebrew"
65364
+ }, ...packages] : packages;
65365
+ },
65366
+ observe: (items) => [
65367
+ "if command -v brew >/dev/null 2>&1; then",
65368
+ ` ff_state "{\\"provider\\":\\"${name}\\",\\"fact\\":\\"brew\\",\\"version\\":\\"$(ff_json "$(brew --version 2>/dev/null | head -n 1 | sed 's/^Homebrew //')")\\"}"`,
65369
+ ` ${report(items.flatMap((i) => i.kind === "package" ? [i.name] : []))}`,
65370
+ "else",
65371
+ ` ff_state '{"provider":"${name}","fact":"brew","version":null}'`,
65372
+ "fi"
65373
+ ].join("\n"),
65374
+ parse(facts) {
65375
+ let brew;
65376
+ const at = /* @__PURE__ */ new Map();
65377
+ for (const raw of facts) {
65378
+ const fact = decodeFact$2(raw);
65379
+ if (fact._tag === "None") continue;
65380
+ const f = fact.value;
65381
+ if (f.fact === "brew") brew = f.version ?? void 0;
65382
+ else at.set(f.item, f.at);
65383
+ }
65384
+ return {
65385
+ ...brew === void 0 ? {} : { brew },
65386
+ at
65387
+ };
65388
+ },
65389
+ resolve: () => void_,
65390
+ decide({ items, observed, ctx }) {
65391
+ return items.map((item) => {
65392
+ const installed = observed.at.get(item.name);
65393
+ if (item.kind === "homebrew") return observed.brew === void 0 ? {
65394
+ kind: "fail",
65395
+ item: item.name,
65396
+ reason: NO_BREW
65397
+ } : {
65398
+ kind: "leave",
65399
+ item: item.name,
65400
+ at: observed.brew
65401
+ };
65402
+ if (name === "cask" && ctx.machine.platform !== "darwin") return {
65403
+ kind: "fail",
65404
+ item: item.name,
65405
+ reason: "Casks need macOS"
65406
+ };
65407
+ if (observed.brew === void 0) return name === "brew" ? {
65408
+ kind: "skip",
65409
+ item: item.name,
65410
+ reason: "needs Homebrew"
65411
+ } : {
65412
+ kind: "fail",
65413
+ item: item.name,
65414
+ reason: NO_BREW
65415
+ };
65416
+ if (installed === void 0) return {
65417
+ kind: "change",
65418
+ item: item.name,
65419
+ to: "latest",
65420
+ change: {
65421
+ kind: "install",
65422
+ name: item.name,
65423
+ range: item.range
65424
+ }
65425
+ };
65426
+ if (item.range === "latest") return {
65427
+ kind: "change",
65428
+ item: item.name,
65429
+ from: installed,
65430
+ to: "latest",
65431
+ change: {
65432
+ kind: "upgrade",
65433
+ name: item.name,
65434
+ range: item.range
65435
+ }
65436
+ };
65437
+ return {
65438
+ kind: "leave",
65439
+ item: item.name,
65440
+ at: installed
65441
+ };
65442
+ });
65443
+ },
65444
+ act(decisions) {
65445
+ const changes = decisions.flatMap((d) => d.kind === "change" ? [d.change] : []);
65446
+ return ["install", "upgrade"].flatMap((what) => {
65447
+ const batch = changes.filter((c) => c.kind === what);
65448
+ return batch.length === 0 ? [] : [step(what, batch)];
65449
+ });
65450
+ }
65451
+ };
65452
+ }
65453
+ const brew = homebrew("brew");
65454
+ const cask = homebrew("cask");
65455
+ const decodeFact$1 = decodeUnknownOption(Union([
65456
+ Struct({
65457
+ fact: Literal("cloneRoot"),
65458
+ path: String$5,
65459
+ present: Boolean$4
65460
+ }),
65461
+ Struct({
65462
+ fact: Literal("repo"),
65463
+ name: String$5,
65464
+ present: Boolean$4,
65465
+ head: optionalKey(String$5),
65466
+ origin: optionalKey(String$5)
65467
+ }),
65468
+ Struct({
65469
+ fact: Literal("output"),
65470
+ repo: String$5,
65471
+ path: String$5,
65472
+ fingerprint: NullOr(String$5)
65473
+ })
65474
+ ]));
65475
+ const decodeRecord = decodeUnknownOption(Struct({
65476
+ commit: String$5,
65477
+ path: String$5,
65478
+ install: String$5,
65479
+ files: Record(String$5, String$5),
65480
+ outputs: Record(String$5, String$5)
65481
+ }));
65482
+ const branchKey = (repo) => `${repo.origin}#${repo.ref}`;
65483
+ const cloneRoot = (config, machine) => {
65484
+ const root = config.repos?.cloneRoot;
65485
+ return (root === void 0 ? void 0 : variantFor(root, machine))?.variant.path ?? "~/.local/share/fleetfizz/clones";
65486
+ };
65487
+ const originUrl = (origin) => `https://github.com/${origin}.git`;
65488
+ const short = (commit) => commit.slice(0, 7);
65489
+ const expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? `${home}${path.slice(1)}` : path;
65490
+ const jsonString = encodeSync(fromJsonString(String$5));
65491
+ const jsonRecord = encodeSync(fromJsonString(Record(String$5, String$5)));
65492
+ const lsRemote = fn("lsRemote")(function* (repo, token) {
65493
+ const spawner = yield* ChildProcessSpawner;
65494
+ const auth = token === void 0 ? [] : [
65495
+ "-c",
65496
+ "credential.helper=",
65497
+ "-c",
65498
+ "credential.helper=!f() { echo username=x-access-token; echo \"password=$FF_TOKEN\"; }; f"
65499
+ ];
65500
+ return yield* scoped(gen(function* () {
65501
+ const handle = yield* spawner.spawn(make$12("git", [
65502
+ ...auth,
65503
+ "ls-remote",
65504
+ originUrl(repo.origin),
65505
+ `refs/heads/${repo.ref}`
65506
+ ], {
65507
+ stdout: "pipe",
65508
+ stderr: "pipe",
65509
+ env: {
65510
+ GIT_TERMINAL_PROMPT: "0",
65511
+ ...token === void 0 ? {} : { FF_TOKEN: token }
65512
+ },
65513
+ extendEnv: true
65514
+ }));
65515
+ const [stdout, stderr, code] = yield* all([
65516
+ runCollect(decodeText(handle.stdout)),
65517
+ runCollect(decodeText(handle.stderr)),
65518
+ handle.exitCode
65519
+ ], { concurrency: "unbounded" });
65520
+ if (code !== 0) {
65521
+ const lines = stderr.join("").trim().split("\n");
65522
+ return { error: lines[lines.length - 1] || `git ls-remote exited ${code}` };
65523
+ }
65524
+ const commit = stdout.join("").split(/\s/)[0];
65525
+ return commit === void 0 || commit === "" ? { error: `no branch ${repo.ref} on origin` } : { commit };
65526
+ })).pipe(orElseSucceed(() => ({ error: "could not run git ls-remote" })));
65527
+ });
65528
+ const observe = (items, root) => {
65529
+ if (items.length === 0) return "";
65530
+ const lines = [
65531
+ `ff_root=${quotePath(root)}`,
65532
+ `if [ -d "$ff_root" ]; then ff_present=true; else ff_present=false; fi`,
65533
+ `ff_state "{\\"provider\\":\\"repos\\",\\"fact\\":\\"cloneRoot\\",\\"path\\":\\"$(ff_json "$ff_root")\\",\\"present\\":$ff_present}"`
65534
+ ];
65535
+ for (const repo of items) {
65536
+ const name = quote(repo.name);
65537
+ lines.push(`ff_d="$ff_root"/${name}`, `if [ -d "$ff_d/.git" ]; then`, ` ff_state "{\\"provider\\":\\"repos\\",\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${name})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$(git -C "$ff_d" rev-parse HEAD 2>/dev/null)")\\",\\"origin\\":\\"$(ff_json "$(git -C "$ff_d" remote get-url origin 2>/dev/null)")\\"}"`, "else", ` ff_state "{\\"provider\\":\\"repos\\",\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${name})\\",\\"present\\":false}"`, "fi");
65538
+ for (const output of repo.outputs ?? []) lines.push(`ff_p=${quotePath(output)}; ff_f=$(ff_fp "$ff_p")`, `if [ -n "$ff_f" ]; then ff_fv="\\"$(ff_json "$ff_f")\\""; else ff_fv=null; fi`, `ff_state "{\\"provider\\":\\"repos\\",\\"fact\\":\\"output\\",\\"repo\\":\\"$(ff_json ${name})\\",\\"path\\":\\"$(ff_json ${quote(output)})\\",\\"fingerprint\\":$ff_fv}"`);
65539
+ }
65540
+ return lines.join("\n");
65541
+ };
65542
+ const decideRepo = (repo, root, observed, resolved, ctx) => {
65543
+ const clone = observed.clones.get(repo.name);
65544
+ const record = observed.records.get(repo.name);
65545
+ const at = clone?.head === void 0 ? {} : { at: short(clone.head) };
65546
+ const fail = (reason) => ({
65547
+ kind: "fail",
65548
+ item: repo.name,
65549
+ ...at,
65550
+ reason
65551
+ });
65552
+ if (observed.cloneRoot?.present === false && !root.startsWith("~")) return fail(`clone root not mounted: ${observed.cloneRoot.path}`);
65553
+ const url = originUrl(repo.origin);
65554
+ if (clone?.present && clone.origin !== void 0 && clone.origin !== url) return fail(`managed clone has origin ${clone.origin}, expected ${url}`);
65555
+ const target = isPin(repo.ref) ? { commit: repo.ref } : resolved[branchKey(repo)];
65556
+ if (target === void 0) return fail(`could not resolve ${repo.ref}`);
65557
+ if ("error" in target) return fail(target.error);
65558
+ const { commit } = target;
65559
+ const outputs = observed.outputs.get(repo.name);
65560
+ const install = decideInstall(record, {
65561
+ commit,
65562
+ path: expandHome(`${root}/${repo.name}`, ctx.home),
65563
+ install: repo.install,
65564
+ files: repo.files ?? {},
65565
+ outputs: Object.fromEntries((repo.outputs ?? []).map((o) => [o, outputs?.get(o)]))
65566
+ }, ctx.rebuild(address("repos", repo.name)));
65567
+ const reason = install.run ? install.reason : !clone?.present ? "not cloned" : clone.head !== commit ? `checked out at ${short(clone.head ?? "")}` : void 0;
65568
+ const change = {
65569
+ item: repo,
65570
+ root,
65571
+ commit
65572
+ };
65573
+ if (reason === void 0) return {
65574
+ kind: "leave",
65575
+ item: repo.name,
65576
+ at: short(commit),
65577
+ upkeep: change
65578
+ };
65579
+ const from = record?.commit ?? clone?.head;
65580
+ return {
65581
+ kind: "change",
65582
+ item: repo.name,
65583
+ ...from === void 0 ? {} : { from: short(from) },
65584
+ to: short(commit),
65585
+ reason,
65586
+ change
65587
+ };
65588
+ };
65589
+ const fetchStep = ({ item, root, commit }) => {
65590
+ const url = originUrl(item.origin);
65591
+ const sync = isPin(item.ref) ? [
65592
+ `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(commit)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(commit)}; fi`,
65593
+ `git -C "$ff_d" checkout --quiet --detach ${quote(commit)}`,
65594
+ `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`
65595
+ ] : [`ff_git -C "$ff_d" fetch --quiet origin ${quote(item.ref)}`, `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`];
65596
+ const files = Object.entries(item.files ?? {}).flatMap(([path, content]) => [`mkdir -p "$(dirname "$ff_d"/${quote(path)})"`, `printf '%s' ${quote(content)} > "$ff_d"/${quote(path)}`]);
65597
+ return {
65598
+ id: `repos:${item.name}:fetch`,
65599
+ items: [item.name],
65600
+ doing: "fetching",
65601
+ run: [
65602
+ `ff_root=${quotePath(root)}`,
65603
+ ...root.startsWith("~") ? [`mkdir -p "$ff_root"`] : [],
65604
+ `ff_d="$ff_root"/${quote(item.name)}`,
65605
+ "(",
65606
+ " set -e",
65607
+ ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
65608
+ ` if [ "$(git -C "$ff_d" remote get-url origin)" != ${quote(url)} ]; then echo "origin is $(git -C "$ff_d" remote get-url origin), expected ${url}"; exit 1; fi`,
65609
+ ...[
65610
+ ...sync,
65611
+ `git -C "$ff_d" clean --quiet -fd`,
65612
+ ...files
65613
+ ].map((l) => ` ${l}`),
65614
+ ")"
65615
+ ]
65616
+ };
65617
+ };
65618
+ const installStep = ({ item, root }, ctx) => {
65619
+ const env = [
65620
+ `FLEETFIZZ_MACHINE=${quote(ctx.machine.name)}`,
65621
+ `FLEETFIZZ_PLATFORM=${quote(ctx.machine.platform)}`,
65622
+ `FLEETFIZZ_MACHINES=${quote(ctx.config.machines.join(","))}`
65623
+ ].join(" ");
65624
+ const tmp = `${recordDir("repos")}/${quote(`${item.name}.json.tmp`)}`;
65625
+ const record = [
65626
+ `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
65627
+ `ff_outs=""`,
65628
+ ...(item.outputs ?? []).map((o) => `ff_fpv=$(ff_fp ${quotePath(o)}); if [ -n "$ff_fpv" ]; then ff_outs="$ff_outs,\\"$(ff_json ${quote(o)})\\":\\"$(ff_json "$ff_fpv")\\""; fi`),
65629
+ `mkdir -p ${recordDir("repos")}`,
65630
+ `printf '%s\\n' "{\\"commit\\":\\"$ff_after\\",\\"path\\":\\"$(ff_json "$ff_d")\\",\\"install\\":$(ff_lit ${quote(jsonString(item.install))}),\\"files\\":$(ff_lit ${quote(jsonRecord(item.files ?? {}))}),\\"outputs\\":{\${ff_outs#,}}}" > ${tmp}`,
65631
+ `mv -f ${tmp} ${recordFile("repos", item.name)}`,
65632
+ `rm -f ${quotePath(RECORDS_DIR)}/${quote(`${item.name}.json`)}`
65633
+ ];
65634
+ return {
65635
+ id: `repos:${item.name}:install`,
65636
+ items: [item.name],
65637
+ doing: "installing",
65638
+ run: [
65639
+ `ff_root=${quotePath(root)}`,
65640
+ `ff_d="$ff_root"/${quote(item.name)}`,
65641
+ `(cd "$ff_d" && ${env} "\${SHELL:-/bin/sh}" -lc ${quote(item.install)})`,
65642
+ `ff_rc=$?`,
65643
+ `if [ "$ff_rc" -eq 0 ]; then`,
65644
+ " (",
65645
+ " set -e",
65646
+ ...record.map((l) => ` ${l}`),
65647
+ " )",
65648
+ ` ff_rc=$?`,
65649
+ ` if [ "$ff_rc" -ne 0 ]; then echo "could not write the install record"; fi`,
65650
+ "fi",
65651
+ `(exit "$ff_rc")`
65652
+ ]
65653
+ };
65654
+ };
65655
+ const repos = {
65656
+ name: "repos",
65657
+ rule: "record",
65658
+ dependsOn: ["vp"],
65659
+ items: (_section, listed) => listed.map(({ name, def }) => ({
65660
+ ...def,
65661
+ name
65662
+ })),
65663
+ observe: (items, scope) => observe(items, cloneRoot(scope.config, scope.machine)),
65664
+ parse(facts, records) {
65665
+ let root;
65666
+ const clones = /* @__PURE__ */ new Map();
65667
+ const outputs = /* @__PURE__ */ new Map();
65668
+ for (const raw of facts) {
65669
+ const fact = decodeFact$1(raw);
65670
+ if (fact._tag === "None") continue;
65671
+ const f = fact.value;
65672
+ switch (f.fact) {
65673
+ case "cloneRoot":
65674
+ root = {
65675
+ path: f.path,
65676
+ present: f.present
65677
+ };
65678
+ break;
65679
+ case "repo":
65680
+ clones.set(f.name, {
65681
+ present: f.present,
65682
+ ...f.head === void 0 || f.head === "" ? {} : { head: f.head },
65683
+ ...f.origin === void 0 || f.origin === "" ? {} : { origin: f.origin }
65684
+ });
65685
+ break;
65686
+ case "output": {
65687
+ const paths = outputs.get(f.repo) ?? /* @__PURE__ */ new Map();
65688
+ paths.set(f.path, f.fingerprint ?? void 0);
65689
+ outputs.set(f.repo, paths);
65690
+ break;
65691
+ }
65692
+ }
65693
+ }
65694
+ const decoded = /* @__PURE__ */ new Map();
65695
+ for (const [name, raw] of Object.entries(records)) {
65696
+ const record = decodeRecord(raw);
65697
+ if (record._tag === "Some") decoded.set(name, record.value);
65698
+ }
65699
+ return {
65700
+ ...root === void 0 ? {} : { cloneRoot: root },
65701
+ clones,
65702
+ outputs,
65703
+ records: decoded
65704
+ };
65705
+ },
65706
+ resolve: (items, { token }) => forEach$1([...new Map(items.filter((r) => !isPin(r.ref)).map((r) => [branchKey(r), r])).entries()], ([key, repo]) => lsRemote(repo, token).pipe(map$5((result) => [key, result])), { concurrency: 8 }).pipe(map$5((entries) => Object.fromEntries(entries))),
65707
+ decide: ({ items, observed, resolved, ctx }) => {
65708
+ const root = cloneRoot(ctx.config, ctx.machine);
65709
+ return items.map((repo) => decideRepo(repo, root, observed, resolved, ctx));
65710
+ },
65711
+ act: (decisions, ctx) => decisions.flatMap((d) => d.kind === "change" ? [fetchStep(d.change), installStep(d.change, ctx)] : d.kind === "leave" && d.upkeep !== void 0 ? [fetchStep(d.upkeep)] : [])
65712
+ };
65713
+ //#endregion
65714
+ //#region ../../packages/providers/src/vp/index.ts
65715
+ const VP_PACKAGE = "vite-plus";
65716
+ const TOOL_PACKAGES = {
65717
+ node: ["node"],
65718
+ npm: ["npm"],
65719
+ pnpm: ["pnpm"],
65720
+ yarn: ["yarn", "@yarnpkg/cli-dist"],
65721
+ bun: ["bun"]
65722
+ };
65723
+ const GlobalEntry = Struct({
65724
+ name: String$5,
65725
+ version: String$5,
65726
+ platform: optionalKey(Struct({ node: optionalKey(String$5) }))
65727
+ });
65728
+ const decodeFact = decodeUnknownOption(Union([
65729
+ Struct({
65730
+ fact: Literal("vp"),
65731
+ version: NullOr(String$5),
65732
+ path: optionalKey(String$5)
65733
+ }),
65734
+ Struct({
65735
+ fact: Literal("default"),
65736
+ tool: String$5,
65737
+ version: String$5
65738
+ }),
65739
+ Struct({
65740
+ fact: Literal("mode"),
65741
+ mode: Literals$2(["managed", "system-first"])
65742
+ }),
65743
+ Struct({
65744
+ fact: Literal("globals"),
65745
+ list: ArraySchema(GlobalEntry)
65746
+ })
65747
+ ]));
65748
+ const VP_INSTALL = [
65749
+ "curl -fsSL https://vite.plus | bash",
65750
+ ...vpEnv.split("\n"),
65751
+ "{ hash -r 2>/dev/null || true; }"
65752
+ ].join(" && ");
65753
+ const isHomebrew = (path) => path !== void 0 && (path.includes("/homebrew/") || path.includes("/Cellar/"));
65754
+ const OBSERVE = [
65755
+ "if command -v vp >/dev/null 2>&1; then",
65756
+ ` ff_v=$(command vp --version 2>/dev/null | sed -n 's/^vp v\\{0,1\\}\\([^ ]*\\).*/\\1/p' | head -n 1)`,
65757
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"vp\\",\\"version\\":\\"$(ff_json "$ff_v")\\",\\"path\\":\\"$(ff_json "$(command -v vp)")\\"}"`,
65758
+ " if command vp env doctor 2>/dev/null | grep -q 'system-first mode'; then ff_m=system-first; else ff_m=managed; fi",
65759
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"mode\\",\\"mode\\":\\"$ff_m\\"}"`,
65760
+ ` command vp env default 2>/dev/null | sed -n -e 's/^Default Node.js version: /node /p' -e 's/^Default \\([a-z]*\\) version: /\\1 /p' | while read -r ff_t ff_n; do`,
65761
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"default\\",\\"tool\\":\\"$(ff_json "$ff_t")\\",\\"version\\":\\"$(ff_json "$ff_n")\\"}"`,
65762
+ " done",
65763
+ ` ff_g=$(command vp list --global --json 2>/dev/null | tr -d '\\n')`,
65764
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"globals\\",\\"list\\":\${ff_g:-[]}}"`,
65765
+ "else",
65766
+ ` ff_state '{"provider":"vp","fact":"vp","version":null}'`,
65767
+ "fi"
65768
+ ].join("\n");
65769
+ const decideSelf = (item, installed, self) => {
65770
+ const at = installed === void 0 ? {} : { at: installed };
65771
+ if (installed === void 0 || import_semver.lt(installed, self)) return {
65772
+ kind: "change",
65773
+ item,
65774
+ ...installed === void 0 ? {} : { from: installed },
65775
+ to: self,
65776
+ change: {
65777
+ kind: "global",
65778
+ name: item,
65779
+ version: self
65780
+ }
65781
+ };
65782
+ if (import_semver.gt(installed, self)) return {
65783
+ kind: "fail",
65784
+ item,
65785
+ ...at,
65786
+ reason: `runs ${SELF_PACKAGE} ${installed}, newer than this ${self}; upgrade here first`
65787
+ };
65788
+ return {
65789
+ kind: "leave",
65790
+ item,
65791
+ ...at
65792
+ };
65793
+ };
65794
+ const MODE_SUBJECT = address("vp", "mode");
65795
+ const NOT_MANAGED = "needs managed mode";
65796
+ const decideMode = (observed, ctx) => {
65797
+ if (observed.vp !== void 0 && observed.mode !== "system-first") return {
65798
+ kind: "leave",
65799
+ item: "mode",
65800
+ at: "managed"
65801
+ };
65802
+ const from = observed.mode === void 0 ? {} : { from: observed.mode };
65803
+ const answer = observed.vp === void 0 ? "managed" : ctx.answer("mode", MODE_SUBJECT);
65804
+ if (answer === "managed") return {
65805
+ kind: "change",
65806
+ item: "mode",
65807
+ ...from,
65808
+ to: "managed",
65809
+ change: { kind: "mode" }
65810
+ };
65811
+ if (answer === "skip") return {
65812
+ kind: "skip",
65813
+ item: "mode",
65814
+ at: "system-first",
65815
+ reason: NOT_MANAGED
65816
+ };
65817
+ return {
65818
+ kind: "ask",
65819
+ item: "mode",
65820
+ at: "system-first",
65821
+ question: {
65822
+ kind: "mode",
65823
+ subject: MODE_SUBJECT,
65824
+ found: "system-first",
65825
+ problem: NOT_MANAGED,
65826
+ choices: [{
65827
+ value: "managed",
65828
+ title: "switch to managed mode"
65829
+ }, {
65830
+ value: "skip",
65831
+ title: "skip Node and package managers on these Machines"
65832
+ }],
65833
+ default: "skip"
65834
+ }
65835
+ };
65836
+ };
65837
+ const decideRanged = (item, observed, resolved, ctx) => {
65838
+ const installed = item.kind === "vp" ? observed.vp?.version : item.kind === "default" ? observed.defaults.get(item.name) : observed.globals.get(item.name)?.version;
65839
+ const decision = decideRangeItem({
65840
+ provider: "vp",
65841
+ item: item.name,
65842
+ range: item.range,
65843
+ installed,
65844
+ available: item.packages.flatMap((p) => resolved[p] ?? []),
65845
+ bumpPath: item.bumpPath,
65846
+ ctx
65847
+ });
65848
+ if (item.kind === "vp" && observed.vp === void 0) {
65849
+ if (decision.kind === "change") return {
65850
+ ...decision,
65851
+ change: {
65852
+ kind: "bootstrap",
65853
+ version: decision.change
65854
+ }
65855
+ };
65856
+ if (decision.kind === "fail" && (item.range === "*" || item.range === "latest")) return {
65857
+ kind: "change",
65858
+ item: item.name,
65859
+ to: "latest",
65860
+ change: { kind: "bootstrap" }
65861
+ };
65862
+ return decision;
65863
+ }
65864
+ if (decision.kind !== "change") return decision;
65865
+ const version = decision.change;
65866
+ if (item.kind === "vp" && isHomebrew(observed.vp?.path)) return {
65867
+ kind: "fail",
65868
+ item: item.name,
65869
+ ...installed === void 0 ? {} : { at: installed },
65870
+ reason: `vp ${installed} was installed with Homebrew; run brew upgrade vite-plus to get ${version}`
65871
+ };
65872
+ const change = item.kind === "vp" ? {
65873
+ kind: "upgrade",
65874
+ version
65875
+ } : item.kind === "default" ? {
65876
+ kind: "default",
65877
+ tool: item.name,
65878
+ version
65879
+ } : {
65880
+ kind: "global",
65881
+ name: item.name,
65882
+ version
65883
+ };
65884
+ return {
65885
+ ...decision,
65886
+ change
65887
+ };
65888
+ };
65889
+ //#endregion
65890
+ //#region ../../packages/providers/src/index.ts
65891
+ const PROVIDERS = [
65892
+ {
65893
+ name: "vp",
65894
+ rule: "range",
65895
+ dependsOn: [],
65896
+ items(section, listed) {
65897
+ const globals = listed.map(({ name, def, path }) => ({
65898
+ name,
65899
+ kind: "global",
65900
+ packages: [name],
65901
+ range: def.range,
65902
+ bumpPath: [...path, "range"]
65903
+ }));
65904
+ const defaults = ["node", ...PACKAGE_MANAGERS].flatMap((tool) => {
65905
+ const range = section?.[tool];
65906
+ return range === void 0 ? [] : [{
65907
+ name: tool,
65908
+ kind: "default",
65909
+ packages: TOOL_PACKAGES[tool],
65910
+ range,
65911
+ bumpPath: ["vp", tool]
65912
+ }];
65913
+ });
65914
+ return [
65915
+ {
65916
+ name: "vp",
65917
+ kind: "vp",
65918
+ packages: [VP_PACKAGE],
65919
+ range: section?.version ?? "*",
65920
+ bumpPath: ["vp", "version"]
65921
+ },
65922
+ {
65923
+ name: "mode",
65924
+ kind: "mode"
65925
+ },
65926
+ ...defaults,
65927
+ ...globals,
65928
+ {
65929
+ name: SELF_PACKAGE,
65930
+ kind: "self"
65931
+ }
65932
+ ];
65933
+ },
65934
+ observe: () => OBSERVE,
65935
+ parse(facts) {
65936
+ let vpFact;
65937
+ let mode;
65938
+ const defaults = /* @__PURE__ */ new Map();
65939
+ let globals = [];
65940
+ for (const raw of facts) {
65941
+ const fact = decodeFact(raw);
65942
+ if (fact._tag === "None") continue;
65943
+ const f = fact.value;
65944
+ if (f.fact === "vp" && f.version !== null) vpFact = {
65945
+ version: f.version,
65946
+ ...f.path === void 0 ? {} : { path: f.path }
65947
+ };
65948
+ else if (f.fact === "mode") mode = f.mode;
65949
+ else if (f.fact === "default") defaults.set(f.tool, f.version);
65950
+ else if (f.fact === "globals") globals = f.list;
65951
+ }
65952
+ if (vpFact === void 0) return {
65953
+ defaults: /* @__PURE__ */ new Map(),
65954
+ globals: /* @__PURE__ */ new Map()
65955
+ };
65956
+ return {
65957
+ vp: vpFact,
65958
+ ...mode === void 0 ? {} : { mode },
65959
+ defaults,
65960
+ globals: new Map(globals.map((g) => [g.name, g]))
65961
+ };
65962
+ },
65963
+ resolve: (items) => resolveVersions(items.flatMap((item) => "packages" in item ? item.packages : [])),
65964
+ decide({ items, observed, resolved, ctx }) {
65965
+ const mode = decideMode(observed, ctx);
65966
+ const managed = mode.kind === "leave" || mode.kind === "change";
65967
+ const decisions = items.map((item) => {
65968
+ switch (item.kind) {
65969
+ case "mode": return mode;
65970
+ case "self": return decideSelf(item.name, observed.globals.get(item.name)?.version, ctx.self);
65971
+ case "default":
65972
+ if (!managed) {
65973
+ const at = observed.defaults.get(item.name);
65974
+ return {
65975
+ kind: "skip",
65976
+ item: item.name,
65977
+ ...at === void 0 ? {} : { at },
65978
+ reason: NOT_MANAGED
65979
+ };
65980
+ }
65981
+ return decideRanged(item, observed, resolved, ctx);
65982
+ default: return decideRanged(item, observed, resolved, ctx);
65983
+ }
65984
+ });
65985
+ const nodeChange = decisions.find((d) => d.kind === "change" && d.change.kind === "default" && d.change.tool === "node");
65986
+ const nodeDefault = nodeChange?.kind === "change" ? nodeChange.to : observed.defaults.get("node");
65987
+ return decisions.map((d, i) => {
65988
+ const item = items[i];
65989
+ const current = observed.globals.get(d.item);
65990
+ const node = current?.platform?.node;
65991
+ if (d.kind !== "leave" || item === void 0 || item.kind !== "global" && item.kind !== "self" || current === void 0 || node === void 0 || nodeDefault === void 0 || node === nodeDefault) return d;
65992
+ return {
65993
+ kind: "change",
65994
+ item: d.item,
65995
+ from: current.version,
65996
+ to: current.version,
65997
+ reason: `Node ${node} → ${nodeDefault}`,
65998
+ change: {
65999
+ kind: "reinstall",
66000
+ name: d.item,
66001
+ version: current.version
66002
+ }
66003
+ };
66004
+ });
66005
+ },
66006
+ act(decisions) {
66007
+ const changes = decisions.flatMap((d) => d.kind === "change" ? [d.change] : []);
66008
+ const steps = [];
66009
+ for (const change of changes) switch (change.kind) {
66010
+ case "bootstrap":
66011
+ steps.push({
66012
+ id: "vp:vp:install",
66013
+ items: ["vp"],
66014
+ doing: "installing",
66015
+ run: [change.version === void 0 ? VP_INSTALL : `${VP_INSTALL} && command vp upgrade ${quote(change.version)}`]
66016
+ });
66017
+ break;
66018
+ case "upgrade":
66019
+ steps.push({
66020
+ id: "vp:vp:upgrade",
66021
+ items: ["vp"],
66022
+ doing: "upgrading",
66023
+ run: [`( command vp upgrade ${quote(change.version)} )`]
66024
+ });
66025
+ break;
66026
+ case "mode":
66027
+ steps.push({
66028
+ id: "vp:mode:on",
66029
+ items: ["mode"],
66030
+ doing: "switching",
66031
+ run: ["( command vp env on )"]
66032
+ });
66033
+ break;
66034
+ case "default":
66035
+ steps.push({
66036
+ id: `vp:${change.tool}:default`,
66037
+ items: [change.tool],
66038
+ doing: "installing",
66039
+ run: [`( command vp env default ${quote(change.tool === "node" ? change.version : `${change.tool}@${change.version}`)} )`]
66040
+ });
66041
+ break;
66042
+ case "reinstall": steps.push({
66043
+ id: `vp:${change.name}:reinstall`,
66044
+ items: [change.name],
66045
+ doing: "reinstalling",
66046
+ run: [`( command vp install -g ${quote(`${change.name}@${change.version}`)} )`]
66047
+ });
66048
+ }
66049
+ const installs = changes.flatMap((c) => c.kind === "global" ? [c] : []);
66050
+ if (installs.length > 0) steps.push({
66051
+ id: "vp:globals:install",
66052
+ items: installs.map((c) => c.name),
66053
+ doing: "installing",
66054
+ run: [`( command vp install -g ${installs.map((c) => quote(`${c.name}@${c.version}`)).join(" ")} )`]
66055
+ });
66056
+ return steps;
66057
+ }
66058
+ },
66059
+ brew,
66060
+ cask,
66061
+ repos
66062
+ ];
65190
66063
  //#endregion
65191
66064
  //#region src/live.tsx
65192
66065
  const cell = (initial) => {
@@ -65272,6 +66145,7 @@ const noLive = { ask: void 0 };
65272
66145
  const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
65273
66146
  //#endregion
65274
66147
  //#region src/machine.ts
66148
+ const decodeAt = decodeUnknownOption(AtFact);
65275
66149
  const runOn = fn("runOn")(function* (target, script, secrets, onLine, ledger) {
65276
66150
  let first;
65277
66151
  const run = yield* runScript(target, script, secrets, (line) => {
@@ -65341,10 +66215,17 @@ const apply = fn("apply")(function* (target, p, script, secrets, ledger) {
65341
66215
  });
65342
66216
  break;
65343
66217
  }
66218
+ case "skip":
66219
+ yield* ledger.stepSkipped(target.name, line.skip.id, line.skip.reason);
66220
+ break;
65344
66221
  case "text":
65345
66222
  if (line.text.trim() !== "") (current?.output ?? head).push(line.text.trimEnd());
65346
66223
  break;
65347
- case "state": if (line.fact.fact === "repo" && line.fact.head !== void 0) yield* ledger.fetched(target.name, line.fact.name, line.fact.head);
66224
+ case "state": {
66225
+ const at = decodeAt(line.fact);
66226
+ if (at._tag === "Some") yield* ledger.reached(target.name, address(at.value.provider, at.value.item), at.value.at);
66227
+ break;
66228
+ }
65348
66229
  }
65349
66230
  }), ledger);
65350
66231
  const finish = fn("finish")(function* (outcome) {
@@ -65365,43 +66246,63 @@ const apply = fn("apply")(function* (target, p, script, secrets, ledger) {
65365
66246
  yield* ledger.fail(target.name, ["apply did not finish", ...head]);
65366
66247
  return yield* finish("failed");
65367
66248
  }
65368
- return yield* finish(failed.length > 0 || p.failures.length > 0 ? "failed" : "ok");
66249
+ const planFailed = p.providers.some((pp) => pp.decisions.some((d) => d.kind === "fail"));
66250
+ return yield* finish(failed.length > 0 || planFailed ? "failed" : "ok");
65369
66251
  });
65370
66252
  //#endregion
65371
66253
  //#region src/questions.ts
65372
- const newest = (group) => [...group].map((q) => q.installed).sort((a, b) => b.localeCompare(a, void 0, { numeric: true }))[0] ?? "";
65373
- const askQuestions = fn("askQuestions")(function* (questions, flags, ledger, ask) {
65374
- const groups = /* @__PURE__ */ new Map();
65375
- for (const q of questions) groups.set(q.package, [...groups.get(q.package) ?? [], q]);
65376
- const decided = {};
65377
- for (const [pkg, group] of groups) {
65378
- const where = group.map((q) => `${q.installed} on ${q.machine}`).join(", ");
65379
- const range = group[0]?.range ?? "";
65380
- const bump = {
65381
- kind: "bump",
65382
- installed: newest(group)
65383
- };
65384
- const bumpTitle = `bump the config to ${bumpedRange(range, newest(group))}`;
65385
- const answer = (decision) => decision.kind === "bump" ? bumpTitle : decision.kind === "skip" ? "skip on these Machines" : "downgrade into the range";
65386
- yield* ledger.note("ask", `${pkg} ${where} is above ${range}`);
65387
- const decision = flags.bump ? bump : flags.enforce ? { kind: "downgrade" } : ask === void 0 ? { kind: "skip" } : yield* ask([
65388
- {
65389
- title: bumpTitle,
65390
- value: bump
65391
- },
65392
- {
65393
- title: "skip on these Machines (leave as is, shown as skipped)",
65394
- value: { kind: "skip" }
65395
- },
65396
- {
65397
- title: "downgrade into the range",
65398
- value: { kind: "downgrade" }
65399
- }
65400
- ]);
65401
- yield* ledger.note("answer", answer(decision));
65402
- decided[pkg] = decision;
66254
+ const parseAnswers = (raw) => {
66255
+ const given = /* @__PURE__ */ new Map();
66256
+ for (const entry of raw) {
66257
+ const at = entry.lastIndexOf("=");
66258
+ const key = entry.slice(0, at);
66259
+ const choice = entry.slice(at + 1);
66260
+ const kind = key.split(":")[0] ?? "";
66261
+ if (at <= 0 || !isQuestionKind(kind)) return { invalid: `--answer ${entry}: expected kind=choice or kind:subject=choice, kind one of ${Object.keys(QUESTION_CHOICES).join(", ")}` };
66262
+ const choices = QUESTION_CHOICES[kind];
66263
+ if (!choices.includes(choice)) return { invalid: `--answer ${entry}: ${kind} takes ${choices.join(", ")}` };
66264
+ given.set(key, choice);
66265
+ }
66266
+ return { answer: (kind, subject) => given.get(`${kind}:${subject}`) ?? given.get(kind) };
66267
+ };
66268
+ const newestOf = (found) => found.toSorted((a, b) => b.localeCompare(a, void 0, { numeric: true }))[0] ?? "";
66269
+ const askQuestions = fn("askQuestions")(function* (input) {
66270
+ const groups = Map.groupBy(input.asked, (a) => `${a.question.kind}:${a.question.subject}`);
66271
+ const answered = [];
66272
+ for (const group of groups.values()) {
66273
+ const question = group[0]?.question;
66274
+ if (question === void 0) continue;
66275
+ const bumps = [...Map.groupBy(group.flatMap(({ question: q }) => q.bump === void 0 ? [] : [{
66276
+ ...q.bump,
66277
+ found: q.found
66278
+ }]), (bump) => JSON.stringify(bump.path)).values()].flatMap((list) => {
66279
+ const first = list[0];
66280
+ return first === void 0 ? [] : [{
66281
+ path: first.path,
66282
+ range: first.range,
66283
+ newest: newestOf(list.map((b) => b.found))
66284
+ }];
66285
+ });
66286
+ const where = [...Map.groupBy(group, (a) => a.question.problem)].map(([problem, list]) => `${list.map((a) => `${a.question.found} on ${a.machine}`).join(", ")} ${problem}`).join("; ");
66287
+ yield* input.ledger.note("ask", `${question.subject} ${where}`);
66288
+ if (input.dryRun) {
66289
+ yield* input.ledger.note("answer", "not asked (dry run)");
66290
+ continue;
66291
+ }
66292
+ const choices = question.choices.map((c) => ({
66293
+ value: c.value,
66294
+ title: c.value === "bump" && bumps.length > 0 ? `bump the config to ${bumps.map((b) => bumpedRange(b.range, b.newest)).join(" and ")}` : c.title
66295
+ }));
66296
+ const answer = input.answer(question.kind, question.subject) ?? (input.ask === void 0 ? question.default : yield* input.ask(choices));
66297
+ yield* input.ledger.note("answer", choices.find((c) => c.value === answer)?.title ?? answer);
66298
+ answered.push({
66299
+ question,
66300
+ machines: group.map((a) => a.machine),
66301
+ bumps,
66302
+ answer
66303
+ });
65403
66304
  }
65404
- return decided;
66305
+ return answered;
65405
66306
  });
65406
66307
  //#endregion
65407
66308
  //#region src/ship.ts
@@ -65411,23 +66312,20 @@ var ShipError = class extends Error {
65411
66312
  const unknownMachines = (config, tailnet, only) => {
65412
66313
  const known = new Set(tailnet.fleet.map((m) => m.name));
65413
66314
  const referenced = [
65414
- ...Object.keys(config.machines),
65415
- ...(config.repos ?? []).flatMap((r) => r.machines ?? []),
65416
- ...config.vp?.machines ?? [],
66315
+ ...config.machines,
66316
+ ...PROVIDERS.flatMap((p) => machinesNamed(p, config)),
65417
66317
  ...only
65418
66318
  ];
65419
66319
  return [...new Set(referenced.filter((name) => !known.has(name)))];
65420
66320
  };
65421
- const packagesToResolve = (config) => {
65422
- const vp = config.vp;
65423
- if (vp === void 0) return [];
65424
- return [
65425
- ...vp.version === void 0 ? [] : [VP_PACKAGE],
65426
- ...vp.node === void 0 ? [] : [NODE_PACKAGE],
65427
- ...Object.keys(vp.globals ?? {})
65428
- ];
66321
+ const unshippable = (config, tailnet, only, source) => {
66322
+ const unknown = unknownMachines(config, tailnet, only);
66323
+ if (unknown.length > 0) return `unknown Machine: ${unknown.join(", ")} (the tailnet has: ${tailnet.fleet.map((m) => m.name).join(", ")})`;
66324
+ const fleet = new Set(config.machines);
66325
+ if (!fleet.has(tailnet.self.name)) return `this Machine (${tailnet.self.name}) is not in machines in ${source}`;
66326
+ const outsideFleet = only.filter((name) => !fleet.has(name));
66327
+ if (outsideFleet.length > 0) return `not in machines in ${source}: ${outsideFleet.join(", ")}`;
65429
66328
  };
65430
- const rebuildOf = (flags) => flags.rebuild.length === 0 ? "none" : flags.rebuild.includes("all") ? "all" : flags.rebuild;
65431
66329
  const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger) {
65432
66330
  const copies = [];
65433
66331
  for (const { machine, state } of checked) {
@@ -65436,25 +66334,44 @@ const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger)
65436
66334
  if (parsed._tag === "Failure") yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
65437
66335
  else copies.push({
65438
66336
  machine: machine.name,
65439
- config: parsed.success
66337
+ config: parsed.success,
66338
+ text: state.config
65440
66339
  });
65441
66340
  }
65442
- return staleCopies(config, copies, own).map((c) => `${c.machine} (shipped ${c.config.shippedAt} from ${c.config.shippedFrom ?? "unknown"})`);
66341
+ return [...Map.groupBy(staleCopies(config, copies, own), (c) => c.text)].flatMap(([text, [first, ...rest]]) => {
66342
+ if (first === void 0) return [];
66343
+ const { config, machine } = first;
66344
+ const machines = [machine, ...rest.map((c) => c.machine)];
66345
+ return [{
66346
+ text,
66347
+ config,
66348
+ machines,
66349
+ description: `${machines.join(", ")} (shipped ${config.shippedAt} from ${config.shippedFrom ?? "unknown"})`
66350
+ }];
66351
+ });
65443
66352
  });
65444
66353
  const schemaJson = encodeSync(fromJsonString(Unknown, { space: 2 }));
65445
66354
  const ship = fn("ship")(function* (flags) {
66355
+ while ((yield* shipPass(flags)) === "adopted");
66356
+ });
66357
+ const shipPass = fn("shipPass")(function* (flags) {
65446
66358
  const fs = yield* FileSystem;
66359
+ const parsed = parseAnswers(flags.answers);
66360
+ if ("invalid" in parsed) return yield* fail$1(new ShipError(parsed.invalid));
66361
+ const given = parsed.answer;
66362
+ const notAddress = flags.rebuild.find((r) => r !== "all" && !PROVIDERS.some((p) => p.rule === "record" && p.name === parseAddress(r)?.provider));
66363
+ if (notAddress !== void 0) return yield* fail$1(new ShipError(`--rebuild ${notAddress}: expected a record Item (repos:<name>) or all`));
65447
66364
  const path = flags.config ?? configPath(process.env);
65448
66365
  const config = yield* loadConfig(path);
66366
+ const ordered = orderProviders(PROVIDERS, (p) => dependenciesOf(p, config));
66367
+ if ("cycle" in ordered) return yield* fail$1(new ShipError(`dependsOn cycle: ${ordered.cycle.join(" → ")}`));
66368
+ const providers = ordered.order;
65449
66369
  const tailnet = yield* readTailnet();
65450
- const unknown = unknownMachines(config, tailnet, flags.only);
65451
- if (unknown.length > 0) return yield* fail$1(new ShipError(`unknown Machine: ${unknown.join(", ")} (the tailnet has: ${tailnet.fleet.map((m) => m.name).join(", ")})`));
65452
- const fleet = new Set(Object.keys(config.machines));
66370
+ const problem = unshippable(config, tailnet, flags.only, path);
66371
+ if (problem !== void 0) return yield* fail$1(new ShipError(problem));
66372
+ const fleet = new Set(config.machines);
65453
66373
  const own = flags.config === void 0 ? tailnet.self.name : void 0;
65454
- if (!fleet.has(tailnet.self.name)) return yield* fail$1(new ShipError(`this Machine (${tailnet.self.name}) is not in machines in ${path}`));
65455
- const outsideFleet = flags.only.filter((name) => !fleet.has(name));
65456
- if (outsideFleet.length > 0) return yield* fail$1(new ShipError(`not in machines in ${path}: ${outsideFleet.join(", ")}`));
65457
- const order = Object.keys(config.machines);
66374
+ const order = config.machines;
65458
66375
  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));
65459
66376
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
65460
66377
  yield* log(`${flags.dryRun ? "dry run, " : ""}${count} from ${tailnet.self.name}`);
@@ -65467,8 +66384,10 @@ const ship = fn("ship")(function* (flags) {
65467
66384
  });
65468
66385
  const redraw = canRedraw();
65469
66386
  let changing = false;
66387
+ let adopted = false;
65470
66388
  const finish = fn("finish")(function* () {
65471
66389
  const state = yield* ledger.end;
66390
+ if (adopted) return;
65472
66391
  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)))));
65473
66392
  yield* log(ledgerText(state, redraw ? {
65474
66393
  full: false,
@@ -65482,11 +66401,6 @@ const ship = fn("ship")(function* (flags) {
65482
66401
  });
65483
66402
  yield* addFinalizer(() => finish());
65484
66403
  const live = redraw ? yield* startLive(store, color) : noLive;
65485
- const cloneRootOf = (machine) => config.machines[machine.name]?.clones ?? "~/.local/share/fleetfizz/clones";
65486
- const checkRepos = (config.repos ?? []).map((r) => ({
65487
- name: repoName(r),
65488
- outputs: r.outputs ?? []
65489
- }));
65490
66404
  const checked = yield* forEach$1(targetsList, fn("checkOne")(function* (machine) {
65491
66405
  const local = machine.name === tailnet.self.name;
65492
66406
  if (!machine.online) {
@@ -65497,9 +66411,19 @@ const ship = fn("ship")(function* (flags) {
65497
66411
  yield* ledger.skip(machine.name, "no Tailscale SSH server");
65498
66412
  return;
65499
66413
  }
66414
+ const identity = {
66415
+ name: machine.name,
66416
+ platform: machine.platform
66417
+ };
65500
66418
  const script = checkScript({
65501
- cloneRoot: cloneRootOf(machine),
65502
- repos: checkRepos
66419
+ scope: {
66420
+ machine: identity,
66421
+ config
66422
+ },
66423
+ providers: providers.map((provider) => ({
66424
+ provider,
66425
+ items: targetedItems(provider, config, identity)
66426
+ }))
65503
66427
  });
65504
66428
  const result = yield* check({
65505
66429
  name: machine.name,
@@ -65515,58 +66439,105 @@ const ship = fn("ship")(function* (flags) {
65515
66439
  }), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
65516
66440
  const stale = yield* staleConfigs(config, own, checked, ledger);
65517
66441
  if (stale.length > 0) {
65518
- const what = `newer config on ${stale.join(", ")}`;
66442
+ const what = `newer config on ${stale.map((s) => s.description).join(", ")}`;
66443
+ const answers = stale.flatMap((s) => s.machines).map((m) => given("config", m));
65519
66444
  if (flags.dryRun) yield* ledger.note("info", `${what}; a Ship would ask before overwriting it`);
65520
- else if (flags.overwriteConfig) yield* ledger.note("info", `${what}; overwriting it (--overwrite-config)`);
66445
+ else if (answers.every((a) => a === "overwrite")) yield* ledger.note("info", `${what}; overwriting it, as answered`);
66446
+ else if (answers.includes("stop")) return yield* fail$1(new ShipError("aborted: the config here is older"));
65521
66447
  else if (live.ask === void 0) return yield* fail$1(new ShipError(`${what}; copy it here first, or pass --overwrite-config`));
65522
66448
  else {
66449
+ const usable = [];
66450
+ for (const s of stale) {
66451
+ const why = unshippable(s.config, tailnet, flags.only, `the config on ${s.machines[0]}`);
66452
+ if (why === void 0) usable.push(s);
66453
+ else yield* ledger.note("info", `cannot ship from here: ${why}`);
66454
+ }
65523
66455
  yield* ledger.note("ask", what);
65524
- const overwrite = yield* live.ask([{
65525
- title: "overwrite it with this Machine's config",
65526
- value: true
65527
- }, {
65528
- title: "stop",
65529
- value: false
65530
- }]);
65531
- yield* ledger.note("answer", overwrite ? "overwrite it" : "stop");
65532
- if (!overwrite) return yield* fail$1(new ShipError("aborted: the config here is older"));
66456
+ const choice = yield* live.ask([
66457
+ ...usable.map((s) => ({
66458
+ title: `use the config on ${s.machines[0]} (replaces this one)`,
66459
+ value: s
66460
+ })),
66461
+ {
66462
+ title: "overwrite it with this Machine's config",
66463
+ value: "overwrite"
66464
+ },
66465
+ {
66466
+ title: "stop",
66467
+ value: "stop"
66468
+ }
66469
+ ]);
66470
+ if (choice === "stop") {
66471
+ yield* ledger.note("answer", "stop");
66472
+ return yield* fail$1(new ShipError("aborted: the config here is older"));
66473
+ }
66474
+ if (choice === "overwrite") yield* ledger.note("answer", "overwrite it");
66475
+ else {
66476
+ yield* ledger.note("answer", `use the config on ${choice.machines[0]}`);
66477
+ yield* fs.writeFileString(path, choice.text);
66478
+ adopted = true;
66479
+ return "adopted";
66480
+ }
65533
66481
  }
65534
66482
  }
65535
- const versions = yield* resolveVersions(packagesToResolve(config));
65536
- const planAll = (cfg, answers) => checked.map((g) => ({
66483
+ const targeted = providers.map((provider) => ({
66484
+ provider,
66485
+ items: checked.flatMap(({ machine }) => targetedItems(provider, config, machine))
66486
+ }));
66487
+ const lent = targeted.some(({ provider, items }) => provider.name === "repos" && items.length > 0) ? yield* readLentToken() : { token: void 0 };
66488
+ if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
66489
+ const resolved = Object.fromEntries(yield* forEach$1(targeted, ({ provider, items }) => provider.resolve(items, lent.token === void 0 ? {} : { token: lent.token }).pipe(map$5((r) => [provider.name, r])), { concurrency: "unbounded" }));
66490
+ const planAll = (cfg, answer) => checked.map((g) => ({
65537
66491
  ...g,
65538
- plan: plan({
65539
- config: cfg,
65540
- machine: {
65541
- name: g.machine.name,
65542
- platform: g.machine.platform
65543
- },
66492
+ plan: planMachine({
66493
+ providers,
65544
66494
  state: g.state,
65545
- self: flags.version,
65546
- versions,
65547
- rebuild: rebuildOf(flags),
65548
- answers
66495
+ resolved,
66496
+ ctx: {
66497
+ machine: {
66498
+ name: g.machine.name,
66499
+ platform: g.machine.platform
66500
+ },
66501
+ config: cfg,
66502
+ self: flags.version,
66503
+ rebuild: (address) => flags.rebuild.includes("all") || flags.rebuild.includes(address),
66504
+ answer
66505
+ }
65549
66506
  })
65550
66507
  }));
65551
66508
  let cfg = config;
65552
- let planned = planAll(cfg, {});
65553
- const questions = planned.flatMap((g) => g.plan.questions);
65554
- if (questions.length > 0 && flags.dryRun) for (const q of questions) {
65555
- yield* ledger.note("ask", `${q.package} ${q.installed} on ${q.machine} is above ${q.range}`);
65556
- yield* ledger.note("answer", "not asked (dry run)");
65557
- }
65558
- if (questions.length > 0 && !flags.dryRun) {
65559
- const decided = yield* askQuestions(questions, flags, ledger, live.ask);
65560
- for (const [pkg, decision] of Object.entries(decided)) {
65561
- if (decision.kind !== "bump") continue;
65562
- changing = true;
65563
- yield* bumpConfig(path, pkg, decision.installed);
65564
- }
65565
- if (Object.values(decided).some((d) => d.kind === "bump")) cfg = yield* loadConfig(path);
65566
- const answers = Object.fromEntries(Object.entries(decided).flatMap(([pkg, d]) => d.kind === "bump" ? [] : [[pkg, d.kind]]));
65567
- planned = planAll(cfg, answers);
65568
- }
65569
- for (const { machine, plan: p, state } of planned) yield* ledger.plan(machine.name, p, state);
66509
+ const decided = /* @__PURE__ */ new Map();
66510
+ const answer = (kind, subject) => decided.get(`${kind}:${subject}`) ?? given(kind, subject);
66511
+ let planned = planAll(cfg, answer);
66512
+ for (;;) {
66513
+ const asked = planned.flatMap((g) => g.plan.providers.flatMap((pp) => pp.decisions.flatMap((d) => d.kind === "ask" ? [{
66514
+ machine: g.machine.name,
66515
+ question: d.question
66516
+ }] : [])));
66517
+ const answered = yield* askQuestions({
66518
+ asked,
66519
+ answer,
66520
+ ledger,
66521
+ ask: live.ask,
66522
+ dryRun: flags.dryRun
66523
+ });
66524
+ let progressed = false;
66525
+ for (const { question, bumps, answer: choice } of answered) {
66526
+ const key = `${question.kind}:${question.subject}`;
66527
+ progressed ||= !decided.has(key);
66528
+ decided.set(key, choice);
66529
+ if (choice !== "bump") continue;
66530
+ for (const bump of bumps) {
66531
+ changing = true;
66532
+ const range = yield* bumpConfig(path, bump.path, bump.newest);
66533
+ progressed ||= range !== bump.range;
66534
+ }
66535
+ if (bumps.length > 0) cfg = yield* loadConfig(path);
66536
+ }
66537
+ if (!progressed) break;
66538
+ planned = planAll(cfg, answer);
66539
+ }
66540
+ for (const { machine, plan: p } of planned) yield* ledger.plan(machine.name, p);
65570
66541
  if (flags.dryRun) return;
65571
66542
  const shippedAt = formatIso(yield* now);
65572
66543
  changing = true;
@@ -65580,44 +66551,36 @@ const ship = fn("ship")(function* (flags) {
65580
66551
  const stamped = stampWith(planned.map((g) => g.machine.name));
65581
66552
  yield* fs.writeFileString(path, stamped);
65582
66553
  const schema = `${schemaJson(fleetConfigJsonSchema())}\n`;
65583
- const lent = planned.some((g) => g.plan.repos.length > 0) ? yield* readLentToken() : { token: void 0 };
65584
- if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
65585
- const machineNames = [...fleet];
65586
66554
  const stdin = applyStdin(lent.token, stamped, schema);
65587
66555
  const delivered = yield* forEach$1(planned, fn("applyOne")(function* ({ machine, local, plan: p }) {
65588
- const script = applyScript({
65589
- plan: p,
65590
- cloneRoot: cloneRootOf(machine),
65591
- env: {
65592
- machine: machine.name,
65593
- platform: machine.platform,
65594
- machines: machineNames
65595
- }
65596
- });
65597
66556
  return (yield* apply({
65598
66557
  name: machine.name,
65599
66558
  local
65600
- }, p, script, stdin, ledger)).done.includes("config") ? machine.name : void 0;
66559
+ }, p, applyScript(p), stdin, ledger)).done.includes("config") ? machine.name : void 0;
65601
66560
  }), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((name) => name !== void 0)));
65602
66561
  yield* fs.writeFileString(path, stampWith(delivered));
65603
66562
  }, scoped);
65604
66563
  //#endregion
65605
- //#region src/bin.ts
66564
+ //#region src/command.ts
65606
66565
  const shipCommand = make("ship", {
65607
66566
  dryRun: Boolean$1("dry-run").pipe(withDefault(false), withDescription$1("Check and plan only. Print every Machine's plan and change nothing.")),
65608
- only: String$1("only").pipe(atLeast(0), withDescription$1("Ship only to these Machines (repeat the flag).")),
65609
- rebuild: String$1("rebuild").pipe(atLeast(0), withDescription$1("Run a repo's Install Command even when its record matches; `all` for every repo.")),
65610
- bump: Boolean$1("bump").pipe(withDefault(false), withDescription$1("Answer every above-range question by bumping the config.")),
65611
- enforce: Boolean$1("enforce").pipe(withDefault(false), withDescription$1("Answer every above-range question by downgrading into the range.")),
65612
- overwriteConfig: Boolean$1("overwrite-config").pipe(withDefault(false), withDescription$1("Ship even when another Machine holds a newer config, overwriting it with this one.")),
65613
- config: String$1("config").pipe(optional, withDescription$1("Config file. Default: $XDG_CONFIG_HOME/fleetfizz/config.jsonc."))
66567
+ only: String$1("only").pipe(atLeast(0), withMetavar("machine"), withDescription$1("Ship only to this Machine. Repeat the flag for more.")),
66568
+ rebuild: String$1("rebuild").pipe(atLeast(0), withMetavar("item"), withDescription$1("Rebuild a record Item (`repos:<name>`) even when it is up to date. `all` for every one.")),
66569
+ answer: String$1("answer").pipe(atLeast(0), withMetavar("answer"), withDescription$1("Answer a Question in advance: `kind=choice` or `kind:subject=choice`. Repeat the flag for more.")),
66570
+ bump: Boolean$1("bump").pipe(withDefault(false), withDescription$1("Answer every above-range Question by bumping the config (`--answer range=bump`).")),
66571
+ enforce: Boolean$1("enforce").pipe(withDefault(false), withDescription$1("Answer every above-range Question by downgrading into the Range (`--answer range=downgrade`).")),
66572
+ overwriteConfig: Boolean$1("overwrite-config").pipe(withDefault(false), withDescription$1("Ship even when another Machine holds a newer config, overwriting it (`--answer config=overwrite`).")),
66573
+ config: String$1("config").pipe(optional, withMetavar("path"), withDescription$1("The config file. Default: `$XDG_CONFIG_HOME/fleetfizz/config.jsonc`."))
65614
66574
  }).pipe(withDescription("Ship the config to every reachable Machine."), withHandler((flags) => ship({
65615
66575
  dryRun: flags.dryRun,
65616
66576
  only: flags.only,
65617
66577
  rebuild: flags.rebuild,
65618
- bump: flags.bump,
65619
- enforce: flags.enforce,
65620
- overwriteConfig: flags.overwriteConfig,
66578
+ answers: [
66579
+ ...flags.answer,
66580
+ ...flags.bump ? ["range=bump"] : [],
66581
+ ...flags.enforce ? ["range=downgrade"] : [],
66582
+ ...flags.overwriteConfig ? ["config=overwrite"] : []
66583
+ ],
65621
66584
  config: flags.config._tag === "Some" ? flags.config.value : void 0,
65622
66585
  version
65623
66586
  }).pipe(catch_((e) => gen(function* () {
@@ -65625,6 +66588,8 @@ const shipCommand = make("ship", {
65625
66588
  process.exitCode = 1;
65626
66589
  })))));
65627
66590
  const reportCommand = make("report", {}).pipe(withDescription("Print the full Ledger of the last Ship from this Machine."), withHandler(() => report()));
66591
+ //#endregion
66592
+ //#region src/bin.ts
65628
66593
  run(make("fizz").pipe(withDescription("Keep every Machine on your tailnet in line with one config."), withSubcommands([shipCommand, reportCommand])), { version }).pipe(provide(layer), runMain);
65629
66594
  //#endregion
65630
66595
  export {};