fleetfizz 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/fleetfizz.mjs +987 -654
  2. package/package.json +1 -1
@@ -6917,6 +6917,11 @@ const ignore$1 = /*#__PURE__*/ dual((args) => isEffect$1(args[0]), (self, option
6917
6917
  });
6918
6918
  });
6919
6919
  /** @internal */
6920
+ const option$1 = (self) => match$1(self, {
6921
+ onFailure: none,
6922
+ onSuccess: some
6923
+ });
6924
+ /** @internal */
6920
6925
  const result$1 = (self) => matchEager(self, {
6921
6926
  onFailure: fail$5,
6922
6927
  onSuccess: succeed$7
@@ -11094,6 +11099,49 @@ const tap = tap$1;
11094
11099
  */
11095
11100
  const result = result$1;
11096
11101
  /**
11102
+ * Converts success to `Option.some` and failure to `Option.none`.
11103
+ *
11104
+ * **When to use**
11105
+ *
11106
+ * Use when you only care whether an effect succeeds and want recoverable
11107
+ * failures represented as `Option.none`.
11108
+ *
11109
+ * **Details**
11110
+ *
11111
+ * Success values become `Option.some`, recoverable failures become
11112
+ * `Option.none`, and defects still fail the effect.
11113
+ *
11114
+ * **Gotchas**
11115
+ *
11116
+ * `option` only captures typed, recoverable failures as `Option.none`.
11117
+ * Defects and interruptions are not captured inside the `Option` and still
11118
+ * fail the effect.
11119
+ *
11120
+ * `option` also discards typed failure values. Use `result` if the failure
11121
+ * value matters.
11122
+ *
11123
+ * **Example** (Capturing success or failure as Option)
11124
+ *
11125
+ * ```ts import.meta.vitest
11126
+ * import { Effect, Option } from "effect"
11127
+ *
11128
+ * const program = Effect.all([
11129
+ * Effect.option(Effect.succeed(1)),
11130
+ * Effect.option(Effect.fail("missing"))
11131
+ * ])
11132
+ *
11133
+ * Effect.runSync(program) // => [Option.some(1), Option.none()]
11134
+ * ```
11135
+ *
11136
+ * @see {@link result} for a version that uses `Result` instead.
11137
+ * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`.
11138
+ *
11139
+ * @stability stable
11140
+ * @category error handling
11141
+ * @since 2.0.0
11142
+ */
11143
+ const option = option$1;
11144
+ /**
11097
11145
  * Transforms an effect to encapsulate both failure and success using the `Exit`
11098
11146
  * data type.
11099
11147
  *
@@ -29233,7 +29281,7 @@ const visualLength = (text) => {
29233
29281
  * Helper function to pad strings to a specified width.
29234
29282
  * @internal
29235
29283
  */
29236
- const pad$1 = (s, width) => {
29284
+ const pad = (s, width) => {
29237
29285
  const actualLength = visualLength(s);
29238
29286
  const padding = Math.max(0, width - actualLength);
29239
29287
  return s + " ".repeat(padding);
@@ -29245,7 +29293,7 @@ const pad$1 = (s, width) => {
29245
29293
  const renderTable = (rows, widthCap) => {
29246
29294
  const maxColumn = Math.max(...rows.map((r) => visualLength(r.left))) + 4;
29247
29295
  const col = widthCap === void 0 ? maxColumn : Math.min(maxColumn, widthCap);
29248
- return rows.map(({ left, right }) => ` ${pad$1(left, Math.max(col, visualLength(left) + 1))}${right}`).join("\n");
29296
+ return rows.map(({ left, right }) => ` ${pad(left, Math.max(col, visualLength(left) + 1))}${right}`).join("\n");
29249
29297
  };
29250
29298
  const formatSubcommandName = (name, alias) => alias ? `${name}, ${alias}` : name;
29251
29299
  /**
@@ -32392,7 +32440,7 @@ const runWith = (command, config) => {
32392
32440
  };
32393
32441
  //#endregion
32394
32442
  //#region package.json
32395
- var version = "0.5.0";
32443
+ var version = "0.5.2";
32396
32444
  //#endregion
32397
32445
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
32398
32446
  /**
@@ -35226,7 +35274,7 @@ const stampText = (text, stamp) => {
35226
35274
  const fleet = machinesOf(previous);
35227
35275
  const shippedTo = {
35228
35276
  ...Object.fromEntries(Object.entries(shippedToOf(previous)).filter(([name]) => fleet.includes(name))),
35229
- ...Object.fromEntries(stamp.reached.map((name) => [name, stamp.shippedAt]))
35277
+ ...Object.fromEntries([...stamp.reached, ...stamp.own === void 0 ? [] : [stamp.own]].map((name) => [name, stamp.shippedAt]))
35230
35278
  };
35231
35279
  let out = set(text, "shippedAt", stamp.shippedAt);
35232
35280
  out = set(out, "shippedFrom", stamp.shippedFrom);
@@ -35244,6 +35292,7 @@ const shippedToOf = (config) => {
35244
35292
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
35245
35293
  };
35246
35294
  const isNewer = (remote, local) => remote !== void 0 && (local === void 0 || remote > local);
35295
+ const staleCopies = (config, copies, own) => copies.filter((c) => c.machine !== own && isNewer(c.config.shippedTo?.[c.machine], config.shippedTo?.[c.machine]));
35247
35296
  //#endregion
35248
35297
  //#region ../../packages/remote/src/shell.ts
35249
35298
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
@@ -35271,11 +35320,11 @@ const fingerprintFn = [
35271
35320
  "}"
35272
35321
  ].join("\n");
35273
35322
  //#endregion
35274
- //#region ../../packages/remote/src/gather.ts
35323
+ //#region ../../packages/remote/src/check.ts
35275
35324
  const STATE_DIR = "~/.local/state/fleetfizz";
35276
35325
  const RECORDS_DIR = `${STATE_DIR}/records`;
35277
35326
  const CONFIG_DIR_SH = "\"${XDG_CONFIG_HOME:-$HOME/.config}/fleetfizz\"";
35278
- const gatherScript = (input) => {
35327
+ const checkScript = (input) => {
35279
35328
  const lines = [
35280
35329
  prelude,
35281
35330
  fingerprintFn,
@@ -35768,6 +35817,7 @@ const doneLine = (id, ok, code) => code === void 0 ? `printf 'ff-done {"id":"%s"
35768
35817
  const jsonString = encodeSync(fromJsonString(String$5));
35769
35818
  const jsonRecord = encodeSync(fromJsonString(Record(String$5, String$5)));
35770
35819
  const stepId = (repo, step) => `repo:${repo}:${step}`;
35820
+ const isFetchStep = (id) => id.startsWith("repo:") && id.endsWith(":fetch");
35771
35821
  const vpStepId = (step) => step.kind === "reinstall" ? `vp:reinstall:${step.name}` : step.kind === "globals" ? "vp:globals" : `vp:${step.kind}`;
35772
35822
  const CONFIG_STEP = "config";
35773
35823
  const VP_INSTALL = [
@@ -35775,6 +35825,15 @@ const VP_INSTALL = [
35775
35825
  ...vpEnv.split("\n"),
35776
35826
  "{ hash -r 2>/dev/null || true; }"
35777
35827
  ].join(" && ");
35828
+ const marked = (id, run, then = []) => [
35829
+ stepLine(id),
35830
+ ...run,
35831
+ `ff_code=$?`,
35832
+ `if [ "$ff_code" -ne 0 ]; then ${doneLine(id, false, "$ff_code")}; else`,
35833
+ doneLine(id, true),
35834
+ ...then,
35835
+ "fi"
35836
+ ];
35778
35837
  const vpStep = (step) => {
35779
35838
  const id = vpStepId(step);
35780
35839
  const command = (() => {
@@ -35786,22 +35845,12 @@ const vpStep = (step) => {
35786
35845
  case "globals": return `command vp install -g ${step.packages.map((p) => quote(`${p.name}@${p.version}`)).join(" ")}`;
35787
35846
  }
35788
35847
  })();
35789
- return [
35790
- stepLine(id),
35791
- step.kind === "bootstrap" ? command : `( ${command} )`,
35792
- `ff_code=$?`,
35793
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(id, false, "$ff_code")}; else ${doneLine(id, true)}; fi`
35794
- ];
35848
+ return marked(id, [step.kind === "bootstrap" ? command : `( ${command} )`]);
35795
35849
  };
35796
35850
  const configStep = () => {
35797
35851
  const dir = CONFIG_DIR_SH;
35798
35852
  const write = (variable, file) => `printf '%s' "$${variable}" | base64 -d > ${dir}/${quote(`${file}.tmp`)} && mv -f ${dir}/${quote(`${file}.tmp`)} ${dir}/${quote(file)}`;
35799
- return [
35800
- stepLine(CONFIG_STEP),
35801
- `( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`,
35802
- `ff_code=$?`,
35803
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(CONFIG_STEP, false, "$ff_code")}; else ${doneLine(CONFIG_STEP, true)}; fi`
35804
- ];
35853
+ return marked(CONFIG_STEP, [`( mkdir -p ${dir} && ${write("ff_config", CONFIG_FILE)} && ${write("ff_schema", SCHEMA_FILE)} )`]);
35805
35854
  };
35806
35855
  const writeRecord = (repo) => {
35807
35856
  return [
@@ -35826,34 +35875,32 @@ const repoSteps = (repo, input) => {
35826
35875
  return [
35827
35876
  `ff_d="$ff_root"/${quote(repo.name)}`,
35828
35877
  `ff_before=$(git -C "$ff_d" rev-parse HEAD 2>/dev/null || true)`,
35829
- stepLine(fetch),
35830
- "(",
35831
- " set -e",
35832
- ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
35833
- ` 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`,
35834
- ...sync.map((l) => ` ${l}`),
35835
- ` git -C "$ff_d" clean --quiet -fd`,
35836
- ...writeFiles(repo).map((l) => ` ${l}`),
35837
- ")",
35838
- `ff_code=$?`,
35839
- `if [ "$ff_code" -ne 0 ]; then ${doneLine(fetch, false, "$ff_code")}; else`,
35840
- `${doneLine(fetch, true)}`,
35841
- `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
35842
- `ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${quote(repo.name)})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$ff_after")\\"}"`,
35843
- `if [ ${repo.decision.run ? "true" : "false"} = true ] || [ "$ff_after" != "$ff_before" ]; then`,
35844
- ` ${stepLine(install)}`,
35845
- ` (cd "$ff_d" && ${envPrefix} "\${SHELL:-/bin/sh}" -lc ${quote(repo.install)})`,
35846
- ` ff_code=$?`,
35847
- ` if [ "$ff_code" -ne 0 ]; then ${doneLine(install, false, "$ff_code")}; else`,
35848
- " (",
35849
- " set -e",
35850
- ...writeRecord(repo).map((l) => ` ${l}`),
35851
- " )",
35852
- ` ff_code=$?`,
35853
- ` if [ "$ff_code" -ne 0 ]; then echo "could not write the install record"; ${doneLine(install, false, "$ff_code")}; else ${doneLine(install, true)}; fi`,
35854
- " fi",
35855
- "fi",
35856
- "fi"
35878
+ ...marked(fetch, [
35879
+ "(",
35880
+ " set -e",
35881
+ ` if [ ! -d "$ff_d/.git" ]; then ff_git clone --quiet ${quote(url)} "$ff_d"; fi`,
35882
+ ` if [ "$(git -C "$ff_d" remote get-url origin)" != ${quote(url)} ]; then echo "origin is $(git -C "$ff_d" remote get-url origin), expected ${url}"; exit 1; fi`,
35883
+ ...sync.map((l) => ` ${l}`),
35884
+ ` git -C "$ff_d" clean --quiet -fd`,
35885
+ ...writeFiles(repo).map((l) => ` ${l}`),
35886
+ ")"
35887
+ ], [
35888
+ `ff_after=$(git -C "$ff_d" rev-parse HEAD)`,
35889
+ `ff_state "{\\"fact\\":\\"repo\\",\\"name\\":\\"$(ff_json ${quote(repo.name)})\\",\\"present\\":true,\\"head\\":\\"$(ff_json "$ff_after")\\"}"`,
35890
+ `if [ ${repo.decision.run ? "true" : "false"} = true ] || [ "$ff_after" != "$ff_before" ]; then`,
35891
+ ` ${stepLine(install)}`,
35892
+ ` (cd "$ff_d" && ${envPrefix} "\${SHELL:-/bin/sh}" -lc ${quote(repo.install)})`,
35893
+ ` ff_code=$?`,
35894
+ ` if [ "$ff_code" -ne 0 ]; then ${doneLine(install, false, "$ff_code")}; else`,
35895
+ " (",
35896
+ " set -e",
35897
+ ...writeRecord(repo).map((l) => ` ${l}`),
35898
+ " )",
35899
+ ` ff_code=$?`,
35900
+ ` if [ "$ff_code" -ne 0 ]; then echo "could not write the install record"; ${doneLine(install, false, "$ff_code")}; else ${doneLine(install, true)}; fi`,
35901
+ " fi",
35902
+ "fi"
35903
+ ])
35857
35904
  ];
35858
35905
  };
35859
35906
  const applyStdin = (token, config, schema) => [
@@ -36039,546 +36086,6 @@ const resolveVersions = fn("resolveVersions")(function* (packages) {
36039
36086
  return Object.fromEntries(entries);
36040
36087
  });
36041
36088
  //#endregion
36042
- //#region src/ledger.ts
36043
- const Note = Struct({
36044
- kind: Literals$2([
36045
- "ask",
36046
- "answer",
36047
- "info"
36048
- ]),
36049
- text: String$5
36050
- });
36051
- const Item = Struct({
36052
- name: String$5,
36053
- kind: Literals$2([
36054
- "version",
36055
- "repo",
36056
- "config"
36057
- ]),
36058
- from: optionalKey(String$5),
36059
- to: optionalKey(String$5),
36060
- reason: optionalKey(String$5),
36061
- changes: Boolean$4,
36062
- status: Literals$2([
36063
- "idle",
36064
- "running",
36065
- "ok",
36066
- "failed",
36067
- "skipped"
36068
- ]),
36069
- doing: optionalKey(String$5),
36070
- detail: optionalKey(String$5),
36071
- output: optionalKey(ArraySchema(String$5)),
36072
- steps: ArraySchema(String$5),
36073
- started: optionalKey(Number$2),
36074
- ended: optionalKey(Number$2)
36075
- });
36076
- const MachineEntry = Struct({
36077
- name: String$5,
36078
- status: Literals$2([
36079
- "gathering",
36080
- "gathered",
36081
- "planned",
36082
- "running",
36083
- "ok",
36084
- "skipped",
36085
- "failed"
36086
- ]),
36087
- detail: optionalKey(String$5),
36088
- output: optionalKey(ArraySchema(String$5)),
36089
- items: ArraySchema(Item),
36090
- started: optionalKey(Number$2),
36091
- ended: optionalKey(Number$2)
36092
- });
36093
- const LedgerState = Struct({
36094
- from: String$5,
36095
- dryRun: Boolean$4,
36096
- notes: ArraySchema(Note),
36097
- machines: ArraySchema(MachineEntry),
36098
- started: Number$2,
36099
- ended: optionalKey(Number$2)
36100
- });
36101
- const short = (sha) => sha === void 0 ? void 0 : sha.slice(0, 7);
36102
- const FETCH = ":fetch";
36103
- const doingOf = (id) => id === "config" ? "copying" : id.endsWith(FETCH) ? "fetching" : "installing";
36104
- const itemsOf = (p, state) => {
36105
- const items = /* @__PURE__ */ new Map();
36106
- const key = (kind, name) => `${kind}:${name}`;
36107
- const base = (name, from, kind = "version") => ({
36108
- name,
36109
- kind,
36110
- ...from === void 0 ? {} : { from },
36111
- changes: false,
36112
- status: "idle",
36113
- steps: []
36114
- });
36115
- const set = (name, change, kind = "version") => items.set(key(kind, name), change(items.get(key(kind, name)) ?? base(name, void 0, kind)));
36116
- const plans = (id, to, from, reason) => ({ from: gathered, ...item }) => {
36117
- const now = from === void 0 ? gathered : from ?? void 0;
36118
- return {
36119
- ...item,
36120
- ...now === void 0 ? {} : { from: now },
36121
- to,
36122
- ...reason === void 0 ? {} : { reason },
36123
- changes: true,
36124
- steps: [...item.steps, id]
36125
- };
36126
- };
36127
- const vp = state.vp?.version ?? void 0;
36128
- items.set(key("version", "vp"), base("vp", vp));
36129
- if (state.node != null) items.set(key("version", "node"), base("node", state.node));
36130
- for (const g of state.globals) items.set(key("version", g.name), base(g.name, g.version));
36131
- for (const step of p.vp) {
36132
- const id = vpStepId(step);
36133
- switch (step.kind) {
36134
- case "bootstrap":
36135
- set("vp", plans(id, "latest", null));
36136
- break;
36137
- case "vp":
36138
- case "node":
36139
- set(step.kind, plans(id, step.version, step.from));
36140
- break;
36141
- case "reinstall":
36142
- set(step.name, plans(id, step.version, step.version, "reinstall"));
36143
- break;
36144
- case "globals": for (const pkg of step.packages) set(pkg.name, plans(id, pkg.version, pkg.from ?? null));
36145
- }
36146
- }
36147
- for (const repo of p.repos) {
36148
- const from = short(state.records[repo.name]?.commit);
36149
- const to = short(repo.head);
36150
- items.set(key("repo", repo.name), {
36151
- name: repo.name,
36152
- kind: "repo",
36153
- ...from === void 0 ? {} : { from },
36154
- ...to === void 0 ? {} : { to },
36155
- ...repo.decision.run ? { reason: repo.decision.reason } : {},
36156
- changes: repo.decision.run,
36157
- status: "idle",
36158
- steps: [stepId(repo.name, "fetch"), stepId(repo.name, "install")]
36159
- });
36160
- }
36161
- for (const s of p.skipped) set(s.what, (i) => ({
36162
- ...i,
36163
- status: "skipped",
36164
- detail: s.reason
36165
- }));
36166
- for (const f of p.failures) {
36167
- const kind = f.section === "repos" ? "repo" : "version";
36168
- set(f.what, (i) => ({
36169
- ...i,
36170
- changes: true,
36171
- status: "failed",
36172
- detail: f.reason
36173
- }), kind);
36174
- }
36175
- items.set(key("config", CONFIG_STEP), {
36176
- name: CONFIG_STEP,
36177
- kind: "config",
36178
- changes: false,
36179
- status: "idle",
36180
- steps: [CONFIG_STEP]
36181
- });
36182
- return [...items.values()];
36183
- };
36184
- 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;
36185
- const tally = (state) => {
36186
- const outcomes = state.machines.map((m) => outcomeOf(m, state.dryRun));
36187
- const n = (o) => outcomes.filter((x) => x === o).length;
36188
- return {
36189
- ok: n("ok"),
36190
- skipped: n("skipped"),
36191
- failed: n("failed")
36192
- };
36193
- };
36194
- const makeLedger = fn("makeLedger")(function* (input) {
36195
- const started = yield* currentTimeMillis;
36196
- let snapshot = {
36197
- state: {
36198
- from: input.from,
36199
- dryRun: input.dryRun,
36200
- notes: [],
36201
- machines: input.machines.map((name) => ({
36202
- name,
36203
- status: "gathering",
36204
- items: []
36205
- })),
36206
- started
36207
- },
36208
- now: started
36209
- };
36210
- const listeners = /* @__PURE__ */ new Set();
36211
- const update = (change) => gen(function* () {
36212
- const now = yield* currentTimeMillis;
36213
- snapshot = {
36214
- state: change(snapshot.state, now),
36215
- now
36216
- };
36217
- for (const listener of listeners) listener();
36218
- });
36219
- const machine = (name, change) => update((state, now) => ({
36220
- ...state,
36221
- machines: state.machines.map((m) => m.name === name ? change(m, now) : m)
36222
- }));
36223
- const items = (name, id, change) => machine(name, (m, now) => ({
36224
- ...m,
36225
- items: m.items.map((i) => i.steps.includes(id) ? change(i, now) : i)
36226
- }));
36227
- const store = {
36228
- get: () => snapshot,
36229
- subscribe: (listener) => {
36230
- listeners.add(listener);
36231
- return () => listeners.delete(listener);
36232
- },
36233
- tick: update((state) => state)
36234
- };
36235
- return {
36236
- ledger: {
36237
- note: fn("ledger.note")(function* (kind, text) {
36238
- yield* update((state) => ({
36239
- ...state,
36240
- notes: [...state.notes, {
36241
- kind,
36242
- text
36243
- }]
36244
- }));
36245
- yield* log(noteText({
36246
- kind,
36247
- text
36248
- }, styled()));
36249
- }),
36250
- gathered: (name) => machine(name, (m) => ({
36251
- ...m,
36252
- status: "gathered"
36253
- })),
36254
- skip: (name, reason) => machine(name, (m) => ({
36255
- ...m,
36256
- status: "skipped",
36257
- detail: reason
36258
- })),
36259
- fail: (name, [reason = "failed", ...output]) => machine(name, (m, now) => ({
36260
- ...m,
36261
- status: "failed",
36262
- detail: reason,
36263
- ...output.length === 0 ? {} : { output },
36264
- ended: now
36265
- })),
36266
- plan: (name, p, state) => machine(name, (m) => ({
36267
- ...m,
36268
- status: "planned",
36269
- items: itemsOf(p, state)
36270
- })),
36271
- start: (name) => machine(name, (m, now) => ({
36272
- ...m,
36273
- status: "running",
36274
- started: now
36275
- })),
36276
- stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" ? item : {
36277
- ...item,
36278
- changes: item.changes || !(id === "config" || id.endsWith(FETCH)),
36279
- status: "running",
36280
- doing: doingOf(id),
36281
- started: item.started ?? now
36282
- }),
36283
- stepDone: (name, id, result) => items(name, id, (item, now) => {
36284
- if (item.status === "failed") return item;
36285
- if (!result.ok) return {
36286
- ...item,
36287
- changes: true,
36288
- status: "failed",
36289
- detail: result.detail,
36290
- ...result.output.length === 0 ? {} : { output: result.output },
36291
- ended: now
36292
- };
36293
- const { doing: _, ...rest } = item;
36294
- if (id.endsWith(FETCH)) return item.changes ? item : {
36295
- ...rest,
36296
- status: "idle",
36297
- ended: now
36298
- };
36299
- return {
36300
- ...rest,
36301
- status: "ok",
36302
- ended: now
36303
- };
36304
- }),
36305
- fetched: (name, repo, head) => machine(name, (m) => ({
36306
- ...m,
36307
- items: m.items.map((i) => i.kind === "repo" && i.name === repo ? {
36308
- ...i,
36309
- to: head.slice(0, 7),
36310
- changes: i.changes || head.slice(0, 7) !== i.from
36311
- } : i)
36312
- })),
36313
- finish: (name, outcome) => machine(name, (m, now) => ({
36314
- ...m,
36315
- status: m.status === "failed" ? "failed" : outcome,
36316
- ended: m.ended ?? now
36317
- })),
36318
- end: gen(function* () {
36319
- yield* update((state, now) => ({
36320
- ...state,
36321
- ended: now,
36322
- machines: state.machines.map((m) => m.status === "running" || m.status === "gathering" ? {
36323
- ...m,
36324
- status: "failed",
36325
- detail: "stopped",
36326
- ended: now,
36327
- items: m.items.map((i) => i.status === "running" ? {
36328
- ...i,
36329
- status: "failed",
36330
- detail: "stopped",
36331
- ended: now
36332
- } : i)
36333
- } : m)
36334
- }));
36335
- return snapshot.state;
36336
- })
36337
- },
36338
- store
36339
- };
36340
- });
36341
- const duration = (ms) => {
36342
- const s = ms / 1e3;
36343
- if (s < 60) return `${s.toFixed(1)}s`;
36344
- const m = Math.floor(s / 60);
36345
- if (m < 60) return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`;
36346
- return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
36347
- };
36348
- 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}`;
36349
- const note = (i) => {
36350
- switch (i.status) {
36351
- case "running": return {
36352
- text: i.doing ?? "running",
36353
- style: "run"
36354
- };
36355
- case "failed": return {
36356
- text: "FAIL",
36357
- style: "fail"
36358
- };
36359
- case "skipped": return {
36360
- text: "SKIP",
36361
- style: "skip"
36362
- };
36363
- case "ok":
36364
- if (i.kind === "config") return {
36365
- text: "copied",
36366
- style: "dim"
36367
- };
36368
- if (i.kind === "repo") return i.from !== void 0 && i.from === i.to ? {
36369
- text: `rebuilt${i.reason === void 0 ? "" : `: ${i.reason}`}`,
36370
- style: "ok"
36371
- } : {
36372
- text: "installed",
36373
- style: "ok"
36374
- };
36375
- return {
36376
- text: i.reason ?? "done",
36377
- style: "ok"
36378
- };
36379
- case "idle":
36380
- if (i.changes) return {
36381
- text: i.reason ?? "",
36382
- style: "dim"
36383
- };
36384
- return {
36385
- text: i.kind === "repo" ? "up to date" : "",
36386
- style: "dim"
36387
- };
36388
- }
36389
- };
36390
- const elapsed = (started, ended, now) => started === void 0 ? "" : duration((ended ?? now) - started);
36391
- const LIVE_OUTPUT = 12;
36392
- const outputRows = (output, indent, running) => {
36393
- const cut = running && output.length > LIVE_OUTPUT;
36394
- const shown = cut ? output.slice(-12) : output;
36395
- return [...cut ? [[{
36396
- text: `${indent}(${output.length - LIVE_OUTPUT} earlier lines)`,
36397
- style: "dim"
36398
- }]] : [], ...shown.map((line) => [{ text: `${indent}${line}` }])];
36399
- };
36400
- const pad = (text, width) => text.padEnd(width);
36401
- const rows = (state, options) => {
36402
- const { full, now, live } = options;
36403
- const running = live && state.ended === void 0;
36404
- const shownItems = (m) => m.items.filter((i) => full || i.changes || i.status === "failed" || i.status === "skipped");
36405
- const visible = state.machines.flatMap(shownItems);
36406
- const nameW = Math.max(0, ...state.machines.map((m) => m.name.length));
36407
- const itemW = Math.max(0, ...visible.map((i) => i.name.length));
36408
- const valueW = Math.max(0, ...visible.map((i) => value(i).length));
36409
- const noteW = Math.max(0, ...visible.map((i) => note(i).text.length));
36410
- const out = [[]];
36411
- for (const m of state.machines) {
36412
- const items = shownItems(m);
36413
- const upToDate = !m.items.some((i) => i.changes);
36414
- const head = [{
36415
- text: pad(m.name, nameW),
36416
- style: "bold"
36417
- }, { text: " " }];
36418
- const time = {
36419
- text: elapsed(m.started, m.ended, now),
36420
- style: "dim"
36421
- };
36422
- switch (m.status) {
36423
- case "gathering":
36424
- case "gathered":
36425
- head.push({ text: " " }, {
36426
- text: m.status,
36427
- style: "dim"
36428
- });
36429
- break;
36430
- case "planned":
36431
- if (m.items.some((i) => i.status === "failed")) head.push({
36432
- text: "FAIL",
36433
- style: "fail"
36434
- });
36435
- else head.push({ text: " " }, {
36436
- text: upToDate ? "up to date" : "",
36437
- style: "dim"
36438
- });
36439
- break;
36440
- case "running":
36441
- head.push({
36442
- text: "RUN ",
36443
- style: "run"
36444
- }, { text: " " }, time);
36445
- break;
36446
- case "ok":
36447
- head.push({
36448
- text: "OK ",
36449
- style: "ok"
36450
- }, { text: " " });
36451
- head.push(upToDate ? {
36452
- text: "up to date",
36453
- style: "dim"
36454
- } : time);
36455
- break;
36456
- case "skipped":
36457
- head.push({
36458
- text: "SKIP",
36459
- style: "skip"
36460
- }, { text: " " }, { text: m.detail ?? "" });
36461
- break;
36462
- case "failed":
36463
- head.push({
36464
- text: "FAIL",
36465
- style: "fail"
36466
- }, { text: " " });
36467
- head.push(m.detail === void 0 ? time : { text: m.detail });
36468
- }
36469
- out.push(head);
36470
- for (const i of items) {
36471
- const n = note(i);
36472
- const t = elapsed(i.started, i.ended, now);
36473
- const idle = i.status === "idle" && !i.changes;
36474
- out.push([
36475
- {
36476
- text: ` ${pad(i.name, itemW)} `,
36477
- ...idle ? { style: "dim" } : {}
36478
- },
36479
- {
36480
- text: pad(value(i), valueW),
36481
- ...idle ? { style: "dim" } : {}
36482
- },
36483
- { text: " " },
36484
- {
36485
- text: pad(n.text, noteW),
36486
- ...n.style === void 0 ? {} : { style: n.style }
36487
- },
36488
- {
36489
- text: t === "" ? "" : ` ${t.padStart(6)}`,
36490
- style: "dim"
36491
- },
36492
- ...i.detail === void 0 ? [] : [{ text: ` ${i.detail}` }]
36493
- ]);
36494
- if (i.output !== void 0) out.push(...outputRows(i.output, " ", running));
36495
- }
36496
- if (m.output !== void 0) out.push(...outputRows(m.output, " ", running));
36497
- }
36498
- out.push([]);
36499
- if (state.ended !== void 0) {
36500
- const t = tally(state);
36501
- out.push([
36502
- {
36503
- text: `${state.dryRun ? "planned" : "ok"} ${t.ok}`,
36504
- ...t.failed === 0 ? { style: "ok" } : {}
36505
- },
36506
- { text: ` skipped ${t.skipped} ` },
36507
- {
36508
- text: `failed ${t.failed}`,
36509
- ...t.failed > 0 ? { style: "fail" } : {}
36510
- },
36511
- ...state.dryRun ? [] : [{
36512
- text: ` in ${duration(state.ended - state.started)}`,
36513
- style: "dim"
36514
- }]
36515
- ]);
36516
- }
36517
- if (live) {
36518
- const legend = state.ended === void 0 ? `f ${full ? "changes only" : "full list"} · ctrl-c stop` : state.dryRun ? void 0 : "full report: fizz report";
36519
- if (legend !== void 0) out.push([{
36520
- text: legend,
36521
- style: "dim"
36522
- }]);
36523
- }
36524
- while (out.length > 0 && out[out.length - 1]?.length === 0) out.pop();
36525
- return out;
36526
- };
36527
- const noteRow = (n) => n.kind === "ask" ? [{
36528
- text: "?",
36529
- style: "ask"
36530
- }, { text: ` ${n.text}` }] : n.kind === "answer" ? [{
36531
- text: ` -> ${n.text}`,
36532
- style: "dim"
36533
- }] : [{ text: n.text }];
36534
- const ANSI = {
36535
- dim: "\x1B[2m",
36536
- bold: "\x1B[1m",
36537
- ok: "\x1B[32m",
36538
- fail: "\x1B[1;31m",
36539
- skip: "\x1B[33m",
36540
- run: "\x1B[34m",
36541
- ask: "\x1B[1;35m"
36542
- };
36543
- const styled = () => Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0;
36544
- const rowText = (row, color) => row.map((s) => color && s.style !== void 0 && s.text !== "" ? `${ANSI[s.style]}${s.text}\u001b[0m` : s.text).join("").trimEnd();
36545
- const noteText = (n, color) => rowText(noteRow(n), color);
36546
- const printRows = (list, color = styled()) => log(list.map((row) => rowText(row, color)).join("\n"));
36547
- //#endregion
36548
- //#region src/report.ts
36549
- const reportPath = () => `${STATE_DIR.replace(/^~/, process.env.HOME ?? "~")}/last-ship.json`;
36550
- const LedgerJson = fromJsonString(LedgerState);
36551
- const encode = encodeEffect(LedgerJson);
36552
- const decode = decodeEffect(LedgerJson);
36553
- const saveReport = fn("saveReport")(function* (state) {
36554
- const fs = yield* FileSystem;
36555
- const path = reportPath();
36556
- yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
36557
- yield* fs.writeFileString(path, yield* encode(state));
36558
- });
36559
- const report = fn("report")(function* () {
36560
- const fs = yield* FileSystem;
36561
- const path = reportPath();
36562
- if (!(yield* fs.exists(path))) return yield* log("no Ship from this Machine yet");
36563
- const state = yield* decode(yield* fs.readFileString(path));
36564
- const when = formatLocal(makeUnsafe(state.started), {
36565
- dateStyle: "medium",
36566
- timeStyle: "short"
36567
- });
36568
- yield* printRows([
36569
- [{
36570
- text: `last Ship ${when} from ${state.from}`,
36571
- style: "dim"
36572
- }],
36573
- ...state.notes.map(noteRow),
36574
- ...rows(state, {
36575
- full: true,
36576
- now: state.ended ?? state.started,
36577
- live: false
36578
- })
36579
- ]);
36580
- });
36581
- //#endregion
36582
36089
  //#region ../../node_modules/.pnpm/react@19.3.0/node_modules/react/cjs/react.production.js
36583
36090
  /**
36584
36091
  * @license React
@@ -37981,8 +37488,8 @@ const exitAlternativeScreen = "\x1B[?1049l";
37981
37488
  //#endregion
37982
37489
  //#region ../../node_modules/.pnpm/is-in-ci@2.0.0/node_modules/is-in-ci/index.js
37983
37490
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
37984
- const check = (key) => key in env && env[key] !== "0" && env[key] !== "false";
37985
- const isInCi = check("CI") || check("CONTINUOUS_INTEGRATION");
37491
+ const check$1 = (key) => key in env && env[key] !== "0" && env[key] !== "false";
37492
+ const isInCi = check$1("CI") || check$1("CONTINUOUS_INTEGRATION");
37986
37493
  //#endregion
37987
37494
  //#region ../../node_modules/.pnpm/auto-bind@5.0.1/node_modules/auto-bind/index.js
37988
37495
  const getAllProperties = (object) => {
@@ -64506,6 +64013,79 @@ const getInstance = (stdout, createInstance) => {
64506
64013
  return instance;
64507
64014
  };
64508
64015
  //#endregion
64016
+ //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/render-to-string.js
64017
+ /**
64018
+ Render a React element to a string synchronously. Unlike `render()`, this function does not write to stdout, does not set up any terminal event listeners, and returns the rendered output as a string.
64019
+
64020
+ Useful for generating documentation, writing output to files, testing, or any scenario where you need the rendered output as a string without starting a persistent terminal application.
64021
+
64022
+ **Notes:**
64023
+
64024
+ - Terminal-specific hooks (`useInput`, `useStdin`, `useStdout`, `useStderr`, `useApp`, `useFocus`, `useFocusManager`) return default no-op values since there is no terminal session. They will not throw, but they will not function as in a live terminal.
64025
+ - `useEffect` callbacks will execute during rendering (due to synchronous rendering mode), but state updates they trigger will not affect the returned output, which reflects the initial render.
64026
+ - `useLayoutEffect` callbacks fire synchronously during commit, so state updates they trigger **will** be reflected in the output.
64027
+ - The `<Static>` component is supported — its output is prepended to the dynamic output.
64028
+ - If a component throws during rendering, the error is propagated to the caller after cleanup.
64029
+
64030
+ @example
64031
+ ```
64032
+ import {renderToString, Text, Box} from 'ink';
64033
+
64034
+ const output = renderToString(
64035
+ <Box padding={1}>
64036
+ <Text color="green">Hello World</Text>
64037
+ </Box>,
64038
+ {columns: 40}
64039
+ );
64040
+
64041
+ console.log(output);
64042
+ ```
64043
+ */
64044
+ const renderToString = (node, options) => {
64045
+ const columns = options?.columns ?? 80;
64046
+ const rootNode = createNode("ink-root");
64047
+ let capturedStaticOutput = "";
64048
+ rootNode.onStaticChange = () => {
64049
+ capturedStaticOutput = "";
64050
+ };
64051
+ rootNode.onComputeLayout = () => {
64052
+ rootNode.yogaNode.setWidth(columns);
64053
+ rootNode.yogaNode.calculateLayout(void 0, void 0, Yoga.DIRECTION_LTR);
64054
+ };
64055
+ rootNode.onImmediateRender = () => {
64056
+ const { staticOutput } = renderer(rootNode, false);
64057
+ if (staticOutput !== "") capturedStaticOutput += staticOutput;
64058
+ };
64059
+ let uncaughtError;
64060
+ const container = reconciler_default.createContainer(rootNode, import_constants.LegacyRoot, null, false, null, "render-to-string", (error) => {
64061
+ uncaughtError ??= types.isNativeError(error) ? error : new Error(String(error));
64062
+ }, () => {}, () => {}, () => {}, null);
64063
+ let didTeardownSucceed = false;
64064
+ try {
64065
+ let output;
64066
+ let outputHeight;
64067
+ let staticOutput;
64068
+ try {
64069
+ reconciler_default.updateContainerSync((0, import_react.createElement)(RootNodeContext.Provider, { value: rootNode }, node), container, null, () => {});
64070
+ reconciler_default.flushSyncWork();
64071
+ ({output, outputHeight} = renderer(rootNode, false));
64072
+ staticOutput = capturedStaticOutput;
64073
+ } finally {
64074
+ reconciler_default.updateContainerSync(null, container, null, () => {});
64075
+ reconciler_default.flushSyncWork();
64076
+ didTeardownSucceed = true;
64077
+ rootNode.yogaNode.free();
64078
+ }
64079
+ if (uncaughtError !== void 0) throw uncaughtError;
64080
+ if (outputHeight > 0) return staticOutput + output;
64081
+ return staticOutput.endsWith("\n") ? staticOutput.slice(0, -1) : staticOutput;
64082
+ } finally {
64083
+ if (!didTeardownSucceed && rootNode.yogaNode) try {
64084
+ rootNode.yogaNode.freeRecursive();
64085
+ } catch {}
64086
+ }
64087
+ };
64088
+ //#endregion
64509
64089
  //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/hooks/use-stdin.js
64510
64090
  /**
64511
64091
  A React hook that returns the stdin stream and stdin-related utilities.
@@ -64591,6 +64171,332 @@ const useInput = (inputHandler, options = {}) => {
64591
64171
  }, [options.isActive, internal_eventEmitter]);
64592
64172
  };
64593
64173
  //#endregion
64174
+ //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/hooks/use-stdout.js
64175
+ /**
64176
+ A React hook that returns the stdout stream where Ink renders your app.
64177
+ */
64178
+ function useStdout() {
64179
+ return (0, import_react.use)(StdoutContext);
64180
+ }
64181
+ //#endregion
64182
+ //#region ../../node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.0/node_modules/ink/build/hooks/use-window-size.js
64183
+ /**
64184
+ A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.
64185
+ */
64186
+ const useWindowSize = () => {
64187
+ const { stdout } = useStdout();
64188
+ const [size, setSize] = (0, import_react.useState)(() => getWindowSize(stdout));
64189
+ (0, import_react.useEffect)(() => {
64190
+ const onResize = () => {
64191
+ setSize(getWindowSize(stdout));
64192
+ };
64193
+ stdout.on("resize", onResize);
64194
+ onResize();
64195
+ return () => {
64196
+ stdout.off("resize", onResize);
64197
+ };
64198
+ }, [stdout]);
64199
+ return size;
64200
+ };
64201
+ //#endregion
64202
+ //#region src/ledger.ts
64203
+ const Note = Struct({
64204
+ kind: Literals$2([
64205
+ "ask",
64206
+ "answer",
64207
+ "info"
64208
+ ]),
64209
+ text: String$5
64210
+ });
64211
+ const Item = Struct({
64212
+ name: String$5,
64213
+ kind: Literals$2([
64214
+ "version",
64215
+ "repo",
64216
+ "config"
64217
+ ]),
64218
+ from: optionalKey(String$5),
64219
+ to: optionalKey(String$5),
64220
+ reason: optionalKey(String$5),
64221
+ changes: Boolean$4,
64222
+ status: Literals$2([
64223
+ "idle",
64224
+ "running",
64225
+ "ok",
64226
+ "failed",
64227
+ "skipped"
64228
+ ]),
64229
+ doing: optionalKey(String$5),
64230
+ detail: optionalKey(String$5),
64231
+ output: optionalKey(ArraySchema(String$5)),
64232
+ steps: ArraySchema(String$5),
64233
+ started: optionalKey(Number$2),
64234
+ ended: optionalKey(Number$2)
64235
+ });
64236
+ const MachineEntry = Struct({
64237
+ name: String$5,
64238
+ status: Literals$2([
64239
+ "checking",
64240
+ "waiting",
64241
+ "planned",
64242
+ "running",
64243
+ "ok",
64244
+ "skipped",
64245
+ "failed"
64246
+ ]),
64247
+ detail: optionalKey(String$5),
64248
+ output: optionalKey(ArraySchema(String$5)),
64249
+ items: ArraySchema(Item),
64250
+ started: optionalKey(Number$2),
64251
+ ended: optionalKey(Number$2)
64252
+ });
64253
+ const LedgerState = Struct({
64254
+ from: String$5,
64255
+ dryRun: Boolean$4,
64256
+ notes: ArraySchema(Note),
64257
+ machines: ArraySchema(MachineEntry),
64258
+ started: Number$2,
64259
+ ended: optionalKey(Number$2)
64260
+ });
64261
+ const short = (sha) => sha === void 0 ? void 0 : sha.slice(0, 7);
64262
+ const doingOf = (id) => id === "config" ? "copying" : isFetchStep(id) ? "fetching" : "installing";
64263
+ const itemsOf = (p, state) => {
64264
+ const items = /* @__PURE__ */ new Map();
64265
+ const key = (kind, name) => `${kind}:${name}`;
64266
+ const base = (name, from, kind = "version") => ({
64267
+ name,
64268
+ kind,
64269
+ ...from === void 0 ? {} : { from },
64270
+ changes: false,
64271
+ status: "idle",
64272
+ steps: []
64273
+ });
64274
+ const set = (name, change, kind = "version") => items.set(key(kind, name), change(items.get(key(kind, name)) ?? base(name, void 0, kind)));
64275
+ const plans = (id, to, from, reason) => ({ from: checked, ...item }) => {
64276
+ const now = from === void 0 ? checked : from ?? void 0;
64277
+ return {
64278
+ ...item,
64279
+ ...now === void 0 ? {} : { from: now },
64280
+ to,
64281
+ ...reason === void 0 ? {} : { reason },
64282
+ changes: true,
64283
+ steps: [...item.steps, id]
64284
+ };
64285
+ };
64286
+ const vp = state.vp?.version ?? void 0;
64287
+ items.set(key("version", "vp"), base("vp", vp));
64288
+ if (state.node != null) items.set(key("version", "node"), base("node", state.node));
64289
+ for (const g of state.globals) items.set(key("version", g.name), base(g.name, g.version));
64290
+ for (const step of p.vp) {
64291
+ const id = vpStepId(step);
64292
+ switch (step.kind) {
64293
+ case "bootstrap":
64294
+ set("vp", plans(id, "latest", null));
64295
+ break;
64296
+ case "vp":
64297
+ case "node":
64298
+ set(step.kind, plans(id, step.version, step.from));
64299
+ break;
64300
+ case "reinstall":
64301
+ set(step.name, plans(id, step.version, step.version, "reinstall"));
64302
+ break;
64303
+ case "globals": for (const pkg of step.packages) set(pkg.name, plans(id, pkg.version, pkg.from ?? null));
64304
+ }
64305
+ }
64306
+ for (const repo of p.repos) {
64307
+ const from = short(state.records[repo.name]?.commit);
64308
+ const to = short(repo.head);
64309
+ items.set(key("repo", repo.name), {
64310
+ name: repo.name,
64311
+ kind: "repo",
64312
+ ...from === void 0 ? {} : { from },
64313
+ ...to === void 0 ? {} : { to },
64314
+ ...repo.decision.run ? { reason: repo.decision.reason } : {},
64315
+ changes: repo.decision.run,
64316
+ status: "idle",
64317
+ steps: [stepId(repo.name, "fetch"), stepId(repo.name, "install")]
64318
+ });
64319
+ }
64320
+ for (const s of p.skipped) set(s.what, (i) => ({
64321
+ ...i,
64322
+ status: "skipped",
64323
+ detail: s.reason
64324
+ }));
64325
+ for (const f of p.failures) {
64326
+ const kind = f.section === "repos" ? "repo" : "version";
64327
+ set(f.what, (i) => ({
64328
+ ...i,
64329
+ changes: true,
64330
+ status: "failed",
64331
+ detail: f.reason
64332
+ }), kind);
64333
+ }
64334
+ items.set(key("config", CONFIG_STEP), {
64335
+ name: CONFIG_STEP,
64336
+ kind: "config",
64337
+ changes: false,
64338
+ status: "idle",
64339
+ steps: [CONFIG_STEP]
64340
+ });
64341
+ return [...items.values()];
64342
+ };
64343
+ const outcomeOf = (m, dryRun) => m.status === "skipped" ? "skipped" : m.status === "failed" || m.items.some((i) => i.status === "failed") ? "failed" : m.status === "ok" || m.status === "planned" && dryRun ? "ok" : void 0;
64344
+ const anyFailed = (state) => state.machines.some((m) => outcomeOf(m, state.dryRun) === "failed");
64345
+ const makeLedger = fn("makeLedger")(function* (input) {
64346
+ const started = yield* currentTimeMillis;
64347
+ let snapshot = {
64348
+ state: {
64349
+ from: input.from,
64350
+ dryRun: input.dryRun,
64351
+ notes: [],
64352
+ machines: input.machines.map((name) => ({
64353
+ name,
64354
+ status: "checking",
64355
+ items: []
64356
+ })),
64357
+ started
64358
+ },
64359
+ now: started
64360
+ };
64361
+ const listeners = /* @__PURE__ */ new Set();
64362
+ const update = (change) => gen(function* () {
64363
+ const now = yield* currentTimeMillis;
64364
+ snapshot = {
64365
+ state: change(snapshot.state, now),
64366
+ now
64367
+ };
64368
+ for (const listener of listeners) listener();
64369
+ });
64370
+ const machine = (name, change) => update((state, now) => ({
64371
+ ...state,
64372
+ machines: state.machines.map((m) => m.name === name ? change(m, now) : m)
64373
+ }));
64374
+ const items = (name, id, change) => machine(name, (m, now) => ({
64375
+ ...m,
64376
+ items: m.items.map((i) => i.steps.includes(id) ? change(i, now) : i)
64377
+ }));
64378
+ const store = {
64379
+ get: () => snapshot,
64380
+ subscribe: (listener) => {
64381
+ listeners.add(listener);
64382
+ return () => listeners.delete(listener);
64383
+ },
64384
+ tick: update((state) => state)
64385
+ };
64386
+ return {
64387
+ ledger: {
64388
+ note: fn("ledger.note")(function* (kind, text) {
64389
+ yield* update((state) => ({
64390
+ ...state,
64391
+ notes: [...state.notes, {
64392
+ kind,
64393
+ text
64394
+ }]
64395
+ }));
64396
+ yield* input.print({
64397
+ kind,
64398
+ text
64399
+ });
64400
+ }),
64401
+ checked: (name) => machine(name, (m) => ({
64402
+ ...m,
64403
+ status: "waiting"
64404
+ })),
64405
+ skip: (name, reason) => machine(name, (m) => ({
64406
+ ...m,
64407
+ status: "skipped",
64408
+ detail: reason
64409
+ })),
64410
+ fail: (name, [reason = "failed", ...output]) => machine(name, (m, now) => ({
64411
+ ...m,
64412
+ status: "failed",
64413
+ detail: reason,
64414
+ ...output.length === 0 ? {} : { output },
64415
+ ended: now
64416
+ })),
64417
+ plan: (name, p, state) => machine(name, (m) => ({
64418
+ ...m,
64419
+ status: "planned",
64420
+ items: itemsOf(p, state)
64421
+ })),
64422
+ start: (name) => machine(name, (m, now) => ({
64423
+ ...m,
64424
+ status: "running",
64425
+ started: now
64426
+ })),
64427
+ stepStarted: (name, id) => items(name, id, (item, now) => item.status === "failed" ? item : {
64428
+ ...item,
64429
+ changes: item.changes || !(id === "config" || isFetchStep(id)),
64430
+ status: "running",
64431
+ doing: doingOf(id),
64432
+ started: item.started ?? now
64433
+ }),
64434
+ stepDone: (name, id, result) => items(name, id, (item, now) => {
64435
+ if (item.status === "failed") return item;
64436
+ if (!result.ok) return {
64437
+ ...item,
64438
+ changes: true,
64439
+ status: "failed",
64440
+ detail: result.detail,
64441
+ ...result.output.length === 0 ? {} : { output: result.output },
64442
+ ended: now
64443
+ };
64444
+ const { doing: _, ...rest } = item;
64445
+ if (isFetchStep(id)) return item.changes ? item : {
64446
+ ...rest,
64447
+ status: "idle",
64448
+ ended: now
64449
+ };
64450
+ return {
64451
+ ...rest,
64452
+ status: "ok",
64453
+ ended: now
64454
+ };
64455
+ }),
64456
+ fetched: (name, repo, head) => machine(name, (m) => ({
64457
+ ...m,
64458
+ items: m.items.map((i) => i.kind === "repo" && i.name === repo ? {
64459
+ ...i,
64460
+ to: head.slice(0, 7),
64461
+ changes: i.changes || head.slice(0, 7) !== i.from
64462
+ } : i)
64463
+ })),
64464
+ finish: (name, outcome) => machine(name, (m, now) => ({
64465
+ ...m,
64466
+ status: m.status === "failed" ? "failed" : outcome,
64467
+ ended: m.ended ?? now
64468
+ })),
64469
+ end: gen(function* () {
64470
+ yield* update((state, now) => ({
64471
+ ...state,
64472
+ ended: now,
64473
+ machines: state.machines.map((m) => m.status === "running" || m.status === "checking" ? {
64474
+ ...m,
64475
+ status: "failed",
64476
+ detail: "stopped",
64477
+ ended: now,
64478
+ items: m.items.map((i) => i.status === "running" ? {
64479
+ ...i,
64480
+ status: "failed",
64481
+ detail: "stopped",
64482
+ ended: now
64483
+ } : i)
64484
+ } : m)
64485
+ }));
64486
+ return snapshot.state;
64487
+ })
64488
+ },
64489
+ store
64490
+ };
64491
+ });
64492
+ const duration = (ms) => {
64493
+ const s = ms / 1e3;
64494
+ if (s < 60) return `${s.toFixed(1)}s`;
64495
+ const m = Math.floor(s / 60);
64496
+ if (m < 60) return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`;
64497
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
64498
+ };
64499
+ //#endregion
64594
64500
  //#region ../../node_modules/.pnpm/react@19.3.0/node_modules/react/cjs/react-jsx-runtime.production.js
64595
64501
  /**
64596
64502
  * @license React
@@ -64838,12 +64744,12 @@ var require_react_jsx_runtime_development = /* @__PURE__ */ __commonJSMin(((expo
64838
64744
  })();
64839
64745
  }));
64840
64746
  //#endregion
64841
- //#region src/live.tsx
64747
+ //#region src/view.tsx
64842
64748
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
64843
64749
  if (process.env.NODE_ENV === "production") module.exports = require_react_jsx_runtime_production();
64844
64750
  else module.exports = require_react_jsx_runtime_development();
64845
64751
  })))();
64846
- const STYLE = {
64752
+ const TONE = {
64847
64753
  dim: { dimColor: true },
64848
64754
  bold: { bold: true },
64849
64755
  ok: { color: "green" },
@@ -64858,6 +64764,431 @@ const STYLE = {
64858
64764
  bold: true
64859
64765
  }
64860
64766
  };
64767
+ const ModeContext = (0, import_react.createContext)({
64768
+ color: false,
64769
+ live: false
64770
+ });
64771
+ const T = (props) => {
64772
+ const { color } = (0, import_react.useContext)(ModeContext);
64773
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64774
+ ...color && props.tone !== void 0 ? TONE[props.tone] : {},
64775
+ children: props.children
64776
+ });
64777
+ };
64778
+ const Line = (props) => {
64779
+ const { live } = (0, import_react.useContext)(ModeContext);
64780
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
64781
+ paddingLeft: props.indent ?? 0,
64782
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64783
+ wrap: live ? "truncate-end" : "wrap",
64784
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
64785
+ tone: props.tone,
64786
+ children: props.children
64787
+ })
64788
+ })
64789
+ });
64790
+ };
64791
+ const Cell = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
64792
+ width: props.width,
64793
+ flexShrink: 0,
64794
+ justifyContent: props.right ? "flex-end" : "flex-start",
64795
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64796
+ wrap: "truncate-end",
64797
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
64798
+ tone: props.tone,
64799
+ children: props.children
64800
+ })
64801
+ })
64802
+ });
64803
+ 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}`;
64804
+ const statusOf = (i, dryRun) => {
64805
+ switch (i.status) {
64806
+ case "running": return {
64807
+ text: `▸ ${i.doing ?? "running"}`,
64808
+ tone: "run"
64809
+ };
64810
+ case "failed": return {
64811
+ text: "✗ failed",
64812
+ tone: "fail",
64813
+ detail: i.detail
64814
+ };
64815
+ case "skipped": return {
64816
+ text: "– skipped",
64817
+ tone: "skip",
64818
+ detail: i.detail
64819
+ };
64820
+ case "ok":
64821
+ if (i.kind === "config") return {
64822
+ text: "✓ copied",
64823
+ tone: "dim"
64824
+ };
64825
+ if (i.kind === "repo") return i.from !== void 0 && i.from === i.to ? {
64826
+ text: "✓ rebuilt",
64827
+ tone: "ok",
64828
+ detail: i.reason
64829
+ } : {
64830
+ text: "✓ installed",
64831
+ tone: "ok"
64832
+ };
64833
+ return {
64834
+ text: "✓ done",
64835
+ tone: "ok",
64836
+ detail: i.reason
64837
+ };
64838
+ case "idle":
64839
+ if (i.changes) return {
64840
+ text: dryRun ? "· planned" : "· queued",
64841
+ tone: "dim",
64842
+ detail: i.reason
64843
+ };
64844
+ return {
64845
+ text: "",
64846
+ detail: i.kind === "repo" ? "up to date" : void 0
64847
+ };
64848
+ }
64849
+ };
64850
+ const countsOf = (m) => {
64851
+ const changes = m.items.filter((i) => i.changes && i.status !== "skipped");
64852
+ return {
64853
+ changes: changes.length,
64854
+ done: changes.filter((i) => i.status === "ok").length,
64855
+ failed: m.items.filter((i) => i.status === "failed").length,
64856
+ skipped: m.items.filter((i) => i.status === "skipped").length
64857
+ };
64858
+ };
64859
+ const settled = (m, dryRun) => dryRun || m.status === "ok" || m.status === "failed";
64860
+ const upToDate = (m, dryRun) => {
64861
+ const c = countsOf(m);
64862
+ return c.changes === 0 && c.skipped === 0 && settled(m, dryRun);
64863
+ };
64864
+ const summaryOf = (m, dryRun) => {
64865
+ if (m.status === "checking" || m.status === "waiting") return [{
64866
+ text: m.status,
64867
+ tone: "dim"
64868
+ }];
64869
+ if (m.status === "skipped") return [{
64870
+ text: "– skipped",
64871
+ tone: "skip"
64872
+ }, { text: ` ${m.detail ?? ""}` }];
64873
+ if (m.status === "failed" && m.detail !== void 0) return [{
64874
+ text: "✗ failed",
64875
+ tone: "fail"
64876
+ }, { text: ` ${m.detail}` }];
64877
+ const { changes, done, failed, skipped } = countsOf(m);
64878
+ if (changes === 0 && skipped === 0) return [{
64879
+ text: "up to date",
64880
+ tone: settled(m, dryRun) ? "ok" : "dim"
64881
+ }];
64882
+ const finished = m.status === "ok" || m.status === "failed";
64883
+ const parts = [];
64884
+ if (dryRun && m.status === "planned") {
64885
+ if (changes > failed) parts.push({
64886
+ text: `${changes - failed} to change`,
64887
+ tone: "dim"
64888
+ });
64889
+ } else if (changes > 0 && (!finished || done > 0)) parts.push({
64890
+ text: `${done} of ${changes} done`,
64891
+ tone: done === changes ? "ok" : "dim"
64892
+ });
64893
+ if (failed > 0) parts.push({
64894
+ text: `${failed} failed`,
64895
+ tone: "fail"
64896
+ });
64897
+ if (skipped > 0) parts.push({
64898
+ text: `${skipped} skipped`,
64899
+ tone: "skip"
64900
+ });
64901
+ return joined(parts);
64902
+ };
64903
+ const joined = (parts) => parts.flatMap((part, n) => n === 0 ? [part] : [{
64904
+ text: ", ",
64905
+ tone: "dim"
64906
+ }, part]);
64907
+ const shipSummary = (state) => {
64908
+ const outcomes = state.machines.map((m) => {
64909
+ const outcome = outcomeOf(m, state.dryRun);
64910
+ if (outcome !== "ok") return outcome;
64911
+ const { changes, skipped } = countsOf(m);
64912
+ if (changes > 0) return state.dryRun ? "to change" : "updated";
64913
+ return skipped > 0 ? "skipped" : "up to date";
64914
+ });
64915
+ const counted = (outcome, tone) => {
64916
+ const n = outcomes.filter((o) => o === outcome).length;
64917
+ return n === 0 ? [] : [{
64918
+ text: `${n} ${outcome}`,
64919
+ tone
64920
+ }];
64921
+ };
64922
+ const parts = joined([
64923
+ ...counted("updated", "ok"),
64924
+ ...counted("to change", "dim"),
64925
+ ...counted("up to date", "ok"),
64926
+ ...counted("failed", "fail"),
64927
+ ...counted("skipped", "skip")
64928
+ ]);
64929
+ return parts.length === 0 ? [{
64930
+ text: "nothing changed",
64931
+ tone: "dim"
64932
+ }] : parts;
64933
+ };
64934
+ const elapsed = (started, ended, now) => started === void 0 ? "" : duration((ended ?? now) - started);
64935
+ const Parts = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, { children: props.parts.map((part, n) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
64936
+ tone: part.tone,
64937
+ children: part.text
64938
+ }, n)) });
64939
+ const GAP = 2;
64940
+ const INDENT = 2;
64941
+ const nameAt = (w) => w.status + GAP + INDENT;
64942
+ const LIVE_OUTPUT = 12;
64943
+ const outputLines = (key, output, indent, cut) => {
64944
+ const hidden = cut ? Math.max(0, output.length - LIVE_OUTPUT) : 0;
64945
+ return [...hidden > 0 ? [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Line, {
64946
+ indent,
64947
+ tone: "dim",
64948
+ children: [
64949
+ "(",
64950
+ hidden,
64951
+ " earlier lines)"
64952
+ ]
64953
+ }, `${key}/earlier`)] : [], ...output.slice(hidden).map((line, n) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Line, {
64954
+ indent,
64955
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
64956
+ tone: "fail",
64957
+ children: "│ "
64958
+ }), line]
64959
+ }, `${key}/${n}`))];
64960
+ };
64961
+ const MachineLine = (props) => {
64962
+ const { m, w } = props;
64963
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
64964
+ columnGap: GAP,
64965
+ children: [
64966
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
64967
+ width: nameAt(w) + w.item + GAP + w.value,
64968
+ tone: "bold",
64969
+ children: m.name
64970
+ }),
64971
+ w.time > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
64972
+ width: w.time,
64973
+ right: true,
64974
+ tone: "dim",
64975
+ children: upToDate(m, props.dryRun) ? "" : elapsed(m.started, m.ended, props.now)
64976
+ }),
64977
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: summaryOf(m, props.dryRun) })
64978
+ ]
64979
+ });
64980
+ };
64981
+ const ItemLine = (props) => {
64982
+ const { i, w } = props;
64983
+ const status = statusOf(i, props.dryRun);
64984
+ const quiet = i.status === "idle" ? "dim" : void 0;
64985
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
64986
+ columnGap: GAP,
64987
+ children: [
64988
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
64989
+ width: w.status,
64990
+ tone: status.tone,
64991
+ children: status.text
64992
+ }),
64993
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
64994
+ width: INDENT + w.item,
64995
+ tone: quiet,
64996
+ children: `${" ".repeat(INDENT)}${i.name}`
64997
+ }),
64998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
64999
+ width: w.value,
65000
+ tone: quiet,
65001
+ children: value(i)
65002
+ }),
65003
+ w.time > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, {
65004
+ width: w.time,
65005
+ right: true,
65006
+ tone: "dim",
65007
+ children: elapsed(i.started, i.ended, props.now)
65008
+ }),
65009
+ status.detail !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
65010
+ tone: quiet,
65011
+ children: status.detail
65012
+ })
65013
+ ]
65014
+ });
65015
+ };
65016
+ const LedgerView = (props) => {
65017
+ const { state, full, now, height } = props;
65018
+ const { dryRun } = state;
65019
+ const { live } = (0, import_react.useContext)(ModeContext);
65020
+ const running = live && state.ended === void 0;
65021
+ const shownItems = (m) => m.items.filter((i) => full || i.changes || i.status === "failed" || i.status === "skipped");
65022
+ const visible = state.machines.flatMap(shownItems);
65023
+ const width = (texts) => Math.max(0, ...texts.map((t) => t.length));
65024
+ const item = width(visible.map((i) => i.name));
65025
+ const status = width(visible.map((i) => statusOf(i, dryRun).text));
65026
+ const w = {
65027
+ status,
65028
+ item,
65029
+ value: Math.max(width(visible.map(value)), width(state.machines.map((m) => m.name)) - (status + GAP + INDENT + item + GAP)),
65030
+ time: width([...visible.map((i) => elapsed(i.started, i.ended, now)), ...state.machines.map((m) => elapsed(m.started, m.ended, now))])
65031
+ };
65032
+ const blocks = state.machines.map((m) => ({
65033
+ m,
65034
+ head: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MachineLine, {
65035
+ m,
65036
+ w,
65037
+ dryRun,
65038
+ now
65039
+ }, m.name),
65040
+ body: [...m.output === void 0 ? [] : outputLines(m.name, m.output, nameAt(w), running), ...shownItems(m).flatMap((i) => [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ItemLine, {
65041
+ i,
65042
+ w,
65043
+ dryRun,
65044
+ now
65045
+ }, `${m.name}/${i.kind}:${i.name}`), ...i.output === void 0 ? [] : outputLines(`${m.name}/${i.kind}:${i.name}`, i.output, nameAt(w), running)])]
65046
+ }));
65047
+ const legend = state.ended === void 0 ? `f ${full ? "changes only" : "full list"} · ctrl-c stop` : void 0;
65048
+ const room = height === void 0 ? Infinity : height - (legend === void 0 ? 1 : 3);
65049
+ const count = (bs) => bs.reduce((n, b) => n + 1 + b.body.length, Math.max(0, bs.length - 1));
65050
+ const fits = count(blocks) <= room;
65051
+ let spare = room - count(blocks.map((b) => ({
65052
+ ...b,
65053
+ body: []
65054
+ })));
65055
+ let cuts = 0;
65056
+ const fitted = [];
65057
+ for (const b of blocks) {
65058
+ const open = b.m.status === "running";
65059
+ if (fits) fitted.push(b);
65060
+ else if (open && b.body.length <= spare) {
65061
+ fitted.push(b);
65062
+ spare -= b.body.length;
65063
+ } else if (open && spare > 0) {
65064
+ const ellipsis = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
65065
+ indent: nameAt(w),
65066
+ tone: "dim",
65067
+ children: "…"
65068
+ }, `${b.m.name}/cut`);
65069
+ fitted.push({
65070
+ ...b,
65071
+ body: [...b.body.slice(0, spare - 1), ellipsis]
65072
+ });
65073
+ spare = 0;
65074
+ cuts += 1;
65075
+ } else fitted.push({
65076
+ ...b,
65077
+ body: []
65078
+ });
65079
+ }
65080
+ const lines = fitted.flatMap((b, n) => [
65081
+ ...n === 0 ? [] : [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }, `${b.m.name}/gap`)],
65082
+ b.head,
65083
+ ...b.body
65084
+ ]);
65085
+ const shown = lines.length <= room ? lines : lines.slice(0, Math.max(0, room - 1));
65086
+ const hidden = count(blocks) - (shown.length - cuts);
65087
+ if (room < 1) return null;
65088
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
65089
+ flexDirection: "column",
65090
+ marginTop: 1,
65091
+ children: [
65092
+ shown,
65093
+ shown.length < lines.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
65094
+ tone: "dim",
65095
+ children: "…"
65096
+ }),
65097
+ state.ended !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
65098
+ marginTop: 1,
65099
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Parts, { parts: [...shipSummary(state), ...dryRun ? [] : [{
65100
+ text: ` in ${duration(state.ended - state.started)}`,
65101
+ tone: "dim"
65102
+ }]] })
65103
+ }),
65104
+ state.ended !== void 0 && props.hint !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
65105
+ tone: "dim",
65106
+ children: props.hint
65107
+ }),
65108
+ legend !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
65109
+ marginTop: 1,
65110
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Line, {
65111
+ tone: "dim",
65112
+ children: [legend, hidden > 0 && ` · ${hidden} lines hidden to fit the window`]
65113
+ })
65114
+ })
65115
+ ]
65116
+ });
65117
+ };
65118
+ const NoteLine = (props) => {
65119
+ const { kind, text } = props.note;
65120
+ return kind === "ask" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
65121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
65122
+ tone: "ask",
65123
+ children: "?"
65124
+ }),
65125
+ " ",
65126
+ text
65127
+ ] }) : kind === "answer" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
65128
+ tone: "dim",
65129
+ children: ` → ${text}`
65130
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: text });
65131
+ };
65132
+ const Styled = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModeContext.Provider, {
65133
+ value: {
65134
+ color: props.color,
65135
+ live: props.live
65136
+ },
65137
+ children: props.children
65138
+ });
65139
+ const colorful = () => Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0;
65140
+ const columns = () => process.stdout.columns ?? 1e4;
65141
+ const toText = (node, color = colorful()) => renderToString(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Styled, {
65142
+ color,
65143
+ live: false,
65144
+ children: node
65145
+ }), { columns: columns() });
65146
+ const ledgerText = (state, options, color) => toText(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LedgerView, {
65147
+ state,
65148
+ ...options
65149
+ }), color);
65150
+ const noteText = (note, color) => toText(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoteLine, { note }), color);
65151
+ //#endregion
65152
+ //#region src/report.tsx
65153
+ const reportPath = () => `${STATE_DIR.replace(/^~/, process.env.HOME ?? "~")}/last-ship.json`;
65154
+ const LedgerJson = fromJsonString(LedgerState);
65155
+ const encode = encodeEffect(LedgerJson);
65156
+ const decode = decodeEffect(LedgerJson);
65157
+ const saveReport = fn("saveReport")(function* (state) {
65158
+ const fs = yield* FileSystem;
65159
+ const path = reportPath();
65160
+ yield* fs.makeDirectory(path.slice(0, path.lastIndexOf("/")), { recursive: true });
65161
+ yield* fs.writeFileString(path, yield* encode(state));
65162
+ });
65163
+ const report = fn("report")(function* () {
65164
+ const fs = yield* FileSystem;
65165
+ const path = reportPath();
65166
+ if (!(yield* fs.exists(path))) return yield* log("no Ship from this Machine yet");
65167
+ const decoded = yield* decode(yield* fs.readFileString(path)).pipe(option);
65168
+ if (isNone(decoded)) return yield* log(`the last report (${path}) is from another version of fleetfizz; the next Ship replaces it`);
65169
+ const state = decoded.value;
65170
+ const when = formatLocal(makeUnsafe(state.started), {
65171
+ dateStyle: "medium",
65172
+ timeStyle: "short"
65173
+ });
65174
+ yield* log(toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
65175
+ flexDirection: "column",
65176
+ children: [
65177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
65178
+ tone: "dim",
65179
+ children: `last Ship ${when} from ${state.from}`
65180
+ }),
65181
+ state.notes.map((note, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoteLine, { note }, i)),
65182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LedgerView, {
65183
+ state,
65184
+ full: true,
65185
+ now: state.ended ?? state.started
65186
+ })
65187
+ ]
65188
+ })));
65189
+ });
65190
+ //#endregion
65191
+ //#region src/live.tsx
64861
65192
  const cell = (initial) => {
64862
65193
  let value = initial;
64863
65194
  const listeners = /* @__PURE__ */ new Set();
@@ -64878,6 +65209,7 @@ const LiveLedger = (props) => {
64878
65209
  const asking = (0, import_react.useSyncExternalStore)(props.asking.subscribe, props.asking.get);
64879
65210
  const [full, setFull] = (0, import_react.useState)(false);
64880
65211
  const [cursor, setCursor] = (0, import_react.useState)(0);
65212
+ const { rows } = useWindowSize();
64881
65213
  const { isRawModeSupported } = useStdin();
64882
65214
  useInput((input, key) => {
64883
65215
  if (key.ctrl && input === "c") process.kill(process.pid, "SIGINT");
@@ -64889,42 +65221,36 @@ const LiveLedger = (props) => {
64889
65221
  asking.answer(cursor);
64890
65222
  } else if (input === "f") setFull((f) => !f);
64891
65223
  }, { isActive: isRawModeSupported });
64892
- const style = (s) => props.color && s !== void 0 ? STYLE[s] : {};
64893
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
64894
- flexDirection: "column",
64895
- children: [asking?.titles.map((title, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
64896
- wrap: "truncate-end",
64897
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64898
- ...style(i === cursor ? "ask" : "dim"),
64899
- children: i === cursor ? " › " : " "
64900
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64901
- ...style(i === cursor ? void 0 : "dim"),
64902
- children: title
65224
+ const choices = asking?.titles ?? [];
65225
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Styled, {
65226
+ color: props.color,
65227
+ live: true,
65228
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
65229
+ flexDirection: "column",
65230
+ children: [choices.map((title, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Line, {
65231
+ tone: i === cursor ? void 0 : "dim",
65232
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
65233
+ tone: i === cursor ? "ask" : "dim",
65234
+ children: i === cursor ? " › " : " "
65235
+ }), title]
65236
+ }, i)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LedgerView, {
65237
+ state,
65238
+ full,
65239
+ now,
65240
+ height: rows - 1 - choices.length
64903
65241
  })]
64904
- }, `ask-${i}`)), rows(state, {
64905
- full,
64906
- now,
64907
- live: true
64908
- }).map((row, r) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64909
- wrap: "truncate-end",
64910
- children: row.length === 0 ? " " : row.map((segment, s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
64911
- ...style(segment.style),
64912
- children: segment.text
64913
- }, s))
64914
- }, r))]
65242
+ })
64915
65243
  });
64916
65244
  };
64917
65245
  const startLive = fn("startLive")(function* (store, color) {
64918
65246
  const asking = cell(void 0);
64919
- const view = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LiveLedger, {
65247
+ const instance = render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LiveLedger, {
64920
65248
  store,
64921
65249
  asking,
64922
65250
  color
64923
- });
64924
- const instance = render(view, { exitOnCtrlC: false });
65251
+ }), { exitOnCtrlC: false });
64925
65252
  yield* addFinalizer(() => promise(async () => {
64926
- instance.rerender(view);
64927
- await instance.waitUntilRenderFlush();
65253
+ instance.clear();
64928
65254
  instance.unmount();
64929
65255
  await instance.waitUntilExit();
64930
65256
  }));
@@ -64962,25 +65288,25 @@ const runOn = fn("runOn")(function* (target, script, secrets, onLine, ledger) {
64962
65288
  }
64963
65289
  return run;
64964
65290
  });
64965
- const GATHER_TIMEOUT = "2 minutes";
64966
- const gather = fn("gather")(function* (target, script, ledger) {
65291
+ const CHECK_TIMEOUT = "2 minutes";
65292
+ const check = fn("check")(function* (target, script, ledger) {
64967
65293
  const facts = [];
64968
65294
  const output = [];
64969
65295
  const run = yield* runOn(target, script, [], (line) => sync(() => {
64970
65296
  if (line.kind === "state") facts.push(line.fact);
64971
65297
  else if (line.kind === "text" && line.text.trim() !== "") output.push(line.text.trimEnd());
64972
65298
  }), ledger).pipe(timeoutOrElse({
64973
- duration: GATHER_TIMEOUT,
64974
- orElse: () => ledger.fail(target.name, [`gather took more than ${GATHER_TIMEOUT}; something on that Machine is blocking`, ...output]).pipe(as({ kind: "failed" }))
65299
+ duration: CHECK_TIMEOUT,
65300
+ orElse: () => ledger.fail(target.name, [`checking took more than ${CHECK_TIMEOUT}; something on that Machine is blocking`, ...output]).pipe(as({ kind: "failed" }))
64975
65301
  }));
64976
65302
  if (run.kind !== "ran") return { kind: run.kind };
64977
65303
  const state = assembleState(facts);
64978
65304
  if (!state.complete) {
64979
- yield* ledger.fail(target.name, ["gather did not finish", ...output]);
65305
+ yield* ledger.fail(target.name, ["checking did not finish", ...output]);
64980
65306
  return { kind: "failed" };
64981
65307
  }
64982
65308
  return {
64983
- kind: "gathered",
65309
+ kind: "checked",
64984
65310
  state
64985
65311
  };
64986
65312
  });
@@ -65064,7 +65390,7 @@ const askQuestions = fn("askQuestions")(function* (questions, flags, ledger, ask
65064
65390
  value: bump
65065
65391
  },
65066
65392
  {
65067
- title: "skip on these Machines (leave as is, report SKIP)",
65393
+ title: "skip on these Machines (leave as is, shown as skipped)",
65068
65394
  value: { kind: "skip" }
65069
65395
  },
65070
65396
  {
@@ -65102,19 +65428,18 @@ const packagesToResolve = (config) => {
65102
65428
  ];
65103
65429
  };
65104
65430
  const rebuildOf = (flags) => flags.rebuild.length === 0 ? "none" : flags.rebuild.includes("all") ? "all" : flags.rebuild;
65105
- const staleConfigs = fn("staleConfigs")(function* (local, inputIsLocalFile, gathered, ledger) {
65106
- const stale = [];
65107
- for (const { machine, local: isLocal, state } of gathered) {
65108
- if (isLocal && inputIsLocalFile || state.config === void 0) continue;
65431
+ const staleConfigs = fn("staleConfigs")(function* (config, own, checked, ledger) {
65432
+ const copies = [];
65433
+ for (const { machine, state } of checked) {
65434
+ if (state.config === void 0) continue;
65109
65435
  const parsed = yield* result(parseConfig(machine.name, state.config));
65110
- if (parsed._tag === "Failure") {
65111
- yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
65112
- continue;
65113
- }
65114
- const remote = parsed.success;
65115
- if (isNewer(remote.shippedTo?.[machine.name], local.shippedTo?.[machine.name])) stale.push(`${machine.name} (shipped ${remote.shippedAt} from ${remote.shippedFrom ?? "unknown"})`);
65436
+ if (parsed._tag === "Failure") yield* ledger.note("info", `the config on ${machine.name} does not parse; it will be overwritten`);
65437
+ else copies.push({
65438
+ machine: machine.name,
65439
+ config: parsed.success
65440
+ });
65116
65441
  }
65117
- return stale;
65442
+ return staleCopies(config, copies, own).map((c) => `${c.machine} (shipped ${c.config.shippedAt} from ${c.config.shippedFrom ?? "unknown"})`);
65118
65443
  });
65119
65444
  const schemaJson = encodeSync(fromJsonString(Unknown, { space: 2 }));
65120
65445
  const ship = fn("ship")(function* (flags) {
@@ -65125,6 +65450,7 @@ const ship = fn("ship")(function* (flags) {
65125
65450
  const unknown = unknownMachines(config, tailnet, flags.only);
65126
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(", ")})`));
65127
65452
  const fleet = new Set(Object.keys(config.machines));
65453
+ const own = flags.config === void 0 ? tailnet.self.name : void 0;
65128
65454
  if (!fleet.has(tailnet.self.name)) return yield* fail$1(new ShipError(`this Machine (${tailnet.self.name}) is not in machines in ${path}`));
65129
65455
  const outsideFleet = flags.only.filter((name) => !fleet.has(name));
65130
65456
  if (outsideFleet.length > 0) return yield* fail$1(new ShipError(`not in machines in ${path}: ${outsideFleet.join(", ")}`));
@@ -65132,30 +65458,36 @@ const ship = fn("ship")(function* (flags) {
65132
65458
  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));
65133
65459
  const count = `${targetsList.length} machine${targetsList.length === 1 ? "" : "s"}`;
65134
65460
  yield* log(`${flags.dryRun ? "dry run, " : ""}${count} from ${tailnet.self.name}`);
65461
+ const color = colorful();
65135
65462
  const { ledger, store } = yield* makeLedger({
65136
65463
  from: tailnet.self.name,
65137
65464
  dryRun: flags.dryRun,
65138
- machines: targetsList.map((m) => m.name)
65465
+ machines: targetsList.map((m) => m.name),
65466
+ print: (note) => log(noteText(note, color))
65139
65467
  });
65140
- const live = canRedraw() ? yield* startLive(store, styled()) : noLive;
65468
+ const redraw = canRedraw();
65141
65469
  let changing = false;
65142
65470
  const finish = fn("finish")(function* () {
65143
65471
  const state = yield* ledger.end;
65144
- if (changing) yield* saveReport(state).pipe(catch_((e) => error(`could not save the report for fizz report: ${e.message}`)));
65145
- if (live === noLive) yield* printRows(rows(state, {
65146
- full: true,
65472
+ const saved = changing && (yield* saveReport(state).pipe(as(true), catch_((e) => error(`could not save the report for fizz report: ${e.message}`).pipe(as(false)))));
65473
+ yield* log(ledgerText(state, redraw ? {
65474
+ full: false,
65147
65475
  now: state.ended ?? 0,
65148
- live: false
65149
- }));
65150
- if (tally(state).failed > 0) process.exitCode = 1;
65476
+ ...saved ? { hint: "full report: fizz report" } : {}
65477
+ } : {
65478
+ full: true,
65479
+ now: state.ended ?? 0
65480
+ }, color));
65481
+ if (anyFailed(state)) process.exitCode = 1;
65151
65482
  });
65152
65483
  yield* addFinalizer(() => finish());
65484
+ const live = redraw ? yield* startLive(store, color) : noLive;
65153
65485
  const cloneRootOf = (machine) => config.machines[machine.name]?.clones ?? "~/.local/share/fleetfizz/clones";
65154
- const gatherRepos = (config.repos ?? []).map((r) => ({
65486
+ const checkRepos = (config.repos ?? []).map((r) => ({
65155
65487
  name: repoName(r),
65156
65488
  outputs: r.outputs ?? []
65157
65489
  }));
65158
- const gathered = yield* forEach$1(targetsList, fn("gatherOne")(function* (machine) {
65490
+ const checked = yield* forEach$1(targetsList, fn("checkOne")(function* (machine) {
65159
65491
  const local = machine.name === tailnet.self.name;
65160
65492
  if (!machine.online) {
65161
65493
  yield* ledger.skip(machine.name, "offline");
@@ -65165,23 +65497,23 @@ const ship = fn("ship")(function* (flags) {
65165
65497
  yield* ledger.skip(machine.name, "no Tailscale SSH server");
65166
65498
  return;
65167
65499
  }
65168
- const script = gatherScript({
65500
+ const script = checkScript({
65169
65501
  cloneRoot: cloneRootOf(machine),
65170
- repos: gatherRepos
65502
+ repos: checkRepos
65171
65503
  });
65172
- const result = yield* gather({
65504
+ const result = yield* check({
65173
65505
  name: machine.name,
65174
65506
  local
65175
65507
  }, script, ledger);
65176
- if (result.kind !== "gathered") return void 0;
65177
- yield* ledger.gathered(machine.name);
65508
+ if (result.kind !== "checked") return void 0;
65509
+ yield* ledger.checked(machine.name);
65178
65510
  return {
65179
65511
  machine,
65180
65512
  local,
65181
65513
  state: result.state
65182
65514
  };
65183
65515
  }), { concurrency: "unbounded" }).pipe(map$5((list) => list.filter((g) => g !== void 0)));
65184
- const stale = yield* staleConfigs(config, flags.config === void 0, gathered, ledger);
65516
+ const stale = yield* staleConfigs(config, own, checked, ledger);
65185
65517
  if (stale.length > 0) {
65186
65518
  const what = `newer config on ${stale.join(", ")}`;
65187
65519
  if (flags.dryRun) yield* ledger.note("info", `${what}; a Ship would ask before overwriting it`);
@@ -65201,7 +65533,7 @@ const ship = fn("ship")(function* (flags) {
65201
65533
  }
65202
65534
  }
65203
65535
  const versions = yield* resolveVersions(packagesToResolve(config));
65204
- const planAll = (cfg, answers) => gathered.map((g) => ({
65536
+ const planAll = (cfg, answers) => checked.map((g) => ({
65205
65537
  ...g,
65206
65538
  plan: plan({
65207
65539
  config: cfg,
@@ -65242,7 +65574,8 @@ const ship = fn("ship")(function* (flags) {
65242
65574
  const stampWith = (reached) => stampText(original, {
65243
65575
  shippedAt,
65244
65576
  shippedFrom: tailnet.self.name,
65245
- reached: [...reached, ...flags.config === void 0 ? [tailnet.self.name] : []]
65577
+ reached,
65578
+ own
65246
65579
  });
65247
65580
  const stamped = stampWith(planned.map((g) => g.machine.name));
65248
65581
  yield* fs.writeFileString(path, stamped);
@@ -65271,7 +65604,7 @@ const ship = fn("ship")(function* (flags) {
65271
65604
  //#endregion
65272
65605
  //#region src/bin.ts
65273
65606
  const shipCommand = make("ship", {
65274
- dryRun: Boolean$1("dry-run").pipe(withDefault(false), withDescription$1("Gather and plan only. Print every Machine's plan and change nothing.")),
65607
+ dryRun: Boolean$1("dry-run").pipe(withDefault(false), withDescription$1("Check and plan only. Print every Machine's plan and change nothing.")),
65275
65608
  only: String$1("only").pipe(atLeast(0), withDescription$1("Ship only to these Machines (repeat the flag).")),
65276
65609
  rebuild: String$1("rebuild").pipe(atLeast(0), withDescription$1("Run a repo's Install Command even when its record matches; `all` for every repo.")),
65277
65610
  bump: Boolean$1("bump").pipe(withDefault(false), withDescription$1("Answer every above-range question by bumping the config.")),