fleetfizz 0.5.1 → 0.6.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 +1760 -993
  2. package/package.json +2 -1
  3. package/schema.json +467 -17
@@ -22876,6 +22876,10 @@ function decodeUnknownEffect$1(schema, options) {
22876
22876
  const parser = run$3(schema.ast);
22877
22877
  return options === void 0 ? parser : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions));
22878
22878
  }
22879
+ /** @internal */
22880
+ function decodeUnknownOption$1(schema, options) {
22881
+ return asOption(decodeUnknownEffect$1(schema, options));
22882
+ }
22879
22883
  /**
22880
22884
  * Creates a decoder for `unknown` input that reports failure safely as a
22881
22885
  * `Result`.
@@ -22961,6 +22965,16 @@ function runWithCompiler(compiler, ast) {
22961
22965
  function asExit(parser) {
22962
22966
  return (input, options) => runSyncExit(parser(input, options));
22963
22967
  }
22968
+ /** @internal */
22969
+ function asOption(parser) {
22970
+ const parserExit = asExit(parser);
22971
+ return (input, options) => {
22972
+ const exit = parserExit(input, options);
22973
+ if (isSuccess(exit)) return some(exit.value);
22974
+ getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues");
22975
+ return none();
22976
+ };
22977
+ }
22964
22978
  function asResult(parser) {
22965
22979
  const parserExit = asExit(parser);
22966
22980
  return (input, options) => {
@@ -24468,6 +24482,34 @@ function decodeUnknownEffect(schema, options) {
24468
24482
  */
24469
24483
  const decodeEffect = decodeUnknownEffect;
24470
24484
  /**
24485
+ * Decodes an `unknown` input against a schema, returning an `Option` that is
24486
+ * `Some` with the decoded value on success or `None` for schema mismatches.
24487
+ *
24488
+ * **When to use**
24489
+ *
24490
+ * Use when you do not know the input type statically and only need to know
24491
+ * whether decoding succeeded.
24492
+ *
24493
+ * **Details**
24494
+ *
24495
+ * Prefer this over {@link decodeUnknownExit} or {@link decodeUnknownEffect}
24496
+ * when you don't need error details. For input already typed as the schema's
24497
+ * `Encoded` type use {@link decodeOption}.
24498
+ * Options may be provided either when creating the decoder or when applying it;
24499
+ * application options override creation options.
24500
+ *
24501
+ * **Gotchas**
24502
+ *
24503
+ * Only causes made entirely of schema issues are converted to `None`. Causes
24504
+ * that contain defects, interruptions, or other non-schema reasons throw
24505
+ * instead.
24506
+ *
24507
+ * @stability stable
24508
+ * @category decoding
24509
+ * @since 3.10.0
24510
+ */
24511
+ const decodeUnknownOption = decodeUnknownOption$1;
24512
+ /**
24471
24513
  * Decodes an `unknown` input against a schema, returning a `Result` that
24472
24514
  * succeeds with the decoded value or fails with a {@link SchemaError} for schema
24473
24515
  * mismatches.
@@ -25893,145 +25935,6 @@ function defaultShouldQuit(input) {
25893
25935
  */
25894
25936
  const layer = /*#__PURE__*/ provideMerge(layer$11, /*#__PURE__*/ mergeAll(layer$7, layer$9, layer$5, layer$3, layer$2));
25895
25937
  //#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
25938
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/cli/CliError.js
26036
25939
  /**
26037
25940
  * Defines structured errors for the unstable CLI parser and runner.
@@ -29124,6 +29027,145 @@ const getParamMetadata = (param) => {
29124
29027
  });
29125
29028
  };
29126
29029
  //#endregion
29030
+ //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Console.js
29031
+ /**
29032
+ * Context reference for the current console service in the Effect system, allowing access to the active console implementation from within the Effect context.
29033
+ *
29034
+ * **When to use**
29035
+ *
29036
+ * Use when you need an effect to run against a provided console implementation,
29037
+ * such as tests or alternate runtimes, rather than the default console.
29038
+ *
29039
+ * **Details**
29040
+ *
29041
+ * When no override is provided, the reference resolves to `globalThis.console`.
29042
+ *
29043
+ * **Example** (Accessing the current console)
29044
+ *
29045
+ * ```ts import.meta.vitest
29046
+ * import { Console, Effect } from "effect"
29047
+ *
29048
+ * const messages: Array<unknown> = []
29049
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29050
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(...args)
29051
+ * })
29052
+ * const program = Console.consoleWith((console) =>
29053
+ * Effect.sync(() => {
29054
+ * console.log("Hello from current console!")
29055
+ * })
29056
+ * )
29057
+ *
29058
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29059
+ * messages // => ["Hello from current console!"]
29060
+ * ```
29061
+ *
29062
+ * @see {@link consoleWith} for using the current console service inside an effect
29063
+ *
29064
+ * @stability stable
29065
+ * @category services
29066
+ * @since 2.0.0
29067
+ */
29068
+ const Console = ConsoleRef;
29069
+ /**
29070
+ * Creates an Effect that provides access to the current console service and lets you perform operations with it within an Effect context.
29071
+ *
29072
+ * **Example** (Accessing the current console service)
29073
+ *
29074
+ * ```ts import.meta.vitest
29075
+ * import { Console, Effect } from "effect"
29076
+ *
29077
+ * const messages: Array<unknown> = []
29078
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29079
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(...args),
29080
+ * error: (...args: ReadonlyArray<unknown>) => messages.push(...args)
29081
+ * })
29082
+ * const program = Console.consoleWith((console) =>
29083
+ * Effect.sync(() => {
29084
+ * console.log("Hello, world!")
29085
+ * console.error("This is an error message")
29086
+ * })
29087
+ * )
29088
+ *
29089
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29090
+ * messages // => ["Hello, world!", "This is an error message"]
29091
+ * ```
29092
+ *
29093
+ * @stability stable
29094
+ * @category constructors
29095
+ * @since 2.0.0
29096
+ */
29097
+ const consoleWith = (f) => withFiber$1((fiber) => f(fiber.getRef(Console)));
29098
+ /**
29099
+ * Writes an error-level message to the console, typically displayed with error
29100
+ * styling by the active console implementation.
29101
+ *
29102
+ * **Example** (Writing error messages)
29103
+ *
29104
+ * ```ts import.meta.vitest
29105
+ * import { Console, Effect } from "effect"
29106
+ *
29107
+ * const messages: Array<ReadonlyArray<unknown>> = []
29108
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29109
+ * error: (...args: ReadonlyArray<unknown>) => messages.push(args)
29110
+ * })
29111
+ * const program = Effect.gen(function*() {
29112
+ * yield* Console.error("Something went wrong!")
29113
+ * yield* Console.error("Error details:", {
29114
+ * code: 500,
29115
+ * message: "Internal Server Error"
29116
+ * })
29117
+ * })
29118
+ *
29119
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29120
+ * const expected = [
29121
+ * ["Something went wrong!"],
29122
+ * ["Error details:", { code: 500, message: "Internal Server Error" }]
29123
+ * ]
29124
+ * messages // => expected
29125
+ * ```
29126
+ *
29127
+ * @stability stable
29128
+ * @category accessors
29129
+ * @since 2.0.0
29130
+ */
29131
+ const error = (...args) => consoleWith((console) => sync$1(() => {
29132
+ console.error(...args);
29133
+ }));
29134
+ /**
29135
+ * Logs a general-purpose message to the console.
29136
+ *
29137
+ * **Example** (Writing log messages)
29138
+ *
29139
+ * ```ts import.meta.vitest
29140
+ * import { Console, Effect } from "effect"
29141
+ *
29142
+ * const messages: Array<ReadonlyArray<unknown>> = []
29143
+ * const testConsole: Console.Console = Object.assign(Object.create(console), {
29144
+ * log: (...args: ReadonlyArray<unknown>) => messages.push(args)
29145
+ * })
29146
+ * const program = Effect.gen(function*() {
29147
+ * yield* Console.log("Hello, world!")
29148
+ * yield* Console.log("User data:", { name: "John", age: 30 })
29149
+ * yield* Console.log("Processing", 42, "items")
29150
+ * })
29151
+ *
29152
+ * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
29153
+ * const expected = [
29154
+ * ["Hello, world!"],
29155
+ * ["User data:", { name: "John", age: 30 }],
29156
+ * ["Processing", 42, "items"]
29157
+ * ]
29158
+ * messages // => expected
29159
+ * ```
29160
+ *
29161
+ * @stability stable
29162
+ * @category accessors
29163
+ * @since 2.0.0
29164
+ */
29165
+ const log = (...args) => consoleWith((console) => sync$1(() => {
29166
+ console.log(...args);
29167
+ }));
29168
+ //#endregion
29127
29169
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/cli/CliOutput.js
29128
29170
  /**
29129
29171
  * Formats CLI help and errors as text.
@@ -32440,7 +32482,7 @@ const runWith = (command, config) => {
32440
32482
  };
32441
32483
  //#endregion
32442
32484
  //#region package.json
32443
- var version = "0.5.1";
32485
+ var version = "0.6.0";
32444
32486
  //#endregion
32445
32487
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
32446
32488
  /**
@@ -35097,51 +35139,45 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
35097
35139
  })))(), 1);
35098
35140
  var BumpError = class extends TaggedError()("BumpError", {
35099
35141
  path: String$5,
35100
- package: String$5,
35142
+ at: String$5,
35101
35143
  reason: String$5
35102
35144
  }) {
35103
35145
  get message() {
35104
- return `${this.path}: cannot bump ${this.package}: ${this.reason}`;
35146
+ return `${this.path}: cannot bump ${this.at}: ${this.reason}`;
35105
35147
  }
35106
35148
  };
35107
- const rangePath = (pkg) => pkg === "vite-plus" ? ["vp", "version"] : pkg === "node" ? ["vp", "node"] : [
35108
- "vp",
35109
- "globals",
35110
- pkg
35111
- ];
35112
35149
  const bumpedRange = (current, installed) => import_semver.valid(current) !== null ? installed : `^${installed}`;
35113
- const bumpText = (text, pkg, installed) => {
35114
- const path = rangePath(pkg);
35150
+ const bumpText = (text, path, installed) => {
35115
35151
  const node = findNodeAtLocation(parseTree(text) ?? {
35116
35152
  type: "null",
35117
35153
  offset: 0,
35118
35154
  length: 0
35119
- }, path);
35155
+ }, [...path]);
35120
35156
  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
35157
  const range = bumpedRange(node.value, installed);
35122
35158
  return {
35123
- text: applyEdits(text, modify(text, path, range, { formattingOptions: {
35159
+ text: applyEdits(text, modify(text, [...path], range, { formattingOptions: {
35124
35160
  insertSpaces: true,
35125
35161
  tabSize: 2
35126
35162
  } })),
35127
35163
  range
35128
35164
  };
35129
35165
  };
35130
- const bumpConfig = fn("bumpConfig")(function* (configPath, pkg, installed) {
35166
+ const bumpConfig = fn("bumpConfig")(function* (configPath, path, installed) {
35131
35167
  const fs = yield* FileSystem;
35132
35168
  const text = yield* fs.readFileString(configPath);
35133
- const result = bumpText(text, pkg, installed);
35169
+ const result = bumpText(text, path, installed);
35134
35170
  if ("reason" in result) return yield* new BumpError({
35135
35171
  path: configPath,
35136
- package: pkg,
35172
+ at: path.join("."),
35137
35173
  reason: result.reason
35138
35174
  });
35139
35175
  yield* fs.writeFileString(configPath, result.text);
35140
35176
  return result.range;
35141
35177
  });
35142
35178
  const Targeting = {
35143
- platforms: optionalKey(NonEmptyArray(Literals$2(["darwin", "linux"]))),
35144
- machines: optionalKey(NonEmptyArray(NonEmptyString))
35179
+ platforms: optionalKey(NonEmptyArray(Literals$2(["darwin", "linux"]))).annotateKey({ description: "`darwin`, `linux`, or both." }),
35180
+ machines: optionalKey(NonEmptyArray(NonEmptyString)).annotateKey({ description: "Machine names from `machines`." })
35145
35181
  };
35146
35182
  const Origin = String$5.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35147
35183
  title: "GitHub origin",
@@ -35149,26 +35185,76 @@ const Origin = String$5.check(isPattern(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
35149
35185
  }));
35150
35186
  const Ref = NonEmptyString;
35151
35187
  const isPin = (ref) => /^[0-9a-f]{40}$/.test(ref);
35188
+ const ProviderName = Literals$2([
35189
+ "vp",
35190
+ "brew",
35191
+ "cask",
35192
+ "repos"
35193
+ ]);
35152
35194
  const Range = NonEmptyString.check(makeFilter((range) => range === "latest" || import_semver.validRange(range) !== null, {
35153
35195
  title: "Range",
35154
- description: "a semver range, \"*\", or \"latest\""
35196
+ 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
35197
  }));
35156
35198
  const TrackedRepo = Struct({
35157
35199
  ...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))
35200
+ origin: Origin.annotateKey({ description: "The GitHub `owner/repo`." }),
35201
+ ref: Ref.annotateKey({ description: "A branch, or a full 40-character commit SHA to pin it." }),
35202
+ 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
+ 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." })
35164
35206
  });
35207
+ const Global = Union([Range, Struct({
35208
+ ...Targeting,
35209
+ range: Range
35210
+ })]);
35211
+ const PACKAGE_MANAGERS = [
35212
+ "npm",
35213
+ "pnpm",
35214
+ "yarn",
35215
+ "bun"
35216
+ ];
35217
+ const NOT_GLOBALS = {
35218
+ vp: "the vp Item is vp itself; set it with vp.version",
35219
+ mode: "the mode Item is vp's managed mode",
35220
+ node: "set Node with vp.node",
35221
+ ...Object.fromEntries(PACKAGE_MANAGERS.map((pm) => [pm, `set ${pm} with vp.${pm}`])),
35222
+ "vite-plus": "set vp with vp.version",
35223
+ fleetfizz: "a Ship installs the fleetfizz that ships"
35224
+ };
35165
35225
  const VpSection = Struct({
35166
35226
  ...Targeting,
35167
- version: optionalKey(Range),
35168
- node: optionalKey(Range),
35169
- globals: optionalKey(Record(NonEmptyString, Range))
35227
+ dependsOn: optionalKey(ArraySchema(ProviderName)).annotateKey({ description: "Providers whose Items must succeed on a Machine before vp changes there." }),
35228
+ version: optionalKey(Range).annotateKey({ description: "The vp version. Pin it exactly." }),
35229
+ node: optionalKey(Range).annotateKey({ description: "The default Node version. Pin it exactly." }),
35230
+ npm: optionalKey(Range).annotateKey({ description: "The default npm version." }),
35231
+ pnpm: optionalKey(Range).annotateKey({ description: "The default pnpm version." }),
35232
+ 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." })
35170
35241
  });
35171
- const MachineSettings = Struct({ clones: optionalKey(NonEmptyString) });
35242
+ 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({
35244
+ ...Targeting,
35245
+ 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", {
35253
+ title: "Formulae",
35254
+ 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\`.` }) });
35172
35258
  const FleetConfig = Struct({
35173
35259
  $schema: optionalKey(String$5),
35174
35260
  shippedAt: optionalKey(String$5),
@@ -35177,9 +35263,11 @@ const FleetConfig = Struct({
35177
35263
  machines: Record(NonEmptyString, MachineSettings).check(makeFilter((machines) => Object.keys(machines).length > 0, {
35178
35264
  title: "Fleet",
35179
35265
  description: "at least one Machine"
35180
- })),
35181
- repos: optionalKey(ArraySchema(TrackedRepo)),
35182
- vp: optionalKey(VpSection)
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." }),
35268
+ vp: optionalKey(VpSection).annotateKey({ description: "The vite-plus toolchain." }),
35269
+ brew: optionalKey(BrewSection).annotateKey({ description: "Homebrew Formulae. Needs Homebrew on the Machine; a Ship never installs it." }),
35270
+ cask: optionalKey(CaskSection).annotateKey({ description: "Homebrew Casks, macOS only. Needs Homebrew on the Machine." })
35183
35271
  });
35184
35272
  const repoName = (repo) => repo.name ?? repo.origin.replace("/", "-");
35185
35273
  const SELF_PACKAGE = "fleetfizz";
@@ -35274,7 +35362,7 @@ const stampText = (text, stamp) => {
35274
35362
  const fleet = machinesOf(previous);
35275
35363
  const shippedTo = {
35276
35364
  ...Object.fromEntries(Object.entries(shippedToOf(previous)).filter(([name]) => fleet.includes(name))),
35277
- ...Object.fromEntries(stamp.reached.map((name) => [name, stamp.shippedAt]))
35365
+ ...Object.fromEntries([...stamp.reached, ...stamp.own === void 0 ? [] : [stamp.own]].map((name) => [name, stamp.shippedAt]))
35278
35366
  };
35279
35367
  let out = set(text, "shippedAt", stamp.shippedAt);
35280
35368
  out = set(out, "shippedFrom", stamp.shippedFrom);
@@ -35292,6 +35380,7 @@ const shippedToOf = (config) => {
35292
35380
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
35293
35381
  };
35294
35382
  const isNewer = (remote, local) => remote !== void 0 && (local === void 0 || remote > local);
35383
+ const staleCopies = (config, copies, own) => copies.filter((c) => c.machine !== own && isNewer(c.config.shippedTo?.[c.machine], config.shippedTo?.[c.machine]));
35295
35384
  //#endregion
35296
35385
  //#region ../../packages/remote/src/shell.ts
35297
35386
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
@@ -35307,8 +35396,10 @@ const vpEnv = [
35307
35396
  ].join("\n");
35308
35397
  const prelude = [
35309
35398
  vpEnv,
35399
+ `export PATH="$PATH:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin"`,
35310
35400
  `ff_json() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' | tr -d '\\n'; }`,
35311
- `ff_state() { printf 'ff-state %s\\n' "$1"; }`
35401
+ `ff_state() { printf 'ff-state %s\\n' "$1"; }`,
35402
+ `ff_at() { ff_state "{\\"provider\\":\\"$(ff_json "$1")\\",\\"fact\\":\\"at\\",\\"item\\":\\"$(ff_json "$2")\\",\\"at\\":\\"$(ff_json "$3")\\"}"; }`
35312
35403
  ].join("\n");
35313
35404
  const fingerprintFn = [
35314
35405
  "ff_fp() {",
@@ -35319,474 +35410,56 @@ const fingerprintFn = [
35319
35410
  "}"
35320
35411
  ].join("\n");
35321
35412
  //#endregion
35322
- //#region ../../packages/remote/src/gather.ts
35413
+ //#region ../../packages/remote/src/check.ts
35323
35414
  const STATE_DIR = "~/.local/state/fleetfizz";
35324
35415
  const RECORDS_DIR = `${STATE_DIR}/records`;
35325
35416
  const CONFIG_DIR_SH = "\"${XDG_CONFIG_HOME:-$HOME/.config}/fleetfizz\"";
35326
- const gatherScript = (input) => {
35417
+ const recordDir = (provider) => `${quotePath(RECORDS_DIR)}/${quote(provider)}`;
35418
+ const recordFile = (provider, item) => `${recordDir(provider)}/${quote(`${item}.json`)}`;
35419
+ const LEGACY_RECORD_PROVIDER = "repos";
35420
+ const recordFact = (provider, item) => [
35421
+ `ff_r=${recordFile(provider, item)}`,
35422
+ ...provider === LEGACY_RECORD_PROVIDER ? [`[ -f "$ff_r" ] || ff_r=${quotePath(RECORDS_DIR)}/${quote(`${item}.json`)}`] : [],
35423
+ `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"record\\",\\"provider\\":\\"${provider}\\",\\"item\\":\\"$(ff_json ${quote(item)})\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`
35424
+ ];
35425
+ const checkScript = (input) => {
35327
35426
  const lines = [
35328
35427
  prelude,
35329
35428
  fingerprintFn,
35330
- `ff_state "{\\"fact\\":\\"home\\",\\"path\\":\\"$(ff_json "$HOME")\\"}"`,
35331
- "if command -v vp >/dev/null 2>&1; then",
35332
- ` ff_v=$(command vp --version 2>/dev/null | sed -n 's/^vp v\\{0,1\\}\\([^ ]*\\).*/\\1/p' | head -n 1)`,
35333
- ` ff_state "{\\"fact\\":\\"vp\\",\\"version\\":\\"$(ff_json "$ff_v")\\",\\"path\\":\\"$(ff_json "$(command -v vp)")\\"}"`,
35334
- ` ff_n=$(command vp env default 2>/dev/null | sed -n 's/^Default Node.js version: //p' | head -n 1)`,
35335
- ` if [ -n "$ff_n" ]; then ff_state "{\\"fact\\":\\"node\\",\\"version\\":\\"$(ff_json "$ff_n")\\"}"; else ff_state '{"fact":"node","version":null}'; fi`,
35336
- ` ff_g=$(command vp list --global --json 2>/dev/null | tr -d '\\n')`,
35337
- ` ff_state "{\\"fact\\":\\"globals\\",\\"list\\":\${ff_g:-[]}}"`,
35338
- "else",
35339
- ` ff_state '{"fact":"vp","version":null}'`,
35340
- "fi",
35341
- `ff_root=${quotePath(input.cloneRoot)}`,
35342
- `if [ -d "$ff_root" ]; then ff_present=true; else ff_present=false; fi`,
35343
- `ff_state "{\\"fact\\":\\"cloneRoot\\",\\"path\\":\\"$(ff_json "$ff_root")\\",\\"present\\":$ff_present}"`
35429
+ `ff_state "{\\"fact\\":\\"home\\",\\"path\\":\\"$(ff_json "$HOME")\\"}"`
35344
35430
  ];
35345
- for (const repo of input.repos) {
35346
- const name = quote(repo.name);
35347
- 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");
35348
- 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`);
35349
- }
35350
- for (const repo of input.repos) {
35351
- const name = quote(repo.name);
35352
- 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`);
35431
+ for (const { provider, items } of input.providers) {
35432
+ lines.push(provider.observe(items, input.scope));
35433
+ if (provider.rule === "record") for (const item of items) lines.push(...recordFact(provider.name, item.name));
35353
35434
  }
35354
35435
  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`);
35355
35436
  lines.push(`ff_state '{"fact":"end"}'`);
35356
35437
  return lines.join("\n");
35357
35438
  };
35358
35439
  //#endregion
35359
- //#region ../../packages/core/src/range.ts
35360
- const newest$1 = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
35361
- const decideRange = (range, installed, available) => {
35362
- if (range === "latest") {
35363
- const version = newest$1(available);
35364
- if (version === void 0) return { kind: "unavailable" };
35365
- return installed === version ? { kind: "leave" } : {
35366
- kind: "install",
35367
- version
35368
- };
35369
- }
35370
- if (range === "*") {
35371
- if (installed !== void 0) return { kind: "leave" };
35372
- const version = newest$1(available);
35373
- return version === void 0 ? { kind: "unavailable" } : {
35374
- kind: "install",
35375
- version
35376
- };
35377
- }
35378
- if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
35379
- if (installed !== void 0 && import_semver.gtr(installed, range)) return {
35380
- kind: "ask",
35381
- installed
35382
- };
35383
- const version = import_semver.maxSatisfying([...available], range);
35384
- return version === null ? { kind: "unavailable" } : {
35385
- kind: "install",
35386
- version
35387
- };
35388
- };
35389
- //#endregion
35390
- //#region ../../packages/core/src/record.ts
35391
- const sameRecord = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([k, v]) => b[k] === v);
35392
- const decideInstall = (record, now, rebuild) => {
35393
- if (rebuild) return {
35394
- run: true,
35395
- reason: "--rebuild"
35396
- };
35397
- if (record === void 0) return {
35398
- run: true,
35399
- reason: "never installed"
35400
- };
35401
- if (record.commit !== now.commit) return {
35402
- run: true,
35403
- reason: `new commit ${now.commit.slice(0, 7)}`
35404
- };
35405
- if (record.path !== now.path) return {
35406
- run: true,
35407
- reason: "clone moved"
35408
- };
35409
- if (record.install !== now.install) return {
35410
- run: true,
35411
- reason: "install command changed"
35412
- };
35413
- if (!sameRecord(record.files, now.files)) return {
35414
- run: true,
35415
- reason: "files changed"
35416
- };
35417
- for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
35418
- run: true,
35419
- reason: `drift: ${path}`
35420
- };
35421
- return { run: false };
35422
- };
35423
- //#endregion
35424
- //#region ../../packages/core/src/targeting.ts
35425
- const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
35426
- //#endregion
35427
- //#region ../../packages/core/src/plan.ts
35428
- const VP_PACKAGE = "vite-plus";
35429
- const NODE_PACKAGE = "node";
35430
- const expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? `${home}${path.slice(1)}` : path;
35431
- const isHomebrew = (path) => path !== void 0 && (path.includes("/homebrew/") || path.includes("/Cellar/"));
35432
- const planVp = (input) => {
35433
- const out = {
35434
- steps: [],
35435
- questions: [],
35436
- skipped: [],
35437
- failures: []
35438
- };
35439
- const { state, machine } = input;
35440
- const vp = input.config.vp;
35441
- const targeted = vp !== void 0 && targets(vp, machine);
35442
- const missing = state.vp === void 0 || state.vp.version === null;
35443
- if (missing) out.steps.push({ kind: "bootstrap" });
35444
- const installed = missing ? void 0 : state.vp?.version ?? void 0;
35445
- const decide = (pkg, range, current, shown = pkg) => {
35446
- if (range === void 0) return void 0;
35447
- const decision = decideRange(range, current, input.versions[pkg] ?? []);
35448
- switch (decision.kind) {
35449
- case "leave": return;
35450
- case "install": return decision.version;
35451
- case "unavailable":
35452
- out.failures.push({
35453
- what: shown,
35454
- reason: `no published version satisfies ${range}`
35455
- });
35456
- return;
35457
- case "ask": {
35458
- const answer = input.answers[pkg];
35459
- if (answer === "downgrade") {
35460
- const target = decideRange(range, void 0, input.versions[pkg] ?? []);
35461
- if (target.kind === "install") return target.version;
35462
- out.failures.push({
35463
- what: shown,
35464
- reason: `no published version satisfies ${range}`
35465
- });
35466
- return;
35467
- }
35468
- if (answer === "skip") {
35469
- out.skipped.push({
35470
- what: shown,
35471
- reason: `${decision.installed} is above ${range}`
35472
- });
35473
- return;
35474
- }
35475
- out.questions.push({
35476
- machine: machine.name,
35477
- package: pkg,
35478
- installed: decision.installed,
35479
- range
35480
- });
35481
- return;
35482
- }
35483
- }
35484
- };
35485
- const nodeNow = missing ? void 0 : state.node ?? void 0;
35486
- let nodeTarget;
35487
- if (targeted) {
35488
- const vpTarget = decide(VP_PACKAGE, vp.version, installed, "vp");
35489
- if (vpTarget !== void 0) {
35490
- if (isHomebrew(state.vp?.path)) out.failures.push({
35491
- what: "vp",
35492
- reason: `vp ${installed} was installed with Homebrew; run brew upgrade vite-plus to get ${vpTarget}`
35493
- });
35494
- else out.steps.push({
35495
- kind: "vp",
35496
- from: installed ?? null,
35497
- version: vpTarget
35498
- });
35499
- }
35500
- nodeTarget = decide(NODE_PACKAGE, vp.node, nodeNow, "node");
35501
- if (nodeTarget !== void 0) out.steps.push({
35502
- kind: "node",
35503
- from: nodeNow ?? null,
35504
- version: nodeTarget
35505
- });
35506
- }
35507
- const nodeDefault = nodeTarget ?? nodeNow;
35508
- const byName = new Map((missing ? [] : state.globals).map((g) => [g.name, g]));
35509
- const installs = [];
35510
- const wanted = Object.entries(targeted ? vp.globals ?? {} : {});
35511
- for (const [name, range] of [...wanted, [SELF_PACKAGE, input.self]]) {
35512
- const current = byName.get(name);
35513
- const version = name === "fleetfizz" ? decideSelf(current?.version, input.self, out) : decide(name, range, current?.version);
35514
- if (version !== void 0) installs.push(current === void 0 ? {
35515
- name,
35516
- version
35517
- } : {
35518
- name,
35519
- from: current.version,
35520
- version
35521
- });
35522
- else if (current !== void 0 && nodeDefault !== void 0 && current.platform?.node !== void 0 && current.platform.node !== nodeDefault) out.steps.push({
35523
- kind: "reinstall",
35524
- name,
35525
- version: current.version
35526
- });
35527
- }
35528
- if (installs.length > 0) out.steps.push({
35529
- kind: "globals",
35530
- packages: installs
35531
- });
35532
- return out;
35533
- };
35534
- const decideSelf = (installed, self, out) => {
35535
- if (installed === void 0 || import_semver.lt(installed, self)) return self;
35536
- if (import_semver.gt(installed, self)) out.failures.push({
35537
- what: SELF_PACKAGE,
35538
- reason: `runs ${SELF_PACKAGE} ${installed}, newer than this ${self}; upgrade here first`
35539
- });
35540
- };
35541
- const planRepos = (input) => {
35542
- const repos = [];
35543
- const failures = [];
35544
- const { config, machine, state } = input;
35545
- const home = state.home ?? "~";
35546
- const root = config.machines?.[machine.name]?.clones ?? "~/.local/share/fleetfizz/clones";
35547
- for (const repo of config.repos ?? []) {
35548
- if (!targets(repo, machine)) continue;
35549
- const name = repoName(repo);
35550
- if (state.cloneRoot !== void 0 && !state.cloneRoot.present && !root.startsWith("~")) {
35551
- failures.push({
35552
- what: name,
35553
- reason: `clone root not mounted: ${state.cloneRoot.path}`
35554
- });
35555
- continue;
35556
- }
35557
- const current = state.repos[name];
35558
- const expectedOrigin = `https://github.com/${repo.origin}.git`;
35559
- if (current?.present && current.origin !== void 0 && current.origin !== expectedOrigin) {
35560
- failures.push({
35561
- what: name,
35562
- reason: `managed clone has origin ${current.origin}, expected ${expectedOrigin}`
35563
- });
35564
- continue;
35565
- }
35566
- const path = `${root}/${name}`;
35567
- const expanded = expandHome(path, home);
35568
- const files = repo.files ?? {};
35569
- const outputs = repo.outputs ?? [];
35570
- const rebuild = input.rebuild === "all" || input.rebuild !== "none" && input.rebuild.includes(name);
35571
- const decision = decideInstall(state.records[name], {
35572
- commit: current?.head ?? "",
35573
- path: expanded,
35574
- install: repo.install,
35575
- files,
35576
- outputs: Object.fromEntries(outputs.map((o) => [o, state.outputs[name]?.[o] ?? void 0]))
35577
- }, rebuild);
35578
- repos.push({
35579
- name,
35580
- origin: repo.origin,
35581
- ref: repo.ref,
35582
- pin: isPin(repo.ref),
35583
- path,
35584
- expanded,
35585
- ...current?.head === void 0 ? {} : { head: current.head },
35586
- install: repo.install,
35587
- files,
35588
- outputs,
35589
- decision
35590
- });
35591
- }
35592
- return {
35593
- repos,
35594
- failures
35595
- };
35596
- };
35597
- const plan = (input) => {
35598
- const vp = planVp(input);
35599
- const repos = planRepos(input);
35600
- return {
35601
- machine: input.machine,
35602
- vp: vp.steps,
35603
- repos: repos.repos,
35604
- questions: vp.questions,
35605
- skipped: vp.skipped,
35606
- failures: [...vp.failures.map((f) => ({
35607
- ...f,
35608
- section: "vp"
35609
- })), ...repos.failures.map((f) => ({
35610
- ...f,
35611
- section: "repos"
35612
- }))]
35613
- };
35614
- };
35615
- //#endregion
35616
- //#region ../../packages/core/src/state.ts
35617
- const GlobalEntry = Struct({
35618
- name: String$5,
35619
- version: String$5,
35620
- platform: optionalKey(Struct({ node: optionalKey(String$5) }))
35621
- });
35622
- const InstallRecordSchema = Struct({
35623
- commit: String$5,
35624
- path: String$5,
35625
- install: String$5,
35626
- files: Record(String$5, String$5),
35627
- outputs: Record(String$5, String$5)
35628
- });
35629
- const Fact = Union([
35630
- Struct({
35631
- fact: Literal("home"),
35632
- path: String$5
35633
- }),
35634
- Struct({ fact: Literal("end") }),
35635
- Struct({
35636
- fact: Literal("vp"),
35637
- version: NullOr(String$5),
35638
- path: optionalKey(String$5)
35639
- }),
35640
- Struct({
35641
- fact: Literal("node"),
35642
- version: NullOr(String$5)
35643
- }),
35644
- Struct({
35645
- fact: Literal("globals"),
35646
- list: ArraySchema(GlobalEntry)
35647
- }),
35648
- Struct({
35649
- fact: Literal("cloneRoot"),
35650
- path: String$5,
35651
- present: Boolean$4
35652
- }),
35653
- Struct({
35654
- fact: Literal("repo"),
35655
- name: String$5,
35656
- present: Boolean$4,
35657
- head: optionalKey(String$5),
35658
- origin: optionalKey(String$5)
35659
- }),
35660
- Struct({
35661
- fact: Literal("record"),
35662
- name: String$5,
35663
- record: InstallRecordSchema
35664
- }),
35665
- Struct({
35666
- fact: Literal("config"),
35667
- present: Boolean$4,
35668
- base64: optionalKey(String$5)
35669
- }),
35670
- Struct({
35671
- fact: Literal("output"),
35672
- repo: String$5,
35673
- path: String$5,
35674
- fingerprint: NullOr(String$5)
35675
- })
35676
- ]);
35677
- const emptyState = {
35678
- complete: false,
35679
- globals: [],
35680
- repos: {},
35681
- records: {},
35682
- outputs: {}
35683
- };
35684
- const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
35685
- const assembleState = (facts) => {
35686
- let state = emptyState;
35687
- for (const fact of facts) switch (fact.fact) {
35688
- case "home":
35689
- state = {
35690
- ...state,
35691
- home: fact.path
35692
- };
35693
- break;
35694
- case "end":
35695
- state = {
35696
- ...state,
35697
- complete: true
35698
- };
35699
- break;
35700
- case "vp":
35701
- state = {
35702
- ...state,
35703
- vp: fact.path === void 0 ? { version: fact.version } : {
35704
- version: fact.version,
35705
- path: fact.path
35706
- }
35707
- };
35708
- break;
35709
- case "node":
35710
- state = {
35711
- ...state,
35712
- node: fact.version
35713
- };
35714
- break;
35715
- case "globals":
35716
- state = {
35717
- ...state,
35718
- globals: fact.list
35719
- };
35720
- break;
35721
- case "cloneRoot":
35722
- state = {
35723
- ...state,
35724
- cloneRoot: {
35725
- path: fact.path,
35726
- present: fact.present
35727
- }
35728
- };
35729
- break;
35730
- case "repo": {
35731
- const repo = {
35732
- present: fact.present,
35733
- ...fact.head === void 0 ? {} : { head: fact.head },
35734
- ...fact.origin === void 0 ? {} : { origin: fact.origin }
35735
- };
35736
- state = {
35737
- ...state,
35738
- repos: {
35739
- ...state.repos,
35740
- [fact.name]: repo
35741
- }
35742
- };
35743
- break;
35744
- }
35745
- case "record":
35746
- state = {
35747
- ...state,
35748
- records: {
35749
- ...state.records,
35750
- [fact.name]: fact.record
35751
- }
35752
- };
35753
- break;
35754
- case "config":
35755
- if (fact.present && fact.base64 !== void 0) state = {
35756
- ...state,
35757
- config: decodeBase64(fact.base64)
35758
- };
35759
- break;
35760
- case "output": state = {
35761
- ...state,
35762
- outputs: {
35763
- ...state.outputs,
35764
- [fact.repo]: {
35765
- ...state.outputs[fact.repo],
35766
- [fact.path]: fact.fingerprint
35767
- }
35768
- }
35769
- };
35770
- }
35771
- return state;
35772
- };
35773
- //#endregion
35774
35440
  //#region ../../packages/remote/src/protocol.ts
35441
+ const Fact = Record(String$5, Unknown);
35775
35442
  const StepStart = Struct({ id: String$5 });
35776
35443
  const StepDone = Struct({
35777
35444
  id: String$5,
35778
35445
  ok: Boolean$4,
35779
35446
  code: optionalKey(Number$2)
35780
35447
  });
35781
- const decodeFact = decodeUnknownResult(fromJsonString(Fact));
35448
+ const StepSkip = Struct({
35449
+ id: String$5,
35450
+ reason: String$5
35451
+ });
35452
+ const decodeFact$3 = decodeUnknownResult(fromJsonString(Fact));
35782
35453
  const decodeStart = decodeUnknownResult(fromJsonString(StepStart));
35783
35454
  const decodeDone = decodeUnknownResult(fromJsonString(StepDone));
35455
+ const decodeSkip = decodeUnknownResult(fromJsonString(StepSkip));
35784
35456
  const STATE = "ff-state ";
35785
35457
  const STEP = "ff-step ";
35786
35458
  const DONE = "ff-done ";
35459
+ const SKIP = "ff-skip ";
35787
35460
  const parseLine = (line) => {
35788
35461
  if (line.startsWith(STATE)) {
35789
- const result = decodeFact(line.slice(9));
35462
+ const result = decodeFact$3(line.slice(9));
35790
35463
  if (result._tag === "Success") return {
35791
35464
  kind: "state",
35792
35465
  fact: result.success
@@ -35803,6 +35476,12 @@ const parseLine = (line) => {
35803
35476
  kind: "done",
35804
35477
  done: result.success
35805
35478
  };
35479
+ } else if (line.startsWith(SKIP)) {
35480
+ const result = decodeSkip(line.slice(8));
35481
+ if (result._tag === "Success") return {
35482
+ kind: "skip",
35483
+ skip: result.success
35484
+ };
35806
35485
  }
35807
35486
  return {
35808
35487
  kind: "text",
@@ -35811,96 +35490,47 @@ const parseLine = (line) => {
35811
35490
  };
35812
35491
  const stepLine = (id) => `printf 'ff-step {"id":"%s"}\\n' '${id}'`;
35813
35492
  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}"`;
35493
+ const skipLine = (id, reason) => `printf 'ff-skip {"id":"%s","reason":"%s"}\\n' '${id}' ${reason}`;
35814
35494
  //#endregion
35815
35495
  //#region ../../packages/remote/src/apply.ts
35816
- const jsonString = encodeSync(fromJsonString(String$5));
35817
- const jsonRecord = encodeSync(fromJsonString(Record(String$5, String$5)));
35818
- const stepId = (repo, step) => `repo:${repo}:${step}`;
35819
- const vpStepId = (step) => step.kind === "reinstall" ? `vp:reinstall:${step.name}` : step.kind === "globals" ? "vp:globals" : `vp:${step.kind}`;
35820
35496
  const CONFIG_STEP = "config";
35821
- const VP_INSTALL = [
35822
- "curl -fsSL https://vite.plus | bash",
35823
- ...vpEnv.split("\n"),
35824
- "{ hash -r 2>/dev/null || true; }"
35825
- ].join(" && ");
35826
- const vpStep = (step) => {
35827
- const id = vpStepId(step);
35828
- const command = (() => {
35829
- switch (step.kind) {
35830
- case "bootstrap": return VP_INSTALL;
35831
- case "vp": return `command vp upgrade ${quote(step.version)}`;
35832
- case "node": return `command vp env default ${quote(step.version)}`;
35833
- case "reinstall": return `command vp install -g ${quote(`${step.name}@${step.version}`)}`;
35834
- case "globals": return `command vp install -g ${step.packages.map((p) => quote(`${p.name}@${p.version}`)).join(" ")}`;
35835
- }
35836
- })();
35837
- return [
35838
- stepLine(id),
35839
- step.kind === "bootstrap" ? command : `( ${command} )`,
35840
- `ff_code=$?`,
35841
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(id, false, "$ff_code")}; else ${doneLine(id, true)}; fi`
35842
- ];
35843
- };
35497
+ const marked = (id, run, failed = []) => [
35498
+ stepLine(id),
35499
+ ...run,
35500
+ `ff_code=$?`,
35501
+ `if [ "$ff_code" -ne 0 ]; then`,
35502
+ doneLine(id, false, "$ff_code"),
35503
+ ...failed,
35504
+ "else",
35505
+ doneLine(id, true),
35506
+ "fi"
35507
+ ];
35844
35508
  const configStep = () => {
35845
35509
  const dir = CONFIG_DIR_SH;
35846
35510
  const write = (variable, file) => `printf '%s' "$${variable}" | base64 -d > ${dir}/${quote(`${file}.tmp`)} && mv -f ${dir}/${quote(`${file}.tmp`)} ${dir}/${quote(file)}`;
35847
- return [
35848
- stepLine(CONFIG_STEP),
35849
- `( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`,
35850
- `ff_code=$?`,
35851
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(CONFIG_STEP, false, "$ff_code")}; else ${doneLine(CONFIG_STEP, true)}; fi`
35852
- ];
35853
- };
35854
- const writeRecord = (repo) => {
35855
- return [
35856
- `ff_outs=""`,
35857
- ...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`),
35858
- `mkdir -p ${quotePath(RECORDS_DIR)}`,
35859
- `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`)}`,
35860
- `mv -f ${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json.tmp`)} ${quotePath(RECORDS_DIR)}/${quote(`${repo.name}.json`)}`
35511
+ return marked(CONFIG_STEP, [`( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`]);
35512
+ };
35513
+ const providerSteps = (plan) => {
35514
+ if (plan.decisions.length === 0 && plan.steps.length === 0) return [];
35515
+ const held = `ff_held_${plan.name}`;
35516
+ const items = [...new Set(plan.steps.flatMap((step) => step.items))];
35517
+ const flag = (item) => `ff_f_${plan.name}_${items.indexOf(item)}`;
35518
+ const step = (s) => [
35519
+ `if [ -n "${s.items.map((item) => `\${${flag(item)}:-}`).join("")}" ]; then`,
35520
+ skipLine(s.id, "'an earlier step failed'"),
35521
+ "else",
35522
+ ...marked(s.id, s.run, [...s.items.map((item) => `${flag(item)}=1`), `${held}=1`]),
35523
+ "fi"
35861
35524
  ];
35862
- };
35863
- 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)}`]);
35864
- const repoSteps = (repo, input) => {
35865
- const fetch = stepId(repo.name, "fetch");
35866
- const install = stepId(repo.name, "install");
35867
- const url = `https://github.com/${repo.origin}.git`;
35868
- const sync = repo.pin ? [
35869
- `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(repo.ref)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(repo.ref)}; fi`,
35870
- `git -C "$ff_d" checkout --quiet --detach ${quote(repo.ref)}`,
35871
- `git -C "$ff_d" reset --quiet --hard ${quote(repo.ref)}`
35872
- ] : [`ff_git -C "$ff_d" fetch --quiet origin ${quote(repo.ref)}`, `git -C "$ff_d" reset --quiet --hard ${quote(`origin/${repo.ref}`)}`];
35873
- const envPrefix = `FLEETFIZZ_MACHINE=${quote(input.env.machine)} FLEETFIZZ_PLATFORM=${quote(input.env.platform)} FLEETFIZZ_MACHINES=${quote(input.env.machines.join(","))}`;
35525
+ const why = [`ff_why=""`, ...plan.dependsOn.map((dep) => `if [ -z "$ff_why" ] && [ -n "\${ff_held_${dep}:-}" ]; then ff_why=${quote(dep)}; fi`)];
35526
+ if (plan.steps.length === 0) return [...why, `if [ -n "$ff_why" ]; then ${held}=1; fi`];
35874
35527
  return [
35875
- `ff_d="$ff_root"/${quote(repo.name)}`,
35876
- `ff_before=$(git -C "$ff_d" rev-parse HEAD 2>/dev/null || true)`,
35877
- stepLine(fetch),
35878
- "(",
35879
- " set -e",
35880
- ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
35881
- ` 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`,
35882
- ...sync.map((l) => ` ${l}`),
35883
- ` git -C "$ff_d" clean --quiet -fd`,
35884
- ...writeFiles(repo).map((l) => ` ${l}`),
35885
- ")",
35886
- `ff_code=$?`,
35887
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(fetch, false, "$ff_code")}; else`,
35888
- `${doneLine(fetch, true)}`,
35889
- `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
35890
- `ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${quote(repo.name)})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$ff_after")\\"}"`,
35891
- `if [ ${repo.decision.run ? "true" : "false"} = true ] || [ "$ff_after" != "$ff_before" ]; then`,
35892
- ` ${stepLine(install)}`,
35893
- ` (cd "$ff_d" && ${envPrefix} "\${SHELL:-/bin/sh}" -lc ${quote(repo.install)})`,
35894
- ` ff_code=$?`,
35895
- ` if [ "$ff_code" -ne 0 ]; then ${doneLine(install, false, "$ff_code")}; else`,
35896
- " (",
35897
- " set -e",
35898
- ...writeRecord(repo).map((l) => ` ${l}`),
35899
- " )",
35900
- ` ff_code=$?`,
35901
- ` if [ "$ff_code" -ne 0 ]; then echo "could not write the install record"; ${doneLine(install, false, "$ff_code")}; else ${doneLine(install, true)}; fi`,
35902
- " fi",
35903
- "fi",
35528
+ ...why,
35529
+ `if [ -n "$ff_why" ]; then`,
35530
+ `${held}=1`,
35531
+ ...plan.steps.map((s) => skipLine(s.id, `"depends on $ff_why"`)),
35532
+ "else",
35533
+ ...plan.steps.flatMap(step),
35904
35534
  "fi"
35905
35535
  ];
35906
35536
  };
@@ -35915,24 +35545,18 @@ const encodeBase64 = (text) => {
35915
35545
  for (const byte of bytes) binary += String.fromCharCode(byte);
35916
35546
  return btoa(binary);
35917
35547
  };
35918
- const applyScript = (input) => {
35919
- const lines = [
35920
- prelude,
35921
- fingerprintFn,
35922
- `ff_lit() { printf '%s' "$1"; }`,
35923
- `IFS= read -r ff_token || ff_token=""`,
35924
- `IFS= read -r ff_config || ff_config=""`,
35925
- `IFS= read -r ff_schema || ff_schema=""`,
35926
- `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; }`,
35927
- `ff_root=${quotePath(input.cloneRoot)}`,
35928
- ...input.cloneRoot.startsWith("~") ? [`mkdir -p "$ff_root"`] : []
35929
- ];
35930
- lines.push(...configStep());
35931
- for (const step of input.plan.vp) lines.push(...vpStep(step));
35932
- for (const repo of input.plan.repos) lines.push(...repoSteps(repo, input));
35933
- lines.push(doneLine("end", true));
35934
- return lines.join("\n");
35935
- };
35548
+ const applyScript = (plan) => [
35549
+ prelude,
35550
+ fingerprintFn,
35551
+ `ff_lit() { printf '%s' "$1"; }`,
35552
+ `IFS= read -r ff_token || ff_token=""`,
35553
+ `IFS= read -r ff_config || ff_config=""`,
35554
+ `IFS= read -r ff_schema || ff_schema=""`,
35555
+ `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; }`,
35556
+ ...configStep(),
35557
+ ...plan.providers.flatMap(providerSteps),
35558
+ doneLine("end", true)
35559
+ ].join("\n");
35936
35560
  //#endregion
35937
35561
  //#region ../../packages/remote/src/tailnet.ts
35938
35562
  const StatusNode = Struct({
@@ -37489,8 +37113,8 @@ const exitAlternativeScreen = "\x1B[?1049l";
37489
37113
  //#endregion
37490
37114
  //#region ../../node_modules/.pnpm/is-in-ci@2.0.0/node_modules/is-in-ci/index.js
37491
37115
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
37492
- const check = (key) => key in env && env[key] !== "0" && env[key] !== "false";
37493
- const isInCi = check("CI") || check("CONTINUOUS_INTEGRATION");
37116
+ const check$1 = (key) => key in env && env[key] !== "0" && env[key] !== "false";
37117
+ const isInCi = check$1("CI") || check$1("CONTINUOUS_INTEGRATION");
37494
37118
  //#endregion
37495
37119
  //#region ../../node_modules/.pnpm/auto-bind@5.0.1/node_modules/auto-bind/index.js
37496
37120
  const getAllProperties = (object) => {
@@ -64200,6 +63824,333 @@ const useWindowSize = () => {
64200
63824
  return size;
64201
63825
  };
64202
63826
  //#endregion
63827
+ //#region ../../packages/core/src/targeting.ts
63828
+ const targets = (targeting, machine) => (targeting.platforms?.includes(machine.platform) ?? true) && (targeting.machines?.includes(machine.name) ?? true);
63829
+ //#endregion
63830
+ //#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
+ };
63838
+ const targetedItems = (provider, config, machine) => {
63839
+ 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));
63842
+ };
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 ?? [])];
63846
+ };
63847
+ const hold = (decisions, by) => decisions.map((d) => d.kind === "change" || d.kind === "ask" ? {
63848
+ kind: "skip",
63849
+ item: d.item,
63850
+ ...d.kind === "change" && d.from !== void 0 ? { at: d.from } : {},
63851
+ ...d.kind === "ask" && d.at !== void 0 ? { at: d.at } : {},
63852
+ reason: `depends on ${by}`
63853
+ } : d);
63854
+ const planMachine = (input) => {
63855
+ const ctx = {
63856
+ ...input.ctx,
63857
+ home: input.state.home ?? "~"
63858
+ };
63859
+ const plans = /* @__PURE__ */ new Map();
63860
+ const held = /* @__PURE__ */ new Set();
63861
+ for (const provider of input.providers) {
63862
+ const dependsOn = dependenciesOf(provider, ctx.config);
63863
+ const items = targetedItems(provider, ctx.config, ctx.machine);
63864
+ const decisions = provider.decide({
63865
+ items,
63866
+ observed: provider.parse(input.state.facts[provider.name] ?? [], input.state.records[provider.name] ?? {}),
63867
+ resolved: input.resolved[provider.name],
63868
+ ctx
63869
+ });
63870
+ const by = dependsOn.find((name) => {
63871
+ const dep = plans.get(name);
63872
+ return dep !== void 0 && dep.decisions.length > 0 && (held.has(name) || dep.decisions.some((d) => d.kind === "fail"));
63873
+ });
63874
+ if (by !== void 0) held.add(provider.name);
63875
+ plans.set(provider.name, {
63876
+ name: provider.name,
63877
+ rule: provider.rule,
63878
+ dependsOn,
63879
+ decisions: by === void 0 ? decisions : hold(decisions, by),
63880
+ steps: by === void 0 ? provider.act(decisions, ctx) : []
63881
+ });
63882
+ }
63883
+ return {
63884
+ machine: ctx.machine,
63885
+ providers: [...plans.values()]
63886
+ };
63887
+ };
63888
+ //#endregion
63889
+ //#region ../../packages/core/src/range.ts
63890
+ const newest = (available) => available.filter((v) => import_semver.prerelease(v) === null).sort(import_semver.rcompare)[0];
63891
+ const decideRange = (range, installed, available) => {
63892
+ if (range === "latest") {
63893
+ const version = newest(available);
63894
+ if (version === void 0) return { kind: "unavailable" };
63895
+ return installed === version ? { kind: "leave" } : {
63896
+ kind: "install",
63897
+ version
63898
+ };
63899
+ }
63900
+ if (range === "*") {
63901
+ if (installed !== void 0) return { kind: "leave" };
63902
+ const version = newest(available);
63903
+ return version === void 0 ? { kind: "unavailable" } : {
63904
+ kind: "install",
63905
+ version
63906
+ };
63907
+ }
63908
+ if (installed !== void 0 && import_semver.satisfies(installed, range)) return { kind: "leave" };
63909
+ if (installed !== void 0 && import_semver.gtr(installed, range)) return {
63910
+ kind: "ask",
63911
+ installed
63912
+ };
63913
+ const version = import_semver.maxSatisfying([...available], range);
63914
+ return version === null ? { kind: "unavailable" } : {
63915
+ kind: "install",
63916
+ version
63917
+ };
63918
+ };
63919
+ //#endregion
63920
+ //#region ../../packages/core/src/provider.ts
63921
+ const address = (provider, item) => `${provider}:${item}`;
63922
+ const parseAddress = (text) => {
63923
+ const at = text.indexOf(":");
63924
+ return at <= 0 || at === text.length - 1 ? void 0 : {
63925
+ provider: text.slice(0, at),
63926
+ item: text.slice(at + 1)
63927
+ };
63928
+ };
63929
+ const QUESTION_CHOICES = {
63930
+ range: [
63931
+ "bump",
63932
+ "skip",
63933
+ "downgrade"
63934
+ ],
63935
+ config: ["overwrite", "stop"],
63936
+ mode: ["managed", "skip"]
63937
+ };
63938
+ const isQuestionKind = (kind) => kind in QUESTION_CHOICES;
63939
+ const rangeQuestion = (subject, installed, range, bumpPath) => ({
63940
+ kind: "range",
63941
+ subject,
63942
+ found: installed,
63943
+ problem: `is above ${range}`,
63944
+ choices: [
63945
+ {
63946
+ value: "bump",
63947
+ title: "bump the config"
63948
+ },
63949
+ {
63950
+ value: "skip",
63951
+ title: "skip on these Machines (leave as is, shown as skipped)"
63952
+ },
63953
+ {
63954
+ value: "downgrade",
63955
+ title: "downgrade into the range"
63956
+ }
63957
+ ],
63958
+ default: "skip",
63959
+ bump: {
63960
+ path: bumpPath,
63961
+ range
63962
+ }
63963
+ });
63964
+ const decideRangeItem = (input) => {
63965
+ const { item, range, installed, available } = input;
63966
+ const at = installed === void 0 ? {} : { at: installed };
63967
+ const unavailable = {
63968
+ kind: "fail",
63969
+ item,
63970
+ ...at,
63971
+ reason: `no published version satisfies ${range}`
63972
+ };
63973
+ const install = (version) => ({
63974
+ kind: "change",
63975
+ item,
63976
+ ...installed === void 0 ? {} : { from: installed },
63977
+ to: version,
63978
+ change: version
63979
+ });
63980
+ const decision = decideRange(range, installed, available);
63981
+ switch (decision.kind) {
63982
+ case "leave": return {
63983
+ kind: "leave",
63984
+ item,
63985
+ ...at
63986
+ };
63987
+ case "install": return install(decision.version);
63988
+ case "unavailable": return unavailable;
63989
+ case "ask": {
63990
+ const subject = address(input.provider, item);
63991
+ const answer = input.ctx.answer("range", subject);
63992
+ if (answer === "skip") return {
63993
+ kind: "skip",
63994
+ item,
63995
+ ...at,
63996
+ reason: `is above ${range}`
63997
+ };
63998
+ if (answer === "downgrade") {
63999
+ const target = decideRange(range, void 0, available);
64000
+ return target.kind === "install" ? install(target.version) : unavailable;
64001
+ }
64002
+ return {
64003
+ kind: "ask",
64004
+ item,
64005
+ ...at,
64006
+ question: rangeQuestion(subject, decision.installed, range, input.bumpPath)
64007
+ };
64008
+ }
64009
+ }
64010
+ };
64011
+ const orderProviders = (all, dependsOn) => {
64012
+ const byName = new Map(all.map((p) => [p.name, p]));
64013
+ const order = [];
64014
+ const done = /* @__PURE__ */ new Set();
64015
+ const visit = (p, path) => {
64016
+ if (done.has(p.name)) return void 0;
64017
+ if (path.includes(p.name)) return [...path.slice(path.indexOf(p.name)), p.name];
64018
+ for (const name of dependsOn(p)) {
64019
+ const dep = byName.get(name);
64020
+ const cycle = dep === void 0 ? void 0 : visit(dep, [...path, p.name]);
64021
+ if (cycle !== void 0) return cycle;
64022
+ }
64023
+ done.add(p.name);
64024
+ order.push(p);
64025
+ };
64026
+ for (const p of all) {
64027
+ const cycle = visit(p, []);
64028
+ if (cycle !== void 0) return { cycle };
64029
+ }
64030
+ return { order };
64031
+ };
64032
+ //#endregion
64033
+ //#region ../../packages/core/src/record.ts
64034
+ const sameRecord = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([k, v]) => b[k] === v);
64035
+ const decideInstall = (record, now, rebuild) => {
64036
+ if (rebuild) return {
64037
+ run: true,
64038
+ reason: "--rebuild"
64039
+ };
64040
+ if (record === void 0) return {
64041
+ run: true,
64042
+ reason: "never installed"
64043
+ };
64044
+ if (record.commit !== now.commit) return {
64045
+ run: true,
64046
+ reason: `new commit ${now.commit.slice(0, 7)}`
64047
+ };
64048
+ if (record.path !== now.path) return {
64049
+ run: true,
64050
+ reason: "clone moved"
64051
+ };
64052
+ if (record.install !== now.install) return {
64053
+ run: true,
64054
+ reason: "install command changed"
64055
+ };
64056
+ if (!sameRecord(record.files, now.files)) return {
64057
+ run: true,
64058
+ reason: "files changed"
64059
+ };
64060
+ for (const [path, fingerprint] of Object.entries(now.outputs)) if (fingerprint === void 0 || record.outputs[path] !== fingerprint) return {
64061
+ run: true,
64062
+ reason: `drift: ${path}`
64063
+ };
64064
+ return { run: false };
64065
+ };
64066
+ //#endregion
64067
+ //#region ../../packages/core/src/state.ts
64068
+ const EngineFact = Union([
64069
+ Struct({
64070
+ fact: Literal("home"),
64071
+ path: String$5
64072
+ }),
64073
+ Struct({ fact: Literal("end") }),
64074
+ Struct({
64075
+ fact: Literal("config"),
64076
+ present: Boolean$4,
64077
+ base64: optionalKey(String$5)
64078
+ }),
64079
+ Struct({
64080
+ fact: Literal("record"),
64081
+ provider: String$5,
64082
+ item: String$5,
64083
+ record: Unknown
64084
+ })
64085
+ ]);
64086
+ const ProviderFact = Struct({ provider: String$5 });
64087
+ const AtFact = Struct({
64088
+ provider: String$5,
64089
+ fact: Literal("at"),
64090
+ item: String$5,
64091
+ at: String$5
64092
+ });
64093
+ const decodeEngineFact = decodeUnknownOption(EngineFact);
64094
+ const decodeProviderFact = decodeUnknownOption(ProviderFact);
64095
+ const emptyState = {
64096
+ complete: false,
64097
+ records: {},
64098
+ facts: {}
64099
+ };
64100
+ const decodeBase64 = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
64101
+ const assembleState = (facts) => {
64102
+ let state = emptyState;
64103
+ for (const raw of facts) {
64104
+ const fact = decodeEngineFact(raw);
64105
+ if (fact._tag === "Some") {
64106
+ const f = fact.value;
64107
+ switch (f.fact) {
64108
+ case "home":
64109
+ state = {
64110
+ ...state,
64111
+ home: f.path
64112
+ };
64113
+ break;
64114
+ case "end":
64115
+ state = {
64116
+ ...state,
64117
+ complete: true
64118
+ };
64119
+ break;
64120
+ case "config":
64121
+ if (f.present && f.base64 !== void 0) state = {
64122
+ ...state,
64123
+ config: decodeBase64(f.base64)
64124
+ };
64125
+ break;
64126
+ case "record": state = {
64127
+ ...state,
64128
+ records: {
64129
+ ...state.records,
64130
+ [f.provider]: {
64131
+ ...state.records[f.provider],
64132
+ [f.item]: f.record
64133
+ }
64134
+ }
64135
+ };
64136
+ }
64137
+ continue;
64138
+ }
64139
+ const owned = decodeProviderFact(raw);
64140
+ if (owned._tag === "Some") {
64141
+ const { provider } = owned.value;
64142
+ state = {
64143
+ ...state,
64144
+ facts: {
64145
+ ...state.facts,
64146
+ [provider]: [...state.facts[provider] ?? [], raw]
64147
+ }
64148
+ };
64149
+ }
64150
+ }
64151
+ return state;
64152
+ };
64153
+ //#endregion
64203
64154
  //#region src/ledger.ts
64204
64155
  const Note = Struct({
64205
64156
  kind: Literals$2([
@@ -64212,8 +64163,8 @@ const Note = Struct({
64212
64163
  const Item = Struct({
64213
64164
  name: String$5,
64214
64165
  kind: Literals$2([
64215
- "version",
64216
- "repo",
64166
+ "range",
64167
+ "record",
64217
64168
  "config"
64218
64169
  ]),
64219
64170
  from: optionalKey(String$5),
@@ -64230,7 +64181,10 @@ const Item = Struct({
64230
64181
  doing: optionalKey(String$5),
64231
64182
  detail: optionalKey(String$5),
64232
64183
  output: optionalKey(ArraySchema(String$5)),
64233
- steps: ArraySchema(String$5),
64184
+ steps: ArraySchema(Struct({
64185
+ id: String$5,
64186
+ doing: String$5
64187
+ })),
64234
64188
  started: optionalKey(Number$2),
64235
64189
  ended: optionalKey(Number$2)
64236
64190
  });
@@ -64259,99 +64213,58 @@ const LedgerState = Struct({
64259
64213
  started: Number$2,
64260
64214
  ended: optionalKey(Number$2)
64261
64215
  });
64262
- const short = (sha) => sha === void 0 ? void 0 : sha.slice(0, 7);
64263
- const FETCH = ":fetch";
64264
- const doingOf = (id) => id === "config" ? "copying" : id.endsWith(FETCH) ? "fetching" : "installing";
64265
- const itemsOf = (p, state) => {
64266
- const items = /* @__PURE__ */ new Map();
64267
- const key = (kind, name) => `${kind}:${name}`;
64268
- const base = (name, from, kind = "version") => ({
64269
- name,
64270
- kind,
64271
- ...from === void 0 ? {} : { from },
64272
- changes: false,
64273
- status: "idle",
64274
- steps: []
64275
- });
64276
- const set = (name, change, kind = "version") => items.set(key(kind, name), change(items.get(key(kind, name)) ?? base(name, void 0, kind)));
64277
- const plans = (id, to, from, reason) => ({ from: checked, ...item }) => {
64278
- const now = from === void 0 ? checked : from ?? void 0;
64279
- return {
64280
- ...item,
64281
- ...now === void 0 ? {} : { from: now },
64282
- to,
64283
- ...reason === void 0 ? {} : { reason },
64216
+ const itemsOf = (p) => [...p.providers.flatMap((pp) => pp.decisions.map((d) => {
64217
+ const base = {
64218
+ name: address(pp.name, d.item),
64219
+ kind: pp.rule,
64220
+ steps: pp.steps.filter((step) => step.items.includes(d.item)).map(({ id, doing }) => ({
64221
+ id,
64222
+ doing
64223
+ }))
64224
+ };
64225
+ switch (d.kind) {
64226
+ case "change": return {
64227
+ ...base,
64228
+ ...d.from === void 0 ? {} : { from: d.from },
64229
+ to: d.to,
64230
+ ...d.reason === void 0 ? {} : { reason: d.reason },
64284
64231
  changes: true,
64285
- steps: [...item.steps, id]
64232
+ status: "idle"
64233
+ };
64234
+ case "leave": return {
64235
+ ...base,
64236
+ ...d.at === void 0 ? {} : { from: d.at },
64237
+ changes: false,
64238
+ status: "idle"
64239
+ };
64240
+ case "skip":
64241
+ case "fail": return {
64242
+ ...base,
64243
+ ...d.at === void 0 ? {} : { from: d.at },
64244
+ changes: d.kind === "fail",
64245
+ status: d.kind === "fail" ? "failed" : "skipped",
64246
+ detail: d.reason
64247
+ };
64248
+ case "ask": return {
64249
+ ...base,
64250
+ ...d.at === void 0 ? {} : { from: d.at },
64251
+ changes: false,
64252
+ status: "skipped",
64253
+ detail: d.question.problem
64286
64254
  };
64287
- };
64288
- const vp = state.vp?.version ?? void 0;
64289
- items.set(key("version", "vp"), base("vp", vp));
64290
- if (state.node != null) items.set(key("version", "node"), base("node", state.node));
64291
- for (const g of state.globals) items.set(key("version", g.name), base(g.name, g.version));
64292
- for (const step of p.vp) {
64293
- const id = vpStepId(step);
64294
- switch (step.kind) {
64295
- case "bootstrap":
64296
- set("vp", plans(id, "latest", null));
64297
- break;
64298
- case "vp":
64299
- case "node":
64300
- set(step.kind, plans(id, step.version, step.from));
64301
- break;
64302
- case "reinstall":
64303
- set(step.name, plans(id, step.version, step.version, "reinstall"));
64304
- break;
64305
- case "globals": for (const pkg of step.packages) set(pkg.name, plans(id, pkg.version, pkg.from ?? null));
64306
- }
64307
- }
64308
- for (const repo of p.repos) {
64309
- const from = short(state.records[repo.name]?.commit);
64310
- const to = short(repo.head);
64311
- items.set(key("repo", repo.name), {
64312
- name: repo.name,
64313
- kind: "repo",
64314
- ...from === void 0 ? {} : { from },
64315
- ...to === void 0 ? {} : { to },
64316
- ...repo.decision.run ? { reason: repo.decision.reason } : {},
64317
- changes: repo.decision.run,
64318
- status: "idle",
64319
- steps: [stepId(repo.name, "fetch"), stepId(repo.name, "install")]
64320
- });
64321
64255
  }
64322
- for (const s of p.skipped) set(s.what, (i) => ({
64323
- ...i,
64324
- status: "skipped",
64325
- detail: s.reason
64326
- }));
64327
- for (const f of p.failures) {
64328
- const kind = f.section === "repos" ? "repo" : "version";
64329
- set(f.what, (i) => ({
64330
- ...i,
64331
- changes: true,
64332
- status: "failed",
64333
- detail: f.reason
64334
- }), kind);
64335
- }
64336
- items.set(key("config", CONFIG_STEP), {
64337
- name: CONFIG_STEP,
64338
- kind: "config",
64339
- changes: false,
64340
- status: "idle",
64341
- steps: [CONFIG_STEP]
64342
- });
64343
- return [...items.values()];
64344
- };
64256
+ })), {
64257
+ name: CONFIG_STEP,
64258
+ kind: "config",
64259
+ changes: false,
64260
+ status: "idle",
64261
+ steps: [{
64262
+ id: CONFIG_STEP,
64263
+ doing: "copying"
64264
+ }]
64265
+ }];
64345
64266
  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;
64346
- const tally = (state) => {
64347
- const outcomes = state.machines.map((m) => outcomeOf(m, state.dryRun));
64348
- const n = (o) => outcomes.filter((x) => x === o).length;
64349
- return {
64350
- ok: n("ok"),
64351
- skipped: n("skipped"),
64352
- failed: n("failed")
64353
- };
64354
- };
64267
+ const anyFailed = (state) => state.machines.some((m) => outcomeOf(m, state.dryRun) === "failed");
64355
64268
  const makeLedger = fn("makeLedger")(function* (input) {
64356
64269
  const started = yield* currentTimeMillis;
64357
64270
  let snapshot = {
@@ -64383,7 +64296,7 @@ const makeLedger = fn("makeLedger")(function* (input) {
64383
64296
  }));
64384
64297
  const items = (name, id, change) => machine(name, (m, now) => ({
64385
64298
  ...m,
64386
- items: m.items.map((i) => i.steps.includes(id) ? change(i, now) : i)
64299
+ items: m.items.map((i) => i.steps.some((step) => step.id === id) ? change(i, now) : i)
64387
64300
  }));
64388
64301
  const store = {
64389
64302
  get: () => snapshot,
@@ -64424,25 +64337,24 @@ const makeLedger = fn("makeLedger")(function* (input) {
64424
64337
  ...output.length === 0 ? {} : { output },
64425
64338
  ended: now
64426
64339
  })),
64427
- plan: (name, p, state) => machine(name, (m) => ({
64340
+ plan: (name, p) => machine(name, (m) => ({
64428
64341
  ...m,
64429
64342
  status: "planned",
64430
- items: itemsOf(p, state)
64343
+ items: itemsOf(p)
64431
64344
  })),
64432
64345
  start: (name) => machine(name, (m, now) => ({
64433
64346
  ...m,
64434
64347
  status: "running",
64435
64348
  started: now
64436
64349
  })),
64437
- stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" ? item : {
64350
+ stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" || item.status === "skipped" ? item : {
64438
64351
  ...item,
64439
- changes: item.changes || !(id === "config" || id.endsWith(FETCH)),
64440
64352
  status: "running",
64441
- doing: doingOf(id),
64353
+ doing: item.steps.find((step) => step.id === id)?.doing ?? "running",
64442
64354
  started: item.started ?? now
64443
64355
  }),
64444
64356
  stepDone: (name, id, result) => items(name, id, (item, now) => {
64445
- if (item.status === "failed") return item;
64357
+ if (item.status === "failed" || item.status === "skipped") return item;
64446
64358
  if (!result.ok) return {
64447
64359
  ...item,
64448
64360
  changes: true,
@@ -64451,25 +64363,31 @@ const makeLedger = fn("makeLedger")(function* (input) {
64451
64363
  ...result.output.length === 0 ? {} : { output: result.output },
64452
64364
  ended: now
64453
64365
  };
64366
+ if (item.steps.at(-1)?.id !== id) return item;
64454
64367
  const { doing: _, ...rest } = item;
64455
- if (id.endsWith(FETCH)) return item.changes ? item : {
64368
+ return {
64456
64369
  ...rest,
64457
- status: "idle",
64370
+ status: "ok",
64458
64371
  ended: now
64459
64372
  };
64373
+ }),
64374
+ stepSkipped: (name, id, reason) => items(name, id, (item, now) => {
64375
+ if (item.status === "failed") return item;
64376
+ const { doing: _, ...rest } = item;
64460
64377
  return {
64461
64378
  ...rest,
64462
- status: "ok",
64379
+ status: "skipped",
64380
+ detail: reason,
64463
64381
  ended: now
64464
64382
  };
64465
64383
  }),
64466
- fetched: (name, repo, head) => machine(name, (m) => ({
64384
+ reached: (name, item, at) => machine(name, (m) => ({
64467
64385
  ...m,
64468
- items: m.items.map((i) => i.kind === "repo" && i.name === repo ? {
64386
+ items: m.items.map((i) => i.name !== item ? i : {
64469
64387
  ...i,
64470
- to: head.slice(0, 7),
64471
- changes: i.changes || head.slice(0, 7) !== i.from
64472
- } : i)
64388
+ to: at,
64389
+ changes: i.status === "failed" || i.from !== at
64390
+ })
64473
64391
  })),
64474
64392
  finish: (name, outcome) => machine(name, (m, now) => ({
64475
64393
  ...m,
@@ -64811,6 +64729,10 @@ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
64811
64729
  })
64812
64730
  });
64813
64731
  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}`;
64732
+ const unchanged = (i) => ({
64733
+ text: "",
64734
+ detail: i.kind === "record" ? "up to date" : void 0
64735
+ });
64814
64736
  const statusOf = (i, dryRun) => {
64815
64737
  switch (i.status) {
64816
64738
  case "running": return {
@@ -64832,7 +64754,8 @@ const statusOf = (i, dryRun) => {
64832
64754
  text: "✓ copied",
64833
64755
  tone: "dim"
64834
64756
  };
64835
- if (i.kind === "repo") return i.from !== void 0 && i.from === i.to ? {
64757
+ if (!i.changes) return unchanged(i);
64758
+ if (i.kind === "record") return i.from !== void 0 && i.from === i.to ? {
64836
64759
  text: "✓ rebuilt",
64837
64760
  tone: "ok",
64838
64761
  detail: i.reason
@@ -64851,10 +64774,7 @@ const statusOf = (i, dryRun) => {
64851
64774
  tone: "dim",
64852
64775
  detail: i.reason
64853
64776
  };
64854
- return {
64855
- text: "",
64856
- detail: i.kind === "repo" ? "up to date" : void 0
64857
- };
64777
+ return unchanged(i);
64858
64778
  }
64859
64779
  };
64860
64780
  const countsOf = (m) => {
@@ -64908,10 +64828,38 @@ const summaryOf = (m, dryRun) => {
64908
64828
  text: `${skipped} skipped`,
64909
64829
  tone: "skip"
64910
64830
  });
64911
- return parts.flatMap((part, n) => n === 0 ? [part] : [{
64912
- text: ", ",
64831
+ return joined(parts);
64832
+ };
64833
+ const joined = (parts) => parts.flatMap((part, n) => n === 0 ? [part] : [{
64834
+ text: ", ",
64835
+ tone: "dim"
64836
+ }, part]);
64837
+ const shipSummary = (state) => {
64838
+ const outcomes = state.machines.map((m) => {
64839
+ const outcome = outcomeOf(m, state.dryRun);
64840
+ if (outcome !== "ok") return outcome;
64841
+ const { changes, skipped } = countsOf(m);
64842
+ if (changes > 0) return state.dryRun ? "to change" : "updated";
64843
+ return skipped > 0 ? "skipped" : "up to date";
64844
+ });
64845
+ const counted = (outcome, tone) => {
64846
+ const n = outcomes.filter((o) => o === outcome).length;
64847
+ return n === 0 ? [] : [{
64848
+ text: `${n} ${outcome}`,
64849
+ tone
64850
+ }];
64851
+ };
64852
+ const parts = joined([
64853
+ ...counted("updated", "ok"),
64854
+ ...counted("to change", "dim"),
64855
+ ...counted("up to date", "ok"),
64856
+ ...counted("failed", "fail"),
64857
+ ...counted("skipped", "skip")
64858
+ ]);
64859
+ return parts.length === 0 ? [{
64860
+ text: "nothing changed",
64913
64861
  tone: "dim"
64914
- }, part]);
64862
+ }] : parts;
64915
64863
  };
64916
64864
  const elapsed = (started, ended, now) => started === void 0 ? "" : duration((ended ?? now) - started);
64917
64865
  const Parts = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, { children: props.parts.map((part, n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
@@ -65067,7 +65015,6 @@ const LedgerView = (props) => {
65067
65015
  const shown = lines.length <= room ? lines : lines.slice(0, Math.max(0, room - 1));
65068
65016
  const hidden = count(blocks) - (shown.length - cuts);
65069
65017
  if (room < 1) return null;
65070
- const t = tally(state);
65071
65018
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
65072
65019
  flexDirection: "column",
65073
65020
  marginTop: 1,
@@ -65079,26 +65026,10 @@ const LedgerView = (props) => {
65079
65026
  }),
65080
65027
  state.ended !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
65081
65028
  marginTop: 1,
65082
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: [
65083
- {
65084
- text: `${dryRun ? "planned" : "ok"} ${t.ok}`,
65085
- ...t.ok > 0 ? { tone: "ok" } : {}
65086
- },
65087
- { text: " " },
65088
- {
65089
- text: `skipped ${t.skipped}`,
65090
- ...t.skipped > 0 ? { tone: "skip" } : {}
65091
- },
65092
- { text: " " },
65093
- {
65094
- text: `failed ${t.failed}`,
65095
- ...t.failed > 0 ? { tone: "fail" } : {}
65096
- },
65097
- ...dryRun ? [] : [{
65098
- text: ` in ${duration(state.ended - state.started)}`,
65099
- tone: "dim"
65100
- }]
65101
- ] })
65029
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: [...shipSummary(state), ...dryRun ? [] : [{
65030
+ text: ` in ${duration(state.ended - state.started)}`,
65031
+ tone: "dim"
65032
+ }]] })
65102
65033
  }),
65103
65034
  state.ended !== void 0 && props.hint !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
65104
65035
  tone: "dim",
@@ -65186,6 +65117,757 @@ const report = fn("report")(function* () {
65186
65117
  ]
65187
65118
  })));
65188
65119
  });
65120
+ const decodeFact$2 = decodeUnknownOption(Union([Struct({
65121
+ fact: Literal("brew"),
65122
+ version: NullOr(String$5)
65123
+ }), AtFact]));
65124
+ const NO_BREW = "Homebrew is not installed; install it first";
65125
+ const homebrew = (name) => {
65126
+ const flag = name === "brew" ? "--formula" : "--cask";
65127
+ const lookup = name === "brew" ? "\"$ff_i\"" : "\"${ff_i##*/}\"";
65128
+ 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`;
65129
+ const step = (what, batch) => {
65130
+ const names = batch.map((c) => c.name);
65131
+ const update = batch.some((c) => c.range === "latest") ? "brew update && " : "";
65132
+ return {
65133
+ id: `${name}:${name === "brew" ? "formulae" : "casks"}:${what}`,
65134
+ items: names,
65135
+ doing: what === "install" ? "installing" : "upgrading",
65136
+ run: [[
65137
+ `( ${update}NONINTERACTIVE=1 brew ${what} ${flag} ${names.map(quote).join(" ")}`,
65138
+ "ff_rc=$?",
65139
+ report(names),
65140
+ "exit $ff_rc )"
65141
+ ].join("; ")]
65142
+ };
65143
+ };
65144
+ return {
65145
+ name,
65146
+ rule: "range",
65147
+ dependsOn: name === "cask" ? ["brew"] : [],
65148
+ items(section) {
65149
+ if (section === void 0) return [];
65150
+ const packages = Object.entries(section.items ?? {}).map(([item, entry]) => typeof entry === "string" ? {
65151
+ name: item,
65152
+ kind: "package",
65153
+ range: entry
65154
+ } : {
65155
+ ...entry,
65156
+ name: item,
65157
+ kind: "package"
65158
+ });
65159
+ return name === "brew" ? [{
65160
+ name: "brew",
65161
+ kind: "homebrew"
65162
+ }, ...packages] : packages;
65163
+ },
65164
+ observe: (items) => [
65165
+ "if command -v brew >/dev/null 2>&1; then",
65166
+ ` ff_state "{\\"provider\\":\\"${name}\\",\\"fact\\":\\"brew\\",\\"version\\":\\"$(ff_json "$(brew --version 2>/dev/null | head -n 1 | sed 's/^Homebrew //')")\\"}"`,
65167
+ ` ${report(items.flatMap((i) => i.kind === "package" ? [i.name] : []))}`,
65168
+ "else",
65169
+ ` ff_state '{"provider":"${name}","fact":"brew","version":null}'`,
65170
+ "fi"
65171
+ ].join("\n"),
65172
+ parse(facts) {
65173
+ let brew;
65174
+ const at = /* @__PURE__ */ new Map();
65175
+ for (const raw of facts) {
65176
+ const fact = decodeFact$2(raw);
65177
+ if (fact._tag === "None") continue;
65178
+ const f = fact.value;
65179
+ if (f.fact === "brew") brew = f.version ?? void 0;
65180
+ else at.set(f.item, f.at);
65181
+ }
65182
+ return {
65183
+ ...brew === void 0 ? {} : { brew },
65184
+ at
65185
+ };
65186
+ },
65187
+ resolve: () => void_,
65188
+ decide({ items, observed, ctx }) {
65189
+ return items.map((item) => {
65190
+ const installed = observed.at.get(item.name);
65191
+ if (item.kind === "homebrew") return observed.brew === void 0 ? {
65192
+ kind: "fail",
65193
+ item: item.name,
65194
+ reason: NO_BREW
65195
+ } : {
65196
+ kind: "leave",
65197
+ item: item.name,
65198
+ at: observed.brew
65199
+ };
65200
+ if (name === "cask" && ctx.machine.platform !== "darwin") return {
65201
+ kind: "fail",
65202
+ item: item.name,
65203
+ reason: "Casks need macOS"
65204
+ };
65205
+ if (observed.brew === void 0) return name === "brew" ? {
65206
+ kind: "skip",
65207
+ item: item.name,
65208
+ reason: "needs Homebrew"
65209
+ } : {
65210
+ kind: "fail",
65211
+ item: item.name,
65212
+ reason: NO_BREW
65213
+ };
65214
+ if (installed === void 0) return {
65215
+ kind: "change",
65216
+ item: item.name,
65217
+ to: "latest",
65218
+ change: {
65219
+ kind: "install",
65220
+ name: item.name,
65221
+ range: item.range
65222
+ }
65223
+ };
65224
+ if (item.range === "latest") return {
65225
+ kind: "change",
65226
+ item: item.name,
65227
+ from: installed,
65228
+ to: "latest",
65229
+ change: {
65230
+ kind: "upgrade",
65231
+ name: item.name,
65232
+ range: item.range
65233
+ }
65234
+ };
65235
+ return {
65236
+ kind: "leave",
65237
+ item: item.name,
65238
+ at: installed
65239
+ };
65240
+ });
65241
+ },
65242
+ act(decisions) {
65243
+ const changes = decisions.flatMap((d) => d.kind === "change" ? [d.change] : []);
65244
+ return ["install", "upgrade"].flatMap((what) => {
65245
+ const batch = changes.filter((c) => c.kind === what);
65246
+ return batch.length === 0 ? [] : [step(what, batch)];
65247
+ });
65248
+ }
65249
+ };
65250
+ };
65251
+ const brew = homebrew("brew");
65252
+ const cask = homebrew("cask");
65253
+ const decodeFact$1 = decodeUnknownOption(Union([
65254
+ Struct({
65255
+ fact: Literal("cloneRoot"),
65256
+ path: String$5,
65257
+ present: Boolean$4
65258
+ }),
65259
+ Struct({
65260
+ fact: Literal("repo"),
65261
+ name: String$5,
65262
+ present: Boolean$4,
65263
+ head: optionalKey(String$5),
65264
+ origin: optionalKey(String$5)
65265
+ }),
65266
+ Struct({
65267
+ fact: Literal("output"),
65268
+ repo: String$5,
65269
+ path: String$5,
65270
+ fingerprint: NullOr(String$5)
65271
+ })
65272
+ ]));
65273
+ const decodeRecord = decodeUnknownOption(Struct({
65274
+ commit: String$5,
65275
+ path: String$5,
65276
+ install: String$5,
65277
+ files: Record(String$5, String$5),
65278
+ outputs: Record(String$5, String$5)
65279
+ }));
65280
+ const cloneRoot = (config, machine) => config.machines[machine]?.clones ?? "~/.local/share/fleetfizz/clones";
65281
+ const originUrl = (origin) => `https://github.com/${origin}.git`;
65282
+ const short = (commit) => commit.slice(0, 7);
65283
+ const expandHome = (path, home) => path === "~" ? home : path.startsWith("~/") ? `${home}${path.slice(1)}` : path;
65284
+ const jsonString = encodeSync(fromJsonString(String$5));
65285
+ const jsonRecord = encodeSync(fromJsonString(Record(String$5, String$5)));
65286
+ const lsRemote = fn("lsRemote")(function* (repo, token) {
65287
+ const spawner = yield* ChildProcessSpawner;
65288
+ const auth = token === void 0 ? [] : [
65289
+ "-c",
65290
+ "credential.helper=",
65291
+ "-c",
65292
+ "credential.helper=!f() { echo username=x-access-token; echo \"password=$FF_TOKEN\"; }; f"
65293
+ ];
65294
+ return yield* scoped(gen(function* () {
65295
+ const handle = yield* spawner.spawn(make$12("git", [
65296
+ ...auth,
65297
+ "ls-remote",
65298
+ originUrl(repo.origin),
65299
+ `refs/heads/${repo.ref}`
65300
+ ], {
65301
+ stdout: "pipe",
65302
+ stderr: "pipe",
65303
+ env: {
65304
+ GIT_TERMINAL_PROMPT: "0",
65305
+ ...token === void 0 ? {} : { FF_TOKEN: token }
65306
+ },
65307
+ extendEnv: true
65308
+ }));
65309
+ const [stdout, stderr, code] = yield* all([
65310
+ runCollect(decodeText(handle.stdout)),
65311
+ runCollect(decodeText(handle.stderr)),
65312
+ handle.exitCode
65313
+ ], { concurrency: "unbounded" });
65314
+ if (code !== 0) {
65315
+ const lines = stderr.join("").trim().split("\n");
65316
+ return { error: lines[lines.length - 1] || `git ls-remote exited ${code}` };
65317
+ }
65318
+ const commit = stdout.join("").split(/\s/)[0];
65319
+ return commit === void 0 || commit === "" ? { error: `no branch ${repo.ref} on origin` } : { commit };
65320
+ })).pipe(orElseSucceed(() => ({ error: "could not run git ls-remote" })));
65321
+ });
65322
+ const observe = (items, root) => {
65323
+ if (items.length === 0) return "";
65324
+ const lines = [
65325
+ `ff_root=${quotePath(root)}`,
65326
+ `if [ -d "$ff_root" ]; then ff_present=true; else ff_present=false; fi`,
65327
+ `ff_state "{\\"provider\\":\\"repos\\",\\"fact\\":\\"cloneRoot\\",\\"path\\":\\"$(ff_json "$ff_root")\\",\\"present\\":$ff_present}"`
65328
+ ];
65329
+ for (const repo of items) {
65330
+ const name = quote(repo.name);
65331
+ 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");
65332
+ 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}"`);
65333
+ }
65334
+ return lines.join("\n");
65335
+ };
65336
+ const decideRepo = (repo, root, observed, resolved, ctx) => {
65337
+ const clone = observed.clones.get(repo.name);
65338
+ const record = observed.records.get(repo.name);
65339
+ const at = clone?.head === void 0 ? {} : { at: short(clone.head) };
65340
+ const fail = (reason) => ({
65341
+ kind: "fail",
65342
+ item: repo.name,
65343
+ ...at,
65344
+ reason
65345
+ });
65346
+ if (observed.cloneRoot?.present === false && !root.startsWith("~")) return fail(`clone root not mounted: ${observed.cloneRoot.path}`);
65347
+ const url = originUrl(repo.origin);
65348
+ 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];
65350
+ if (target === void 0) return fail(`could not resolve ${repo.ref}`);
65351
+ if ("error" in target) return fail(target.error);
65352
+ const { commit } = target;
65353
+ const outputs = observed.outputs.get(repo.name);
65354
+ const install = decideInstall(record, {
65355
+ commit,
65356
+ path: expandHome(`${root}/${repo.name}`, ctx.home),
65357
+ install: repo.install,
65358
+ files: repo.files ?? {},
65359
+ outputs: Object.fromEntries((repo.outputs ?? []).map((o) => [o, outputs?.get(o)]))
65360
+ }, ctx.rebuild(address("repos", repo.name)));
65361
+ const reason = install.run ? install.reason : !clone?.present ? "not cloned" : clone.head !== commit ? `checked out at ${short(clone.head ?? "")}` : void 0;
65362
+ const change = {
65363
+ item: repo,
65364
+ root,
65365
+ commit
65366
+ };
65367
+ if (reason === void 0) return {
65368
+ kind: "leave",
65369
+ item: repo.name,
65370
+ at: short(commit),
65371
+ upkeep: change
65372
+ };
65373
+ const from = record?.commit ?? clone?.head;
65374
+ return {
65375
+ kind: "change",
65376
+ item: repo.name,
65377
+ ...from === void 0 ? {} : { from: short(from) },
65378
+ to: short(commit),
65379
+ reason,
65380
+ change
65381
+ };
65382
+ };
65383
+ const fetchStep = ({ item, root, commit }) => {
65384
+ const url = originUrl(item.origin);
65385
+ const sync = isPin(item.ref) ? [
65386
+ `if [ "$(git -C "$ff_d" rev-parse HEAD)" != ${quote(commit)} ]; then ff_git -C "$ff_d" fetch --quiet origin ${quote(commit)}; fi`,
65387
+ `git -C "$ff_d" checkout --quiet --detach ${quote(commit)}`,
65388
+ `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`
65389
+ ] : [`ff_git -C "$ff_d" fetch --quiet origin ${quote(item.ref)}`, `git -C "$ff_d" reset --quiet --hard ${quote(commit)}`];
65390
+ const files = Object.entries(item.files ?? {}).flatMap(([path, content]) => [`mkdir -p "$(dirname "$ff_d"/${quote(path)})"`, `printf '%s' ${quote(content)} > "$ff_d"/${quote(path)}`]);
65391
+ return {
65392
+ id: `repos:${item.name}:fetch`,
65393
+ items: [item.name],
65394
+ doing: "fetching",
65395
+ run: [
65396
+ `ff_root=${quotePath(root)}`,
65397
+ ...root.startsWith("~") ? [`mkdir -p "$ff_root"`] : [],
65398
+ `ff_d="$ff_root"/${quote(item.name)}`,
65399
+ "(",
65400
+ " set -e",
65401
+ ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
65402
+ ` 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`,
65403
+ ...[
65404
+ ...sync,
65405
+ `git -C "$ff_d" clean --quiet -fd`,
65406
+ ...files
65407
+ ].map((l) => ` ${l}`),
65408
+ ")"
65409
+ ]
65410
+ };
65411
+ };
65412
+ const installStep = ({ item, root }, ctx) => {
65413
+ const env = [
65414
+ `FLEETFIZZ_MACHINE=${quote(ctx.machine.name)}`,
65415
+ `FLEETFIZZ_PLATFORM=${quote(ctx.machine.platform)}`,
65416
+ `FLEETFIZZ_MACHINES=${quote(Object.keys(ctx.config.machines).join(","))}`
65417
+ ].join(" ");
65418
+ const tmp = `${recordDir("repos")}/${quote(`${item.name}.json.tmp`)}`;
65419
+ const record = [
65420
+ `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
65421
+ `ff_outs=""`,
65422
+ ...(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`),
65423
+ `mkdir -p ${recordDir("repos")}`,
65424
+ `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}`,
65425
+ `mv -f ${tmp} ${recordFile("repos", item.name)}`,
65426
+ `rm -f ${quotePath(RECORDS_DIR)}/${quote(`${item.name}.json`)}`
65427
+ ];
65428
+ return {
65429
+ id: `repos:${item.name}:install`,
65430
+ items: [item.name],
65431
+ doing: "installing",
65432
+ run: [
65433
+ `ff_root=${quotePath(root)}`,
65434
+ `ff_d="$ff_root"/${quote(item.name)}`,
65435
+ `(cd "$ff_d" && ${env} "\${SHELL:-/bin/sh}" -lc ${quote(item.install)})`,
65436
+ `ff_rc=$?`,
65437
+ `if [ "$ff_rc" -eq 0 ]; then`,
65438
+ " (",
65439
+ " set -e",
65440
+ ...record.map((l) => ` ${l}`),
65441
+ " )",
65442
+ ` ff_rc=$?`,
65443
+ ` if [ "$ff_rc" -ne 0 ]; then echo "could not write the install record"; fi`,
65444
+ "fi",
65445
+ `(exit "$ff_rc")`
65446
+ ]
65447
+ };
65448
+ };
65449
+ const repos = {
65450
+ name: "repos",
65451
+ rule: "record",
65452
+ dependsOn: ["vp"],
65453
+ items: (section) => (section ?? []).map((repo) => ({
65454
+ ...repo,
65455
+ name: repoName(repo)
65456
+ })),
65457
+ observe: (items, scope) => observe(items, cloneRoot(scope.config, scope.machine.name)),
65458
+ parse(facts, records) {
65459
+ let root;
65460
+ const clones = /* @__PURE__ */ new Map();
65461
+ const outputs = /* @__PURE__ */ new Map();
65462
+ for (const raw of facts) {
65463
+ const fact = decodeFact$1(raw);
65464
+ if (fact._tag === "None") continue;
65465
+ const f = fact.value;
65466
+ switch (f.fact) {
65467
+ case "cloneRoot":
65468
+ root = {
65469
+ path: f.path,
65470
+ present: f.present
65471
+ };
65472
+ break;
65473
+ case "repo":
65474
+ clones.set(f.name, {
65475
+ present: f.present,
65476
+ ...f.head === void 0 || f.head === "" ? {} : { head: f.head },
65477
+ ...f.origin === void 0 || f.origin === "" ? {} : { origin: f.origin }
65478
+ });
65479
+ break;
65480
+ case "output": {
65481
+ const paths = outputs.get(f.repo) ?? /* @__PURE__ */ new Map();
65482
+ paths.set(f.path, f.fingerprint ?? void 0);
65483
+ outputs.set(f.repo, paths);
65484
+ break;
65485
+ }
65486
+ }
65487
+ }
65488
+ const decoded = /* @__PURE__ */ new Map();
65489
+ for (const [name, raw] of Object.entries(records)) {
65490
+ const record = decodeRecord(raw);
65491
+ if (record._tag === "Some") decoded.set(name, record.value);
65492
+ }
65493
+ return {
65494
+ ...root === void 0 ? {} : { cloneRoot: root },
65495
+ clones,
65496
+ outputs,
65497
+ records: decoded
65498
+ };
65499
+ },
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))),
65501
+ decide: ({ items, observed, resolved, ctx }) => {
65502
+ const root = cloneRoot(ctx.config, ctx.machine.name);
65503
+ return items.map((repo) => decideRepo(repo, root, observed, resolved, ctx));
65504
+ },
65505
+ 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)] : [])
65506
+ };
65507
+ //#endregion
65508
+ //#region ../../packages/providers/src/vp/index.ts
65509
+ const VP_PACKAGE = "vite-plus";
65510
+ const TOOL_PACKAGES = {
65511
+ node: ["node"],
65512
+ npm: ["npm"],
65513
+ pnpm: ["pnpm"],
65514
+ yarn: ["yarn", "@yarnpkg/cli-dist"],
65515
+ bun: ["bun"]
65516
+ };
65517
+ const GlobalEntry = Struct({
65518
+ name: String$5,
65519
+ version: String$5,
65520
+ platform: optionalKey(Struct({ node: optionalKey(String$5) }))
65521
+ });
65522
+ const decodeFact = decodeUnknownOption(Union([
65523
+ Struct({
65524
+ fact: Literal("vp"),
65525
+ version: NullOr(String$5),
65526
+ path: optionalKey(String$5)
65527
+ }),
65528
+ Struct({
65529
+ fact: Literal("default"),
65530
+ tool: String$5,
65531
+ version: String$5
65532
+ }),
65533
+ Struct({
65534
+ fact: Literal("mode"),
65535
+ mode: Literals$2(["managed", "system-first"])
65536
+ }),
65537
+ Struct({
65538
+ fact: Literal("globals"),
65539
+ list: ArraySchema(GlobalEntry)
65540
+ })
65541
+ ]));
65542
+ const VP_INSTALL = [
65543
+ "curl -fsSL https://vite.plus | bash",
65544
+ ...vpEnv.split("\n"),
65545
+ "{ hash -r 2>/dev/null || true; }"
65546
+ ].join(" && ");
65547
+ const isHomebrew = (path) => path !== void 0 && (path.includes("/homebrew/") || path.includes("/Cellar/"));
65548
+ const OBSERVE = [
65549
+ "if command -v vp >/dev/null 2>&1; then",
65550
+ ` ff_v=$(command vp --version 2>/dev/null | sed -n 's/^vp v\\{0,1\\}\\([^ ]*\\).*/\\1/p' | head -n 1)`,
65551
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"vp\\",\\"version\\":\\"$(ff_json "$ff_v")\\",\\"path\\":\\"$(ff_json "$(command -v vp)")\\"}"`,
65552
+ " if command vp env doctor 2>/dev/null | grep -q 'system-first mode'; then ff_m=system-first; else ff_m=managed; fi",
65553
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"mode\\",\\"mode\\":\\"$ff_m\\"}"`,
65554
+ ` 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`,
65555
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"default\\",\\"tool\\":\\"$(ff_json "$ff_t")\\",\\"version\\":\\"$(ff_json "$ff_n")\\"}"`,
65556
+ " done",
65557
+ ` ff_g=$(command vp list --global --json 2>/dev/null | tr -d '\\n')`,
65558
+ ` ff_state "{\\"provider\\":\\"vp\\",\\"fact\\":\\"globals\\",\\"list\\":\${ff_g:-[]}}"`,
65559
+ "else",
65560
+ ` ff_state '{"provider":"vp","fact":"vp","version":null}'`,
65561
+ "fi"
65562
+ ].join("\n");
65563
+ const decideSelf = (item, installed, self) => {
65564
+ const at = installed === void 0 ? {} : { at: installed };
65565
+ if (installed === void 0 || import_semver.lt(installed, self)) return {
65566
+ kind: "change",
65567
+ item,
65568
+ ...installed === void 0 ? {} : { from: installed },
65569
+ to: self,
65570
+ change: {
65571
+ kind: "global",
65572
+ name: item,
65573
+ version: self
65574
+ }
65575
+ };
65576
+ if (import_semver.gt(installed, self)) return {
65577
+ kind: "fail",
65578
+ item,
65579
+ ...at,
65580
+ reason: `runs ${SELF_PACKAGE} ${installed}, newer than this ${self}; upgrade here first`
65581
+ };
65582
+ return {
65583
+ kind: "leave",
65584
+ item,
65585
+ ...at
65586
+ };
65587
+ };
65588
+ const MODE_SUBJECT = address("vp", "mode");
65589
+ const NOT_MANAGED = "needs managed mode";
65590
+ const decideMode = (observed, ctx) => {
65591
+ if (observed.vp !== void 0 && observed.mode !== "system-first") return {
65592
+ kind: "leave",
65593
+ item: "mode",
65594
+ at: "managed"
65595
+ };
65596
+ const from = observed.mode === void 0 ? {} : { from: observed.mode };
65597
+ const answer = observed.vp === void 0 ? "managed" : ctx.answer("mode", MODE_SUBJECT);
65598
+ if (answer === "managed") return {
65599
+ kind: "change",
65600
+ item: "mode",
65601
+ ...from,
65602
+ to: "managed",
65603
+ change: { kind: "mode" }
65604
+ };
65605
+ if (answer === "skip") return {
65606
+ kind: "skip",
65607
+ item: "mode",
65608
+ at: "system-first",
65609
+ reason: NOT_MANAGED
65610
+ };
65611
+ return {
65612
+ kind: "ask",
65613
+ item: "mode",
65614
+ at: "system-first",
65615
+ question: {
65616
+ kind: "mode",
65617
+ subject: MODE_SUBJECT,
65618
+ found: "system-first",
65619
+ problem: NOT_MANAGED,
65620
+ choices: [{
65621
+ value: "managed",
65622
+ title: "switch to managed mode"
65623
+ }, {
65624
+ value: "skip",
65625
+ title: "skip Node and package managers on these Machines"
65626
+ }],
65627
+ default: "skip"
65628
+ }
65629
+ };
65630
+ };
65631
+ const decideRanged = (item, observed, resolved, ctx) => {
65632
+ const installed = item.kind === "vp" ? observed.vp?.version : item.kind === "default" ? observed.defaults.get(item.name) : observed.globals.get(item.name)?.version;
65633
+ const decision = decideRangeItem({
65634
+ provider: "vp",
65635
+ item: item.name,
65636
+ range: item.range,
65637
+ installed,
65638
+ available: item.packages.flatMap((p) => resolved[p] ?? []),
65639
+ bumpPath: item.bumpPath,
65640
+ ctx
65641
+ });
65642
+ if (item.kind === "vp" && observed.vp === void 0) {
65643
+ if (decision.kind === "change") return {
65644
+ ...decision,
65645
+ change: {
65646
+ kind: "bootstrap",
65647
+ version: decision.change
65648
+ }
65649
+ };
65650
+ if (decision.kind === "fail" && (item.range === "*" || item.range === "latest")) return {
65651
+ kind: "change",
65652
+ item: item.name,
65653
+ to: "latest",
65654
+ change: { kind: "bootstrap" }
65655
+ };
65656
+ return decision;
65657
+ }
65658
+ if (decision.kind !== "change") return decision;
65659
+ const version = decision.change;
65660
+ if (item.kind === "vp" && isHomebrew(observed.vp?.path)) return {
65661
+ kind: "fail",
65662
+ item: item.name,
65663
+ ...installed === void 0 ? {} : { at: installed },
65664
+ reason: `vp ${installed} was installed with Homebrew; run brew upgrade vite-plus to get ${version}`
65665
+ };
65666
+ const change = item.kind === "vp" ? {
65667
+ kind: "upgrade",
65668
+ version
65669
+ } : item.kind === "default" ? {
65670
+ kind: "default",
65671
+ tool: item.name,
65672
+ version
65673
+ } : {
65674
+ kind: "global",
65675
+ name: item.name,
65676
+ version
65677
+ };
65678
+ return {
65679
+ ...decision,
65680
+ change
65681
+ };
65682
+ };
65683
+ //#endregion
65684
+ //#region ../../packages/providers/src/index.ts
65685
+ const PROVIDERS = [
65686
+ {
65687
+ name: "vp",
65688
+ rule: "range",
65689
+ 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
+ });
65712
+ const defaults = ["node", ...PACKAGE_MANAGERS].flatMap((tool) => {
65713
+ const range = section?.[tool];
65714
+ return range === void 0 ? [] : [{
65715
+ name: tool,
65716
+ kind: "default",
65717
+ packages: TOOL_PACKAGES[tool],
65718
+ range,
65719
+ bumpPath: ["vp", tool]
65720
+ }];
65721
+ });
65722
+ return [
65723
+ {
65724
+ name: "vp",
65725
+ kind: "vp",
65726
+ packages: [VP_PACKAGE],
65727
+ range: section?.version ?? "*",
65728
+ bumpPath: ["vp", "version"]
65729
+ },
65730
+ {
65731
+ name: "mode",
65732
+ kind: "mode"
65733
+ },
65734
+ ...defaults,
65735
+ ...globals,
65736
+ {
65737
+ name: SELF_PACKAGE,
65738
+ kind: "self"
65739
+ }
65740
+ ];
65741
+ },
65742
+ observe: () => OBSERVE,
65743
+ parse(facts) {
65744
+ let vpFact;
65745
+ let mode;
65746
+ const defaults = /* @__PURE__ */ new Map();
65747
+ let globals = [];
65748
+ for (const raw of facts) {
65749
+ const fact = decodeFact(raw);
65750
+ if (fact._tag === "None") continue;
65751
+ const f = fact.value;
65752
+ if (f.fact === "vp" && f.version !== null) vpFact = {
65753
+ version: f.version,
65754
+ ...f.path === void 0 ? {} : { path: f.path }
65755
+ };
65756
+ else if (f.fact === "mode") mode = f.mode;
65757
+ else if (f.fact === "default") defaults.set(f.tool, f.version);
65758
+ else if (f.fact === "globals") globals = f.list;
65759
+ }
65760
+ if (vpFact === void 0) return {
65761
+ defaults: /* @__PURE__ */ new Map(),
65762
+ globals: /* @__PURE__ */ new Map()
65763
+ };
65764
+ return {
65765
+ vp: vpFact,
65766
+ ...mode === void 0 ? {} : { mode },
65767
+ defaults,
65768
+ globals: new Map(globals.map((g) => [g.name, g]))
65769
+ };
65770
+ },
65771
+ resolve: (items) => resolveVersions(items.flatMap((item) => "packages" in item ? item.packages : [])),
65772
+ decide({ items, observed, resolved, ctx }) {
65773
+ const mode = decideMode(observed, ctx);
65774
+ const managed = mode.kind === "leave" || mode.kind === "change";
65775
+ const decisions = items.map((item) => {
65776
+ switch (item.kind) {
65777
+ case "mode": return mode;
65778
+ case "self": return decideSelf(item.name, observed.globals.get(item.name)?.version, ctx.self);
65779
+ case "default":
65780
+ if (!managed) {
65781
+ const at = observed.defaults.get(item.name);
65782
+ return {
65783
+ kind: "skip",
65784
+ item: item.name,
65785
+ ...at === void 0 ? {} : { at },
65786
+ reason: NOT_MANAGED
65787
+ };
65788
+ }
65789
+ return decideRanged(item, observed, resolved, ctx);
65790
+ default: return decideRanged(item, observed, resolved, ctx);
65791
+ }
65792
+ });
65793
+ const nodeChange = decisions.find((d) => d.kind === "change" && d.change.kind === "default" && d.change.tool === "node");
65794
+ const nodeDefault = nodeChange?.kind === "change" ? nodeChange.to : observed.defaults.get("node");
65795
+ return decisions.map((d, i) => {
65796
+ const item = items[i];
65797
+ const current = observed.globals.get(d.item);
65798
+ const node = current?.platform?.node;
65799
+ 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;
65800
+ return {
65801
+ kind: "change",
65802
+ item: d.item,
65803
+ from: current.version,
65804
+ to: current.version,
65805
+ reason: `Node ${node} → ${nodeDefault}`,
65806
+ change: {
65807
+ kind: "reinstall",
65808
+ name: d.item,
65809
+ version: current.version
65810
+ }
65811
+ };
65812
+ });
65813
+ },
65814
+ act(decisions) {
65815
+ const changes = decisions.flatMap((d) => d.kind === "change" ? [d.change] : []);
65816
+ const steps = [];
65817
+ for (const change of changes) switch (change.kind) {
65818
+ case "bootstrap":
65819
+ steps.push({
65820
+ id: "vp:vp:install",
65821
+ items: ["vp"],
65822
+ doing: "installing",
65823
+ run: [change.version === void 0 ? VP_INSTALL : `${VP_INSTALL} && command vp upgrade ${quote(change.version)}`]
65824
+ });
65825
+ break;
65826
+ case "upgrade":
65827
+ steps.push({
65828
+ id: "vp:vp:upgrade",
65829
+ items: ["vp"],
65830
+ doing: "upgrading",
65831
+ run: [`( command vp upgrade ${quote(change.version)} )`]
65832
+ });
65833
+ break;
65834
+ case "mode":
65835
+ steps.push({
65836
+ id: "vp:mode:on",
65837
+ items: ["mode"],
65838
+ doing: "switching",
65839
+ run: ["( command vp env on )"]
65840
+ });
65841
+ break;
65842
+ case "default":
65843
+ steps.push({
65844
+ id: `vp:${change.tool}:default`,
65845
+ items: [change.tool],
65846
+ doing: "installing",
65847
+ run: [`( command vp env default ${quote(change.tool === "node" ? change.version : `${change.tool}@${change.version}`)} )`]
65848
+ });
65849
+ break;
65850
+ case "reinstall": steps.push({
65851
+ id: `vp:${change.name}:reinstall`,
65852
+ items: [change.name],
65853
+ doing: "reinstalling",
65854
+ run: [`( command vp install -g ${quote(`${change.name}@${change.version}`)} )`]
65855
+ });
65856
+ }
65857
+ const installs = changes.flatMap((c) => c.kind === "global" ? [c] : []);
65858
+ if (installs.length > 0) steps.push({
65859
+ id: "vp:globals:install",
65860
+ items: installs.map((c) => c.name),
65861
+ doing: "installing",
65862
+ run: [`( command vp install -g ${installs.map((c) => quote(`${c.name}@${c.version}`)).join(" ")} )`]
65863
+ });
65864
+ return steps;
65865
+ }
65866
+ },
65867
+ brew,
65868
+ cask,
65869
+ repos
65870
+ ];
65189
65871
  //#endregion
65190
65872
  //#region src/live.tsx
65191
65873
  const cell = (initial) => {
@@ -65271,6 +65953,7 @@ const noLive = { ask: void 0 };
65271
65953
  const canRedraw = () => Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
65272
65954
  //#endregion
65273
65955
  //#region src/machine.ts
65956
+ const decodeAt = decodeUnknownOption(AtFact);
65274
65957
  const runOn = fn("runOn")(function* (target, script, secrets, onLine, ledger) {
65275
65958
  let first;
65276
65959
  const run = yield* runScript(target, script, secrets, (line) => {
@@ -65287,16 +65970,16 @@ const runOn = fn("runOn")(function* (target, script, secrets, onLine, ledger) {
65287
65970
  }
65288
65971
  return run;
65289
65972
  });
65290
- const GATHER_TIMEOUT = "2 minutes";
65291
- const gather = fn("gather")(function* (target, script, ledger) {
65973
+ const CHECK_TIMEOUT = "2 minutes";
65974
+ const check = fn("check")(function* (target, script, ledger) {
65292
65975
  const facts = [];
65293
65976
  const output = [];
65294
65977
  const run = yield* runOn(target, script, [], (line) => sync(() => {
65295
65978
  if (line.kind === "state") facts.push(line.fact);
65296
65979
  else if (line.kind === "text" && line.text.trim() !== "") output.push(line.text.trimEnd());
65297
65980
  }), ledger).pipe(timeoutOrElse({
65298
- duration: GATHER_TIMEOUT,
65299
- orElse: () => ledger.fail(target.name, [`checking took more than ${GATHER_TIMEOUT}; something on that Machine is blocking`, ...output]).pipe(as({ kind: "failed" }))
65981
+ duration: CHECK_TIMEOUT,
65982
+ orElse: () => ledger.fail(target.name, [`checking took more than ${CHECK_TIMEOUT}; something on that Machine is blocking`, ...output]).pipe(as({ kind: "failed" }))
65300
65983
  }));
65301
65984
  if (run.kind !== "ran") return { kind: run.kind };
65302
65985
  const state = assembleState(facts);
@@ -65305,7 +65988,7 @@ const gather = fn("gather")(function* (target, script, ledger) {
65305
65988
  return { kind: "failed" };
65306
65989
  }
65307
65990
  return {
65308
- kind: "gathered",
65991
+ kind: "checked",
65309
65992
  state
65310
65993
  };
65311
65994
  });
@@ -65340,10 +66023,17 @@ const apply = fn("apply")(function* (target, p, script, secrets, ledger) {
65340
66023
  });
65341
66024
  break;
65342
66025
  }
66026
+ case "skip":
66027
+ yield* ledger.stepSkipped(target.name, line.skip.id, line.skip.reason);
66028
+ break;
65343
66029
  case "text":
65344
66030
  if (line.text.trim() !== "") (current?.output ?? head).push(line.text.trimEnd());
65345
66031
  break;
65346
- case "state": if (line.fact.fact === "repo" && line.fact.head !== void 0) yield* ledger.fetched(target.name, line.fact.name, line.fact.head);
66032
+ case "state": {
66033
+ const at = decodeAt(line.fact);
66034
+ if (at._tag === "Some") yield* ledger.reached(target.name, address(at.value.provider, at.value.item), at.value.at);
66035
+ break;
66036
+ }
65347
66037
  }
65348
66038
  }), ledger);
65349
66039
  const finish = fn("finish")(function* (outcome) {
@@ -65364,43 +66054,53 @@ const apply = fn("apply")(function* (target, p, script, secrets, ledger) {
65364
66054
  yield* ledger.fail(target.name, ["apply did not finish", ...head]);
65365
66055
  return yield* finish("failed");
65366
66056
  }
65367
- return yield* finish(failed.length > 0 || p.failures.length > 0 ? "failed" : "ok");
66057
+ const planFailed = p.providers.some((pp) => pp.decisions.some((d) => d.kind === "fail"));
66058
+ return yield* finish(failed.length > 0 || planFailed ? "failed" : "ok");
65368
66059
  });
65369
66060
  //#endregion
65370
66061
  //#region src/questions.ts
65371
- const newest = (group) => [...group].map((q) => q.installed).sort((a, b) => b.localeCompare(a, void 0, { numeric: true }))[0] ?? "";
65372
- const askQuestions = fn("askQuestions")(function* (questions, flags, ledger, ask) {
65373
- const groups = /* @__PURE__ */ new Map();
65374
- for (const q of questions) groups.set(q.package, [...groups.get(q.package) ?? [], q]);
65375
- const decided = {};
65376
- for (const [pkg, group] of groups) {
65377
- const where = group.map((q) => `${q.installed} on ${q.machine}`).join(", ");
65378
- const range = group[0]?.range ?? "";
65379
- const bump = {
65380
- kind: "bump",
65381
- installed: newest(group)
65382
- };
65383
- const bumpTitle = `bump the config to ${bumpedRange(range, newest(group))}`;
65384
- const answer = (decision) => decision.kind === "bump" ? bumpTitle : decision.kind === "skip" ? "skip on these Machines" : "downgrade into the range";
65385
- yield* ledger.note("ask", `${pkg} ${where} is above ${range}`);
65386
- const decision = flags.bump ? bump : flags.enforce ? { kind: "downgrade" } : ask === void 0 ? { kind: "skip" } : yield* ask([
65387
- {
65388
- title: bumpTitle,
65389
- value: bump
65390
- },
65391
- {
65392
- title: "skip on these Machines (leave as is, report SKIP)",
65393
- value: { kind: "skip" }
65394
- },
65395
- {
65396
- title: "downgrade into the range",
65397
- value: { kind: "downgrade" }
65398
- }
65399
- ]);
65400
- yield* ledger.note("answer", answer(decision));
65401
- decided[pkg] = decision;
66062
+ const parseAnswers = (raw) => {
66063
+ const given = /* @__PURE__ */ new Map();
66064
+ for (const entry of raw) {
66065
+ const at = entry.lastIndexOf("=");
66066
+ const key = entry.slice(0, at);
66067
+ const choice = entry.slice(at + 1);
66068
+ const kind = key.split(":")[0] ?? "";
66069
+ if (at <= 0 || !isQuestionKind(kind)) return { invalid: `--answer ${entry}: expected kind=choice or kind:subject=choice, kind one of ${Object.keys(QUESTION_CHOICES).join(", ")}` };
66070
+ const choices = QUESTION_CHOICES[kind];
66071
+ if (!choices.includes(choice)) return { invalid: `--answer ${entry}: ${kind} takes ${choices.join(", ")}` };
66072
+ given.set(key, choice);
66073
+ }
66074
+ return { answer: (kind, subject) => given.get(`${kind}:${subject}`) ?? given.get(kind) };
66075
+ };
66076
+ const newestOf = (found) => found.toSorted((a, b) => b.localeCompare(a, void 0, { numeric: true }))[0] ?? "";
66077
+ const askQuestions = fn("askQuestions")(function* (input) {
66078
+ const groups = Map.groupBy(input.asked, (a) => `${a.question.kind}:${a.question.subject}`);
66079
+ const answered = [];
66080
+ for (const group of groups.values()) {
66081
+ const question = group[0]?.question;
66082
+ 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}`);
66086
+ if (input.dryRun) {
66087
+ yield* input.ledger.note("answer", "not asked (dry run)");
66088
+ continue;
66089
+ }
66090
+ const choices = question.choices.map((c) => ({
66091
+ value: c.value,
66092
+ title: c.value === "bump" && question.bump !== void 0 ? `bump the config to ${bumpedRange(question.bump.range, newest)}` : c.title
66093
+ }));
66094
+ const answer = input.answer(question.kind, question.subject) ?? (input.ask === void 0 ? question.default : yield* input.ask(choices));
66095
+ yield* input.ledger.note("answer", choices.find((c) => c.value === answer)?.title ?? answer);
66096
+ answered.push({
66097
+ question,
66098
+ machines: group.map((a) => a.machine),
66099
+ newest,
66100
+ answer
66101
+ });
65402
66102
  }
65403
- return decided;
66103
+ return answered;
65404
66104
  });
65405
66105
  //#endregion
65406
66106
  //#region src/ship.ts
@@ -65411,48 +66111,64 @@ const unknownMachines = (config, tailnet, only) => {
65411
66111
  const known = new Set(tailnet.fleet.map((m) => m.name));
65412
66112
  const referenced = [
65413
66113
  ...Object.keys(config.machines),
65414
- ...(config.repos ?? []).flatMap((r) => r.machines ?? []),
65415
- ...config.vp?.machines ?? [],
66114
+ ...PROVIDERS.flatMap((p) => machinesNamed(p, config)),
65416
66115
  ...only
65417
66116
  ];
65418
66117
  return [...new Set(referenced.filter((name) => !known.has(name)))];
65419
66118
  };
65420
- const packagesToResolve = (config) => {
65421
- const vp = config.vp;
65422
- if (vp === void 0) return [];
65423
- return [
65424
- ...vp.version === void 0 ? [] : [VP_PACKAGE],
65425
- ...vp.node === void 0 ? [] : [NODE_PACKAGE],
65426
- ...Object.keys(vp.globals ?? {})
65427
- ];
65428
- };
65429
- const rebuildOf = (flags) => flags.rebuild.length === 0 ? "none" : flags.rebuild.includes("all") ? "all" : flags.rebuild;
65430
- const staleConfigs = fn("staleConfigs")(function* (local, inputIsLocalFile, gathered, ledger) {
65431
- const stale = [];
65432
- for (const { machine, local: isLocal, state } of gathered) {
65433
- if (isLocal && inputIsLocalFile || state.config === void 0) continue;
66119
+ const unshippable = (config, tailnet, only, source) => {
66120
+ const unknown = unknownMachines(config, tailnet, only);
66121
+ 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));
66123
+ if (!fleet.has(tailnet.self.name)) return `this Machine (${tailnet.self.name}) is not in machines in ${source}`;
66124
+ const outsideFleet = only.filter((name) => !fleet.has(name));
66125
+ if (outsideFleet.length > 0) return `not in machines in ${source}: ${outsideFleet.join(", ")}`;
66126
+ };
66127
+ const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger) {
66128
+ const copies = [];
66129
+ for (const { machine, state } of checked) {
66130
+ if (state.config === void 0) continue;
65434
66131
  const parsed = yield* result(parseConfig(machine.name, state.config));
65435
- if (parsed._tag === "Failure") {
65436
- yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
65437
- continue;
65438
- }
65439
- const remote = parsed.success;
65440
- if (isNewer(remote.shippedTo?.[machine.name], local.shippedTo?.[machine.name])) stale.push(`${machine.name} (shipped ${remote.shippedAt} from ${remote.shippedFrom ?? "unknown"})`);
66132
+ if (parsed._tag === "Failure") yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
66133
+ else copies.push({
66134
+ machine: machine.name,
66135
+ config: parsed.success,
66136
+ text: state.config
66137
+ });
65441
66138
  }
65442
- return stale;
66139
+ return [...Map.groupBy(staleCopies(config, copies, own), (c) => c.text)].flatMap(([text, [first, ...rest]]) => {
66140
+ if (first === void 0) return [];
66141
+ const { config, machine } = first;
66142
+ const machines = [machine, ...rest.map((c) => c.machine)];
66143
+ return [{
66144
+ text,
66145
+ config,
66146
+ machines,
66147
+ description: `${machines.join(", ")} (shipped ${config.shippedAt} from ${config.shippedFrom ?? "unknown"})`
66148
+ }];
66149
+ });
65443
66150
  });
65444
66151
  const schemaJson = encodeSync(fromJsonString(Unknown, { space: 2 }));
65445
66152
  const ship = fn("ship")(function* (flags) {
66153
+ while ((yield* shipPass(flags)) === "adopted");
66154
+ });
66155
+ const shipPass = fn("shipPass")(function* (flags) {
65446
66156
  const fs = yield* FileSystem;
66157
+ const parsed = parseAnswers(flags.answers);
66158
+ if ("invalid" in parsed) return yield* fail$1(new ShipError(parsed.invalid));
66159
+ const given = parsed.answer;
66160
+ const notAddress = flags.rebuild.find((r) => r !== "all" && !PROVIDERS.some((p) => p.rule === "record" && p.name === parseAddress(r)?.provider));
66161
+ if (notAddress !== void 0) return yield* fail$1(new ShipError(`--rebuild ${notAddress}: expected a record Item (repos:<name>) or all`));
65447
66162
  const path = flags.config ?? configPath(process.env);
65448
66163
  const config = yield* loadConfig(path);
66164
+ const ordered = orderProviders(PROVIDERS, (p) => dependenciesOf(p, config));
66165
+ if ("cycle" in ordered) return yield* fail$1(new ShipError(`dependsOn cycle: ${ordered.cycle.join(" → ")}`));
66166
+ const providers = ordered.order;
65449
66167
  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(", ")})`));
66168
+ const problem = unshippable(config, tailnet, flags.only, path);
66169
+ if (problem !== void 0) return yield* fail$1(new ShipError(problem));
65452
66170
  const fleet = new Set(Object.keys(config.machines));
65453
- if (!fleet.has(tailnet.self.name)) return yield* fail$1(new ShipError(`this Machine (${tailnet.self.name}) is not in machines in ${path}`));
65454
- const outsideFleet = flags.only.filter((name) => !fleet.has(name));
65455
- if (outsideFleet.length > 0) return yield* fail$1(new ShipError(`not in machines in ${path}: ${outsideFleet.join(", ")}`));
66171
+ const own = flags.config === void 0 ? tailnet.self.name : void 0;
65456
66172
  const order = Object.keys(config.machines);
65457
66173
  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));
65458
66174
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
@@ -65466,8 +66182,10 @@ const ship = fn("ship")(function* (flags) {
65466
66182
  });
65467
66183
  const redraw = canRedraw();
65468
66184
  let changing = false;
66185
+ let adopted = false;
65469
66186
  const finish = fn("finish")(function* () {
65470
66187
  const state = yield* ledger.end;
66188
+ if (adopted) return;
65471
66189
  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)))));
65472
66190
  yield* log(ledgerText(state, redraw ? {
65473
66191
  full: false,
@@ -65477,16 +66195,11 @@ const ship = fn("ship")(function* (flags) {
65477
66195
  full: true,
65478
66196
  now: state.ended ?? 0
65479
66197
  }, color));
65480
- if (tally(state).failed > 0) process.exitCode = 1;
66198
+ if (anyFailed(state)) process.exitCode = 1;
65481
66199
  });
65482
66200
  yield* addFinalizer(() => finish());
65483
66201
  const live = redraw ? yield* startLive(store, color) : noLive;
65484
- const cloneRootOf = (machine) => config.machines[machine.name]?.clones ?? "~/.local/share/fleetfizz/clones";
65485
- const gatherRepos = (config.repos ?? []).map((r) => ({
65486
- name: repoName(r),
65487
- outputs: r.outputs ?? []
65488
- }));
65489
- const gathered = yield* forEach$1(targetsList, fn("gatherOne")(function* (machine) {
66202
+ const checked = yield* forEach$1(targetsList, fn("checkOne")(function* (machine) {
65490
66203
  const local = machine.name === tailnet.self.name;
65491
66204
  if (!machine.online) {
65492
66205
  yield* ledger.skip(machine.name, "offline");
@@ -65496,15 +66209,25 @@ const ship = fn("ship")(function* (flags) {
65496
66209
  yield* ledger.skip(machine.name, "no Tailscale SSH server");
65497
66210
  return;
65498
66211
  }
65499
- const script = gatherScript({
65500
- cloneRoot: cloneRootOf(machine),
65501
- repos: gatherRepos
66212
+ const identity = {
66213
+ name: machine.name,
66214
+ platform: machine.platform
66215
+ };
66216
+ const script = checkScript({
66217
+ scope: {
66218
+ machine: identity,
66219
+ config
66220
+ },
66221
+ providers: providers.map((provider) => ({
66222
+ provider,
66223
+ items: targetedItems(provider, config, identity)
66224
+ }))
65502
66225
  });
65503
- const result = yield* gather({
66226
+ const result = yield* check({
65504
66227
  name: machine.name,
65505
66228
  local
65506
66229
  }, script, ledger);
65507
- if (result.kind !== "gathered") return void 0;
66230
+ if (result.kind !== "checked") return void 0;
65508
66231
  yield* ledger.checked(machine.name);
65509
66232
  return {
65510
66233
  machine,
@@ -65512,60 +66235,109 @@ const ship = fn("ship")(function* (flags) {
65512
66235
  state: result.state
65513
66236
  };
65514
66237
  }), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
65515
- const stale = yield* staleConfigs(config, flags.config === void 0, gathered, ledger);
66238
+ const stale = yield* staleConfigs(config, own, checked, ledger);
65516
66239
  if (stale.length > 0) {
65517
- const what = `newer config on ${stale.join(", ")}`;
66240
+ const what = `newer config on ${stale.map((s) => s.description).join(", ")}`;
66241
+ const answers = stale.flatMap((s) => s.machines).map((m) => given("config", m));
65518
66242
  if (flags.dryRun) yield* ledger.note("info", `${what}; a Ship would ask before overwriting it`);
65519
- else if (flags.overwriteConfig) yield* ledger.note("info", `${what}; overwriting it (--overwrite-config)`);
66243
+ else if (answers.every((a) => a === "overwrite")) yield* ledger.note("info", `${what}; overwriting it, as answered`);
66244
+ else if (answers.includes("stop")) return yield* fail$1(new ShipError("aborted: the config here is older"));
65520
66245
  else if (live.ask === void 0) return yield* fail$1(new ShipError(`${what}; copy it here first, or pass --overwrite-config`));
65521
66246
  else {
66247
+ const usable = [];
66248
+ for (const s of stale) {
66249
+ const why = unshippable(s.config, tailnet, flags.only, `the config on ${s.machines[0]}`);
66250
+ if (why === void 0) usable.push(s);
66251
+ else yield* ledger.note("info", `cannot ship from here: ${why}`);
66252
+ }
65522
66253
  yield* ledger.note("ask", what);
65523
- const overwrite = yield* live.ask([{
65524
- title: "overwrite it with this Machine's config",
65525
- value: true
65526
- }, {
65527
- title: "stop",
65528
- value: false
65529
- }]);
65530
- yield* ledger.note("answer", overwrite ? "overwrite it" : "stop");
65531
- if (!overwrite) return yield* fail$1(new ShipError("aborted: the config here is older"));
66254
+ const choice = yield* live.ask([
66255
+ ...usable.map((s) => ({
66256
+ title: `use the config on ${s.machines[0]} (replaces this one)`,
66257
+ value: s
66258
+ })),
66259
+ {
66260
+ title: "overwrite it with this Machine's config",
66261
+ value: "overwrite"
66262
+ },
66263
+ {
66264
+ title: "stop",
66265
+ value: "stop"
66266
+ }
66267
+ ]);
66268
+ if (choice === "stop") {
66269
+ yield* ledger.note("answer", "stop");
66270
+ return yield* fail$1(new ShipError("aborted: the config here is older"));
66271
+ }
66272
+ if (choice === "overwrite") yield* ledger.note("answer", "overwrite it");
66273
+ else {
66274
+ yield* ledger.note("answer", `use the config on ${choice.machines[0]}`);
66275
+ yield* fs.writeFileString(path, choice.text);
66276
+ adopted = true;
66277
+ return "adopted";
66278
+ }
65532
66279
  }
65533
66280
  }
65534
- const versions = yield* resolveVersions(packagesToResolve(config));
65535
- const planAll = (cfg, answers) => gathered.map((g) => ({
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
+ });
66289
+ const lent = targeted.some(({ provider, items }) => provider.name === "repos" && items.length > 0) ? yield* readLentToken() : { token: void 0 };
66290
+ if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
66291
+ 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" }));
66292
+ const planAll = (cfg, answer) => checked.map((g) => ({
65536
66293
  ...g,
65537
- plan: plan({
65538
- config: cfg,
65539
- machine: {
65540
- name: g.machine.name,
65541
- platform: g.machine.platform
65542
- },
66294
+ plan: planMachine({
66295
+ providers,
65543
66296
  state: g.state,
65544
- self: flags.version,
65545
- versions,
65546
- rebuild: rebuildOf(flags),
65547
- answers
66297
+ resolved,
66298
+ ctx: {
66299
+ machine: {
66300
+ name: g.machine.name,
66301
+ platform: g.machine.platform
66302
+ },
66303
+ config: cfg,
66304
+ self: flags.version,
66305
+ rebuild: (address) => flags.rebuild.includes("all") || flags.rebuild.includes(address),
66306
+ answer
66307
+ }
65548
66308
  })
65549
66309
  }));
65550
66310
  let cfg = config;
65551
- let planned = planAll(cfg, {});
65552
- const questions = planned.flatMap((g) => g.plan.questions);
65553
- if (questions.length > 0 && flags.dryRun) for (const q of questions) {
65554
- yield* ledger.note("ask", `${q.package} ${q.installed} on ${q.machine} is above ${q.range}`);
65555
- yield* ledger.note("answer", "not asked (dry run)");
65556
- }
65557
- if (questions.length > 0 && !flags.dryRun) {
65558
- const decided = yield* askQuestions(questions, flags, ledger, live.ask);
65559
- for (const [pkg, decision] of Object.entries(decided)) {
65560
- if (decision.kind !== "bump") continue;
66311
+ const decided = /* @__PURE__ */ new Map();
66312
+ const answer = (kind, subject) => decided.get(`${kind}:${subject}`) ?? given(kind, subject);
66313
+ let planned = planAll(cfg, answer);
66314
+ for (;;) {
66315
+ const asked = planned.flatMap((g) => g.plan.providers.flatMap((pp) => pp.decisions.flatMap((d) => d.kind === "ask" ? [{
66316
+ machine: g.machine.name,
66317
+ question: d.question
66318
+ }] : [])));
66319
+ const answered = yield* askQuestions({
66320
+ asked,
66321
+ answer,
66322
+ ledger,
66323
+ ask: live.ask,
66324
+ dryRun: flags.dryRun
66325
+ });
66326
+ let progressed = false;
66327
+ for (const { question, newest, answer: choice } of answered) {
66328
+ const key = `${question.kind}:${question.subject}`;
66329
+ progressed ||= !decided.has(key);
66330
+ decided.set(key, choice);
66331
+ if (choice !== "bump" || question.bump === void 0) continue;
65561
66332
  changing = true;
65562
- yield* bumpConfig(path, pkg, decision.installed);
66333
+ const range = yield* bumpConfig(path, question.bump.path, newest);
66334
+ progressed ||= range !== question.bump.range;
66335
+ cfg = yield* loadConfig(path);
65563
66336
  }
65564
- if (Object.values(decided).some((d) => d.kind === "bump")) cfg = yield* loadConfig(path);
65565
- const answers = Object.fromEntries(Object.entries(decided).flatMap(([pkg, d]) => d.kind === "bump" ? [] : [[pkg, d.kind]]));
65566
- planned = planAll(cfg, answers);
66337
+ if (!progressed) break;
66338
+ planned = planAll(cfg, answer);
65567
66339
  }
65568
- for (const { machine, plan: p, state } of planned) yield* ledger.plan(machine.name, p, state);
66340
+ for (const { machine, plan: p } of planned) yield* ledger.plan(machine.name, p);
65569
66341
  if (flags.dryRun) return;
65570
66342
  const shippedAt = formatIso(yield* now);
65571
66343
  changing = true;
@@ -65573,49 +66345,42 @@ const ship = fn("ship")(function* (flags) {
65573
66345
  const stampWith = (reached) => stampText(original, {
65574
66346
  shippedAt,
65575
66347
  shippedFrom: tailnet.self.name,
65576
- reached: [...reached, ...flags.config === void 0 ? [tailnet.self.name] : []]
66348
+ reached,
66349
+ own
65577
66350
  });
65578
66351
  const stamped = stampWith(planned.map((g) => g.machine.name));
65579
66352
  yield* fs.writeFileString(path, stamped);
65580
66353
  const schema = `${schemaJson(fleetConfigJsonSchema())}\n`;
65581
- const lent = planned.some((g) => g.plan.repos.length > 0) ? yield* readLentToken() : { token: void 0 };
65582
- if (lent.token === void 0 && lent.reason !== void 0) yield* ledger.note("info", `no gh token (${lent.reason}); private repos will fail`);
65583
- const machineNames = [...fleet];
65584
66354
  const stdin = applyStdin(lent.token, stamped, schema);
65585
66355
  const delivered = yield* forEach$1(planned, fn("applyOne")(function* ({ machine, local, plan: p }) {
65586
- const script = applyScript({
65587
- plan: p,
65588
- cloneRoot: cloneRootOf(machine),
65589
- env: {
65590
- machine: machine.name,
65591
- platform: machine.platform,
65592
- machines: machineNames
65593
- }
65594
- });
65595
66356
  return (yield* apply({
65596
66357
  name: machine.name,
65597
66358
  local
65598
- }, p, script, stdin, ledger)).done.includes("config") ? machine.name : void 0;
66359
+ }, p, applyScript(p), stdin, ledger)).done.includes("config") ? machine.name : void 0;
65599
66360
  }), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((name) => name !== void 0)));
65600
66361
  yield* fs.writeFileString(path, stampWith(delivered));
65601
66362
  }, scoped);
65602
66363
  //#endregion
65603
- //#region src/bin.ts
66364
+ //#region src/command.ts
65604
66365
  const shipCommand = make("ship", {
65605
66366
  dryRun: Boolean$1("dry-run").pipe(withDefault(false), withDescription$1("Check and plan only. Print every Machine's plan and change nothing.")),
65606
- only: String$1("only").pipe(atLeast(0), withDescription$1("Ship only to these Machines (repeat the flag).")),
65607
- rebuild: String$1("rebuild").pipe(atLeast(0), withDescription$1("Run a repo's Install Command even when its record matches; `all` for every repo.")),
65608
- bump: Boolean$1("bump").pipe(withDefault(false), withDescription$1("Answer every above-range question by bumping the config.")),
65609
- enforce: Boolean$1("enforce").pipe(withDefault(false), withDescription$1("Answer every above-range question by downgrading into the range.")),
65610
- overwriteConfig: Boolean$1("overwrite-config").pipe(withDefault(false), withDescription$1("Ship even when another Machine holds a newer config, overwriting it with this one.")),
65611
- config: String$1("config").pipe(optional, withDescription$1("Config file. Default: $XDG_CONFIG_HOME/fleetfizz/config.jsonc."))
66367
+ only: String$1("only").pipe(atLeast(0), withMetavar("machine"), withDescription$1("Ship only to this Machine. Repeat the flag for more.")),
66368
+ 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.")),
66369
+ 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.")),
66370
+ bump: Boolean$1("bump").pipe(withDefault(false), withDescription$1("Answer every above-range Question by bumping the config (`--answer range=bump`).")),
66371
+ enforce: Boolean$1("enforce").pipe(withDefault(false), withDescription$1("Answer every above-range Question by downgrading into the Range (`--answer range=downgrade`).")),
66372
+ overwriteConfig: Boolean$1("overwrite-config").pipe(withDefault(false), withDescription$1("Ship even when another Machine holds a newer config, overwriting it (`--answer config=overwrite`).")),
66373
+ config: String$1("config").pipe(optional, withMetavar("path"), withDescription$1("The config file. Default: `$XDG_CONFIG_HOME/fleetfizz/config.jsonc`."))
65612
66374
  }).pipe(withDescription("Ship the config to every reachable Machine."), withHandler((flags) => ship({
65613
66375
  dryRun: flags.dryRun,
65614
66376
  only: flags.only,
65615
66377
  rebuild: flags.rebuild,
65616
- bump: flags.bump,
65617
- enforce: flags.enforce,
65618
- overwriteConfig: flags.overwriteConfig,
66378
+ answers: [
66379
+ ...flags.answer,
66380
+ ...flags.bump ? ["range=bump"] : [],
66381
+ ...flags.enforce ? ["range=downgrade"] : [],
66382
+ ...flags.overwriteConfig ? ["config=overwrite"] : []
66383
+ ],
65619
66384
  config: flags.config._tag === "Some" ? flags.config.value : void 0,
65620
66385
  version
65621
66386
  }).pipe(catch_((e) => gen(function* () {
@@ -65623,6 +66388,8 @@ const shipCommand = make("ship", {
65623
66388
  process.exitCode = 1;
65624
66389
  })))));
65625
66390
  const reportCommand = make("report", {}).pipe(withDescription("Print the full Ledger of the last Ship from this Machine."), withHandler(() => report()));
66391
+ //#endregion
66392
+ //#region src/bin.ts
65626
66393
  run(make("fizz").pipe(withDescription("Keep every Machine on your tailnet in line with one config."), withSubcommands([shipCommand, reportCommand])), { version }).pipe(provide(layer), runMain);
65627
66394
  //#endregion
65628
66395
  export {};