fleetfizz 0.6.0 → 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 +338 -138
  2. package/package.json +1 -1
  3. package/schema.json +1064 -245
@@ -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);
@@ -24100,7 +24155,7 @@ function toRepresentations(asts, options) {
24100
24155
  }
24101
24156
  function visitChecks(checks) {
24102
24157
  checks?.forEach((check) => {
24103
- check.annotations?.representation?.schemas?.forEach((schema) => visit(toType(schema)));
24158
+ check.annotations?.representation?.schemas?.forEach((schema) => visit(toType$1(schema)));
24104
24159
  if (check._tag === "FilterGroup") visitChecks(check.checks);
24105
24160
  });
24106
24161
  }
@@ -24240,7 +24295,7 @@ function toRepresentations(asts, options) {
24240
24295
  const { representation, ...ordinary } = annotations;
24241
24296
  const projected = representation === void 0 ? void 0 : representation.schemas === void 0 ? representation : {
24242
24297
  ...representation,
24243
- schemas: representation.schemas.map((schema) => recur(toType(schema)))
24298
+ schemas: representation.schemas.map((schema) => recur(toType$1(schema)))
24244
24299
  };
24245
24300
  return {
24246
24301
  ...projected === void 0 ? void 0 : { representation: projected },
@@ -24296,6 +24351,20 @@ function isSchemaError$1(u) {
24296
24351
  }
24297
24352
  //#endregion
24298
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
+ */
24299
24368
  const TypeId$5 = TypeId$6;
24300
24369
  /**
24301
24370
  * Error thrown or returned when schema decoding or encoding fails.
@@ -24753,6 +24822,15 @@ const optional$2 = /*#__PURE__*/ lambda((self) => {
24753
24822
  return make$3(optional$3(self.ast), { schema });
24754
24823
  });
24755
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
+ /**
24756
24834
  * Creates a schema for a single literal value (string, number, bigint, boolean, or null).
24757
24835
  *
24758
24836
  * **Example** (Defining a string literal)
@@ -25286,6 +25364,45 @@ function isPattern(regExp, annotations) {
25286
25364
  ...annotations
25287
25365
  });
25288
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
+ }
25289
25406
  function makeIsMinLength(minLength, minCodePoints, annotations) {
25290
25407
  return makeFilter((input) => input.length >= minLength, {
25291
25408
  expected: `a value with a length of at least ${minLength}`,
@@ -25326,6 +25443,39 @@ function makeIsMinLength(minLength, minCodePoints, annotations) {
25326
25443
  function isNonEmpty(annotations) {
25327
25444
  return makeIsMinLength(1, 1, annotations);
25328
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
+ }
25329
25479
  /**
25330
25480
  * Schema for non-empty strings. Validates that a string has at least one
25331
25481
  * character.
@@ -32482,7 +32632,7 @@ const runWith = (command, config) => {
32482
32632
  };
32483
32633
  //#endregion
32484
32634
  //#region package.json
32485
- var version = "0.6.0";
32635
+ var version = "0.7.0";
32486
32636
  //#endregion
32487
32637
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
32488
32638
  /**
@@ -35148,15 +35298,18 @@ var BumpError = class extends TaggedError()("BumpError", {
35148
35298
  };
35149
35299
  const bumpedRange = (current, installed) => import_semver.valid(current) !== null ? installed : `^${installed}`;
35150
35300
  const bumpText = (text, path, installed) => {
35151
- const node = findNodeAtLocation(parseTree(text) ?? {
35301
+ const tree = parseTree(text) ?? {
35152
35302
  type: "null",
35153
35303
  offset: 0,
35154
35304
  length: 0
35155
- }, [...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]);
35156
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` };
35157
35310
  const range = bumpedRange(node.value, installed);
35158
35311
  return {
35159
- text: applyEdits(text, modify(text, [...path], range, { formattingOptions: {
35312
+ text: applyEdits(text, modify(text, [...at], range, { formattingOptions: {
35160
35313
  insertSpaces: true,
35161
35314
  tabSize: 2
35162
35315
  } })),
@@ -35200,14 +35353,26 @@ const TrackedRepo = Struct({
35200
35353
  origin: Origin.annotateKey({ description: "The GitHub `owner/repo`." }),
35201
35354
  ref: Ref.annotateKey({ description: "A branch, or a full 40-character commit SHA to pin it." }),
35202
35355
  install: NonEmptyString.annotateKey({ description: "The Install Command, run in a login shell inside the Managed Clone." }),
35203
- name: optionalKey(NonEmptyString).annotateKey({ description: "Names the clone's directory and the repo in output. Default: `<owner>-<repo>`." }),
35204
35356
  outputs: optionalKey(ArraySchema(NonEmptyString)).annotateKey({ description: "Paths outside the clone the Install Command produces. If one changes by hand, the repo reinstalls." }),
35205
- files: optionalKey(Record(NonEmptyString, String$5)).annotateKey({ description: "Gitignored files written into the clone before every install, keyed by path. Never secrets." })
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 })
35206
35367
  });
35207
- const Global = Union([Range, Struct({
35368
+ const VARIANTS = "or a list of Variants, of which a Machine gets the first that targets it";
35369
+ const Global = withShorthand(Struct({
35208
35370
  ...Targeting,
35209
35371
  range: Range
35210
- })]);
35372
+ }), Range, {
35373
+ decode: (range) => ({ range }),
35374
+ encode: (global) => global.range
35375
+ });
35211
35376
  const PACKAGE_MANAGERS = [
35212
35377
  "npm",
35213
35378
  "pnpm",
@@ -35222,54 +35387,53 @@ const NOT_GLOBALS = {
35222
35387
  "vite-plus": "set vp with vp.version",
35223
35388
  fleetfizz: "a Ship installs the fleetfizz that ships"
35224
35389
  };
35225
- const VpSection = Struct({
35226
- ...Targeting,
35227
- dependsOn: optionalKey(ArraySchema(ProviderName)).annotateKey({ description: "Providers whose Items must succeed on a Machine before vp changes there." }),
35390
+ const VpSection = section({
35228
35391
  version: optionalKey(Range).annotateKey({ description: "The vp version. Pin it exactly." }),
35229
35392
  node: optionalKey(Range).annotateKey({ description: "The default Node version. Pin it exactly." }),
35230
35393
  npm: optionalKey(Range).annotateKey({ description: "The default npm version." }),
35231
35394
  pnpm: optionalKey(Range).annotateKey({ description: "The default pnpm version." }),
35232
35395
  yarn: optionalKey(Range).annotateKey({ description: "The default Yarn version." }),
35233
- bun: optionalKey(Range).annotateKey({ description: "The default Bun version." }),
35234
- globals: optionalKey(Record(NonEmptyString, Global).check(makeFilter((globals) => {
35235
- const name = Object.keys(globals).find((n) => n in NOT_GLOBALS);
35236
- return name === void 0 || `"${name}" is not a Global: ${NOT_GLOBALS[name]}`;
35237
- }, {
35238
- title: "Globals",
35239
- description: "npm packages other than vp, node, a package manager, vite-plus, and fleetfizz"
35240
- }))).annotateKey({ description: "Global packages, each with a Range, or `{ range, platforms?, machines? }` to target one." })
35241
- });
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}.`);
35242
35404
  const HomebrewRange = Literals$2(["*", "latest"]).annotate({ description: "\"*\" (install when missing, never upgrade) or \"latest\" (brew update, then upgrade on every Ship)" });
35243
- const HomebrewItems = Record(NonEmptyString, Union([HomebrewRange, Struct({
35405
+ const HomebrewItems = Record(NonEmptyString, variants(withShorthand(Struct({
35244
35406
  ...Targeting,
35245
35407
  range: HomebrewRange
35246
- })]));
35247
- const homebrewSection = (items, noun) => Struct({
35248
- ...Targeting,
35249
- dependsOn: optionalKey(ArraySchema(ProviderName)).annotateKey({ description: "Providers whose Items must succeed on a Machine before this one changes there." }),
35250
- items: optionalKey(items).annotateKey({ description: `${noun}, each \`"*"\` or \`"latest"\`, or \`{ range, platforms?, machines? }\` to target one. A tapped name (\`owner/tap/name\`) taps on install.` })
35251
- });
35252
- const BrewSection = homebrewSection(HomebrewItems.check(makeFilter((items) => !Object.hasOwn(items, "brew") || "\"brew\" is Homebrew itself", {
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", {
35253
35414
  title: "Formulae",
35254
35415
  description: "Formulae other than brew"
35255
- })), "Formulae");
35256
- const CaskSection = homebrewSection(HomebrewItems, "Casks");
35257
- const MachineSettings = Struct({ clones: optionalKey(NonEmptyString).annotateKey({ description: `Where this Machine keeps its Managed Clones. \`~\` expands on that Machine. Default: \`~/.local/share/fleetfizz/clones\`.` }) });
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}.`);
35258
35426
  const FleetConfig = Struct({
35259
35427
  $schema: optionalKey(String$5),
35260
35428
  shippedAt: optionalKey(String$5),
35261
35429
  shippedFrom: optionalKey(NonEmptyString),
35262
35430
  shippedTo: optionalKey(Record(NonEmptyString, String$5)),
35263
- machines: Record(NonEmptyString, MachineSettings).check(makeFilter((machines) => Object.keys(machines).length > 0, {
35264
- title: "Fleet",
35265
- description: "at least one Machine"
35266
- })).annotateKey({ description: "The Fleet, keyed by each Machine's tailnet name. A tailnet member that is not listed is not part of the Fleet." }),
35267
- repos: optionalKey(ArraySchema(TrackedRepo)).annotateKey({ description: "Each Tracked Repo is cloned on every targeted Machine and installed from its origin ref." }),
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." }),
35268
35433
  vp: optionalKey(VpSection).annotateKey({ description: "The vite-plus toolchain." }),
35269
35434
  brew: optionalKey(BrewSection).annotateKey({ description: "Homebrew Formulae. Needs Homebrew on the Machine; a Ship never installs it." }),
35270
35435
  cask: optionalKey(CaskSection).annotateKey({ description: "Homebrew Casks, macOS only. Needs Homebrew on the Machine." })
35271
35436
  });
35272
- const repoName = (repo) => repo.name ?? repo.origin.replace("/", "-");
35273
35437
  const SELF_PACKAGE = "fleetfizz";
35274
35438
  //#endregion
35275
35439
  //#region ../../packages/config/src/jsonSchema.ts
@@ -35302,17 +35466,33 @@ var ConfigNotFound = class extends TaggedError()("ConfigNotFound", { path: Strin
35302
35466
  }
35303
35467
  };
35304
35468
  const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
35305
- const semanticProblems = (config) => {
35469
+ const repeatedKeys = (text) => {
35306
35470
  const problems = [];
35307
- const seen = /* @__PURE__ */ new Set();
35308
- for (const repo of config.repos ?? []) {
35309
- const name = repoName(repo);
35310
- if (seen.has(name)) problems.push(`repos: duplicate name "${name}"`);
35311
- seen.add(name);
35312
- }
35313
- 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, []);
35314
35485
  return problems;
35315
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
+ };
35316
35496
  const parseConfig = fn("parseConfig")(function* (path, text) {
35317
35497
  const parseErrors = [];
35318
35498
  const parsed = parse(text, parseErrors, { allowTrailingComma: true });
@@ -35320,16 +35500,20 @@ const parseConfig = fn("parseConfig")(function* (path, text) {
35320
35500
  path,
35321
35501
  problems: parseErrors.map((e) => `line ${lineOf(text, e.offset)}: ${printParseErrorCode(e.error)}`)
35322
35502
  });
35323
- 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({
35324
35505
  path,
35325
- problems: [formatIssue(error.issue)]
35326
- })));
35327
- const problems = semanticProblems(config);
35328
- 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({
35329
35510
  path,
35330
- problems
35511
+ problems: repeated
35331
35512
  });
35332
- return config;
35513
+ return yield* decodeUnknownEffect(FleetConfig, { onExcessProperty: "error" })(parsed).pipe(mapError((error) => new ConfigError({
35514
+ path,
35515
+ problems: [formatIssue(error.issue)]
35516
+ })));
35333
35517
  });
35334
35518
  const formatIssue = makeFormatterDefault();
35335
35519
  const loadConfig = fn("loadConfig")(function* (path) {
@@ -35371,7 +35555,7 @@ const stampText = (text, stamp) => {
35371
35555
  const machinesOf = (config) => {
35372
35556
  if (typeof config !== "object" || config === null || !("machines" in config)) return [];
35373
35557
  const value = config.machines;
35374
- return typeof value === "object" && value !== null ? Object.keys(value) : [];
35558
+ return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
35375
35559
  };
35376
35560
  const shippedToOf = (config) => {
35377
35561
  if (typeof config !== "object" || config === null || !("shippedTo" in config)) return {};
@@ -63826,24 +64010,46 @@ const useWindowSize = () => {
63826
64010
  //#endregion
63827
64011
  //#region ../../packages/core/src/targeting.ts
63828
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
+ };
63829
64023
  //#endregion
63830
64024
  //#region ../../packages/core/src/engine.ts
63831
- const isObjectSection = (section) => !Array.isArray(section);
63832
- const sectionTargeting = (section) => isObjectSection(section) ? section : {};
63833
- const dependenciesOf = (provider, config) => {
63834
- const section = config[provider.name];
63835
- const extra = section !== void 0 && isObjectSection(section) ? section.dependsOn ?? [] : [];
63836
- return [.../* @__PURE__ */ new Set([...provider.dependsOn, ...extra])];
63837
- };
64025
+ const dependenciesOf = (provider, config) => [.../* @__PURE__ */ new Set([...provider.dependsOn, ...config[provider.name]?.dependsOn ?? []])];
64026
+ const withoutTargeting = ({ platforms: _platforms, machines: _machines, ...def }) => def;
63838
64027
  const targetedItems = (provider, config, machine) => {
63839
64028
  const section = config[provider.name];
63840
- const scoped = section !== void 0 && targets(sectionTargeting(section), machine) ? section : void 0;
63841
- return provider.items(scoped).filter((item) => targets(item, machine));
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);
63842
64046
  };
63843
- const machinesNamed = (provider, config) => {
63844
- const section = config[provider.name];
63845
- return [...section === void 0 ? [] : sectionTargeting(section).machines ?? [], ...provider.items(section).flatMap((item) => item.machines ?? [])];
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));
63846
64051
  };
64052
+ const machinesNamed = (provider, config) => machineNames(config[provider.name]);
63847
64053
  const hold = (decisions, by) => decisions.map((d) => d.kind === "change" || d.kind === "ask" ? {
63848
64054
  kind: "skip",
63849
64055
  item: d.item,
@@ -65122,7 +65328,7 @@ const decodeFact$2 = decodeUnknownOption(Union([Struct({
65122
65328
  version: NullOr(String$5)
65123
65329
  }), AtFact]));
65124
65330
  const NO_BREW = "Homebrew is not installed; install it first";
65125
- const homebrew = (name) => {
65331
+ function homebrew(name) {
65126
65332
  const flag = name === "brew" ? "--formula" : "--cask";
65127
65333
  const lookup = name === "brew" ? "\"$ff_i\"" : "\"${ff_i##*/}\"";
65128
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`;
@@ -65145,17 +65351,13 @@ const homebrew = (name) => {
65145
65351
  name,
65146
65352
  rule: "range",
65147
65353
  dependsOn: name === "cask" ? ["brew"] : [],
65148
- items(section) {
65354
+ items(section, listed) {
65149
65355
  if (section === void 0) return [];
65150
- const packages = Object.entries(section.items ?? {}).map(([item, entry]) => typeof entry === "string" ? {
65356
+ const packages = listed.map(({ name: item, def }) => ({
65151
65357
  name: item,
65152
65358
  kind: "package",
65153
- range: entry
65154
- } : {
65155
- ...entry,
65156
- name: item,
65157
- kind: "package"
65158
- });
65359
+ range: def.range
65360
+ }));
65159
65361
  return name === "brew" ? [{
65160
65362
  name: "brew",
65161
65363
  kind: "homebrew"
@@ -65247,7 +65449,7 @@ const homebrew = (name) => {
65247
65449
  });
65248
65450
  }
65249
65451
  };
65250
- };
65452
+ }
65251
65453
  const brew = homebrew("brew");
65252
65454
  const cask = homebrew("cask");
65253
65455
  const decodeFact$1 = decodeUnknownOption(Union([
@@ -65277,7 +65479,11 @@ const decodeRecord = decodeUnknownOption(Struct({
65277
65479
  files: Record(String$5, String$5),
65278
65480
  outputs: Record(String$5, String$5)
65279
65481
  }));
65280
- const cloneRoot = (config, machine) => config.machines[machine]?.clones ?? "~/.local/share/fleetfizz/clones";
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
+ };
65281
65487
  const originUrl = (origin) => `https://github.com/${origin}.git`;
65282
65488
  const short = (commit) => commit.slice(0, 7);
65283
65489
  const expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? `${home}${path.slice(1)}` : path;
@@ -65346,7 +65552,7 @@ const decideRepo = (repo, root, observed, resolved, ctx) => {
65346
65552
  if (observed.cloneRoot?.present === false && !root.startsWith("~")) return fail(`clone root not mounted: ${observed.cloneRoot.path}`);
65347
65553
  const url = originUrl(repo.origin);
65348
65554
  if (clone?.present && clone.origin !== void 0 && clone.origin !== url) return fail(`managed clone has origin ${clone.origin}, expected ${url}`);
65349
- const target = isPin(repo.ref) ? { commit: repo.ref } : resolved[repo.name];
65555
+ const target = isPin(repo.ref) ? { commit: repo.ref } : resolved[branchKey(repo)];
65350
65556
  if (target === void 0) return fail(`could not resolve ${repo.ref}`);
65351
65557
  if ("error" in target) return fail(target.error);
65352
65558
  const { commit } = target;
@@ -65413,7 +65619,7 @@ const installStep = ({ item, root }, ctx) => {
65413
65619
  const env = [
65414
65620
  `FLEETFIZZ_MACHINE=${quote(ctx.machine.name)}`,
65415
65621
  `FLEETFIZZ_PLATFORM=${quote(ctx.machine.platform)}`,
65416
- `FLEETFIZZ_MACHINES=${quote(Object.keys(ctx.config.machines).join(","))}`
65622
+ `FLEETFIZZ_MACHINES=${quote(ctx.config.machines.join(","))}`
65417
65623
  ].join(" ");
65418
65624
  const tmp = `${recordDir("repos")}/${quote(`${item.name}.json.tmp`)}`;
65419
65625
  const record = [
@@ -65450,11 +65656,11 @@ const repos = {
65450
65656
  name: "repos",
65451
65657
  rule: "record",
65452
65658
  dependsOn: ["vp"],
65453
- items: (section) => (section ?? []).map((repo) => ({
65454
- ...repo,
65455
- name: repoName(repo)
65659
+ items: (_section, listed) => listed.map(({ name, def }) => ({
65660
+ ...def,
65661
+ name
65456
65662
  })),
65457
- observe: (items, scope) => observe(items, cloneRoot(scope.config, scope.machine.name)),
65663
+ observe: (items, scope) => observe(items, cloneRoot(scope.config, scope.machine)),
65458
65664
  parse(facts, records) {
65459
65665
  let root;
65460
65666
  const clones = /* @__PURE__ */ new Map();
@@ -65497,9 +65703,9 @@ const repos = {
65497
65703
  records: decoded
65498
65704
  };
65499
65705
  },
65500
- resolve: (items, { token }) => forEach$1(items.filter((repo) => !isPin(repo.ref)), (repo) => lsRemote(repo, token).pipe(map$5((result) => [repo.name, result])), { concurrency: 8 }).pipe(map$5((entries) => Object.fromEntries(entries))),
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))),
65501
65707
  decide: ({ items, observed, resolved, ctx }) => {
65502
- const root = cloneRoot(ctx.config, ctx.machine.name);
65708
+ const root = cloneRoot(ctx.config, ctx.machine);
65503
65709
  return items.map((repo) => decideRepo(repo, root, observed, resolved, ctx));
65504
65710
  },
65505
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)] : [])
@@ -65687,28 +65893,14 @@ const PROVIDERS = [
65687
65893
  name: "vp",
65688
65894
  rule: "range",
65689
65895
  dependsOn: [],
65690
- items(section) {
65691
- const globals = Object.entries(section?.globals ?? {}).map(([name, global]) => {
65692
- const targeting = typeof global === "string" ? {} : global;
65693
- return {
65694
- ...targeting.platforms === void 0 ? {} : { platforms: targeting.platforms },
65695
- ...targeting.machines === void 0 ? {} : { machines: targeting.machines },
65696
- name,
65697
- kind: "global",
65698
- packages: [name],
65699
- range: typeof global === "string" ? global : global.range,
65700
- bumpPath: typeof global === "string" ? [
65701
- "vp",
65702
- "globals",
65703
- name
65704
- ] : [
65705
- "vp",
65706
- "globals",
65707
- name,
65708
- "range"
65709
- ]
65710
- };
65711
- });
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
+ }));
65712
65904
  const defaults = ["node", ...PACKAGE_MANAGERS].flatMap((tool) => {
65713
65905
  const range = section?.[tool];
65714
65906
  return range === void 0 ? [] : [{
@@ -66080,23 +66272,33 @@ const askQuestions = fn("askQuestions")(function* (input) {
66080
66272
  for (const group of groups.values()) {
66081
66273
  const question = group[0]?.question;
66082
66274
  if (question === void 0) continue;
66083
- const newest = newestOf(group.map((a) => a.question.found));
66084
- const where = group.map((a) => `${a.question.found} on ${a.machine}`).join(", ");
66085
- yield* input.ledger.note("ask", `${question.subject} ${where} ${question.problem}`);
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}`);
66086
66288
  if (input.dryRun) {
66087
66289
  yield* input.ledger.note("answer", "not asked (dry run)");
66088
66290
  continue;
66089
66291
  }
66090
66292
  const choices = question.choices.map((c) => ({
66091
66293
  value: c.value,
66092
- title: c.value === "bump" && question.bump !== void 0 ? `bump the config to ${bumpedRange(question.bump.range, newest)}` : c.title
66294
+ title: c.value === "bump" && bumps.length > 0 ? `bump the config to ${bumps.map((b) => bumpedRange(b.range, b.newest)).join(" and ")}` : c.title
66093
66295
  }));
66094
66296
  const answer = input.answer(question.kind, question.subject) ?? (input.ask === void 0 ? question.default : yield* input.ask(choices));
66095
66297
  yield* input.ledger.note("answer", choices.find((c) => c.value === answer)?.title ?? answer);
66096
66298
  answered.push({
66097
66299
  question,
66098
66300
  machines: group.map((a) => a.machine),
66099
- newest,
66301
+ bumps,
66100
66302
  answer
66101
66303
  });
66102
66304
  }
@@ -66110,7 +66312,7 @@ var ShipError = class extends Error {
66110
66312
  const unknownMachines = (config, tailnet, only) => {
66111
66313
  const known = new Set(tailnet.fleet.map((m) => m.name));
66112
66314
  const referenced = [
66113
- ...Object.keys(config.machines),
66315
+ ...config.machines,
66114
66316
  ...PROVIDERS.flatMap((p) => machinesNamed(p, config)),
66115
66317
  ...only
66116
66318
  ];
@@ -66119,7 +66321,7 @@ const unknownMachines = (config, tailnet, only) => {
66119
66321
  const unshippable = (config, tailnet, only, source) => {
66120
66322
  const unknown = unknownMachines(config, tailnet, only);
66121
66323
  if (unknown.length > 0) return `unknown Machine: ${unknown.join(", ")} (the tailnet has: ${tailnet.fleet.map((m) => m.name).join(", ")})`;
66122
- const fleet = new Set(Object.keys(config.machines));
66324
+ const fleet = new Set(config.machines);
66123
66325
  if (!fleet.has(tailnet.self.name)) return `this Machine (${tailnet.self.name}) is not in machines in ${source}`;
66124
66326
  const outsideFleet = only.filter((name) => !fleet.has(name));
66125
66327
  if (outsideFleet.length > 0) return `not in machines in ${source}: ${outsideFleet.join(", ")}`;
@@ -66167,9 +66369,9 @@ const shipPass = fn("shipPass")(function* (flags) {
66167
66369
  const tailnet = yield* readTailnet();
66168
66370
  const problem = unshippable(config, tailnet, flags.only, path);
66169
66371
  if (problem !== void 0) return yield* fail$1(new ShipError(problem));
66170
- const fleet = new Set(Object.keys(config.machines));
66372
+ const fleet = new Set(config.machines);
66171
66373
  const own = flags.config === void 0 ? tailnet.self.name : void 0;
66172
- const order = Object.keys(config.machines);
66374
+ const order = config.machines;
66173
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));
66174
66376
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
66175
66377
  yield* log(`${flags.dryRun ? "dry run, " : ""}${count} from ${tailnet.self.name}`);
@@ -66278,14 +66480,10 @@ const shipPass = fn("shipPass")(function* (flags) {
66278
66480
  }
66279
66481
  }
66280
66482
  }
66281
- const targeted = providers.map((provider) => {
66282
- const items = /* @__PURE__ */ new Map();
66283
- for (const { machine } of checked) for (const item of targetedItems(provider, config, machine)) items.set(item.name, item);
66284
- return {
66285
- provider,
66286
- items: [...items.values()]
66287
- };
66288
- });
66483
+ const targeted = providers.map((provider) => ({
66484
+ provider,
66485
+ items: checked.flatMap(({ machine }) => targetedItems(provider, config, machine))
66486
+ }));
66289
66487
  const lent = targeted.some(({ provider, items }) => provider.name === "repos" && items.length > 0) ? yield* readLentToken() : { token: void 0 };
66290
66488
  if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
66291
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" }));
@@ -66324,15 +66522,17 @@ const shipPass = fn("shipPass")(function* (flags) {
66324
66522
  dryRun: flags.dryRun
66325
66523
  });
66326
66524
  let progressed = false;
66327
- for (const { question, newest, answer: choice } of answered) {
66525
+ for (const { question, bumps, answer: choice } of answered) {
66328
66526
  const key = `${question.kind}:${question.subject}`;
66329
66527
  progressed ||= !decided.has(key);
66330
66528
  decided.set(key, choice);
66331
- if (choice !== "bump" || question.bump === void 0) continue;
66332
- changing = true;
66333
- const range = yield* bumpConfig(path, question.bump.path, newest);
66334
- progressed ||= range !== question.bump.range;
66335
- cfg = yield* loadConfig(path);
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);
66336
66536
  }
66337
66537
  if (!progressed) break;
66338
66538
  planned = planAll(cfg, answer);