@bli-cockpit/cli 0.2.265 → 0.2.266

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.
@@ -126,7 +126,8 @@ export async function readRegisteredDarwinRuntimePaths(homeDir) {
126
126
  const command = decodeXmlEntities(plist);
127
127
  return {
128
128
  node_executable: /node_bin='([^']*)'/u.exec(command)?.[1] ?? null,
129
- cli_entry_point: /"\$node_bin" '([^']*)' sync /u.exec(command)?.[1] ?? null,
129
+ cli_entry_point: /"\$node_bin"(?: --optimize-for-size)? '([^']*)' sync /u.exec(command)?.[1] ?? null,
130
+ optimize_for_size: /"\$node_bin" '[^']*' sync /u.test(command) ? false : /"\$node_bin" --optimize-for-size '[^']*' sync /u.test(command) ? true : null,
130
131
  };
131
132
  }
132
133
  /**
@@ -222,12 +223,15 @@ function watchPathsBlock(watchPaths) {
222
223
  " </array>",
223
224
  ];
224
225
  }
226
+ /** The V8 flag the scheduled sync runs node with (BLI-23317). */
227
+ export const NODE_OPTIMIZE_FOR_SIZE_FLAG = "--optimize-for-size";
225
228
  function renderDarwinSyncCommand(options) {
229
+ const nodeFlag = options.optimizeForSize === false ? "" : ` ${NODE_OPTIMIZE_FOR_SIZE_FLAG}`;
226
230
  const dashboardArg = isDefaultDashboardUrl(options.dashboardUrl)
227
231
  ? ""
228
232
  : ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
229
233
  const discoveryArg = options.discoveryArgs.length > 0 ? ` ${options.discoveryArgs.join(" ")}` : "";
230
- const commands = options.workDirs.map((root) => `"$node_bin" ${shellQuote(options.cliEntryPoint)} sync --workspace ${shellQuote(root)}${dashboardArg}${discoveryArg} --json || exit_code=1`);
234
+ const commands = options.workDirs.map((root) => `"$node_bin"${nodeFlag} ${shellQuote(options.cliEntryPoint)} sync --workspace ${shellQuote(root)}${dashboardArg}${discoveryArg} --json || exit_code=1`);
231
235
  return [
232
236
  "exit_code=0",
233
237
  ...darwinNodeResolutionPreamble(options.nodeExecutable),
@@ -278,14 +282,16 @@ function darwinAgentRegistrationProblems(plist, expected) {
278
282
  if (!plist.includes(` <key>ThrottleInterval</key>\n <integer>${DARWIN_MIN_SECONDS_BETWEEN_TICKS}</integer>`)) {
279
283
  problems.push(`minimum gap between ticks is not ${DARWIN_MIN_SECONDS_BETWEEN_TICKS} seconds`);
280
284
  }
281
- const expectedCommand = renderDarwinSyncCommand({
285
+ // BLI-23317: the form with --optimize-for-size is what is written; the form without it is still accepted.
286
+ const matchesCurrentRuntime = [true, false].some((optimizeForSize) => plist.includes(programArgumentsBlock(renderDarwinSyncCommand({
282
287
  discoveryArgs: expected.discoveryArgs,
283
288
  workDirs: expected.workDirs,
284
289
  dashboardUrl: expected.dashboardUrl,
285
290
  nodeExecutable: expected.nodeExecutable,
286
291
  cliEntryPoint: expected.cliEntryPoint,
287
- });
288
- if (!plist.includes(programArgumentsBlock(expectedCommand).join("\n"))) {
292
+ optimizeForSize,
293
+ })).join("\n")));
294
+ if (!matchesCurrentRuntime) {
289
295
  problems.push("command does not match the current roots, dashboard URL, or Tower runtime");
290
296
  }
291
297
  return problems;
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
- import { autostartStatus, installAutostartAgent, registeredRuntimePathProblems, } from "./autostart.js";
4
+ import { autostartStatus, installAutostartAgent, registeredLauncherUpgradeProblems, registeredRuntimePathProblems, } from "./autostart.js";
5
5
  import { detectSecondCockpitInstall, secondInstallReason, } from "./second-install.js";
6
6
  import { isAutostartOptedOut } from "./autostart-opt-out.js";
7
7
  export const AUTOSTART_REPAIR_THROTTLE_MARKER = ".last-autostart-repair";
@@ -52,6 +52,14 @@ async function readAutostartProblem(paths, options, platform) {
52
52
  platform,
53
53
  })
54
54
  : [];
55
+ // BLI-23317: an install that still runs node without --optimize-for-size works, so it is not "broken";
56
+ // it is rewritten once here, on the normal cadence, and logged as its own reason.
57
+ const upgradeProblems = status.status === "loaded"
58
+ ? await registeredLauncherUpgradeProblems({ homeDir: options.homeDir, exec: options.exec, platform })
59
+ : [];
60
+ if (upgradeProblems.length > 0) {
61
+ console.error("[autostart-self-heal] registration runs node without --optimize-for-size; rewriting it", JSON.stringify({ reason: "registration_predates_optimize_for_size", platform }));
62
+ }
55
63
  const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
56
64
  // BLI-21564: the reminder job is repaired with the sync agent. A missing or broken reminder job is a
57
65
  // problem of its own (an existing install that predates it gets it created here), and the opt-out
@@ -59,7 +67,7 @@ async function readAutostartProblem(paths, options, platform) {
59
67
  const remindProblem = status.status === "loaded" && status.remind && status.remind.status !== "loaded" && status.remind.status !== "unsupported"
60
68
  ? [`reminder job ${status.remind.status === "absent" ? "absent" : status.remind.message ?? "not loaded"}`]
61
69
  : [];
62
- if (!absent && status.status !== "not_loaded" && runtimeProblems.length === 0 && remindProblem.length === 0) {
70
+ if (!absent && status.status !== "not_loaded" && runtimeProblems.length === 0 && upgradeProblems.length === 0 && remindProblem.length === 0) {
63
71
  // Healthy is the steady state and stays silent. A marker left by an
64
72
  // earlier attempt is spent: that repair worked, so the next break starts
65
73
  // fresh instead of waiting out a retry window.
@@ -70,6 +78,7 @@ async function readAutostartProblem(paths, options, platform) {
70
78
  ...(absent ? ["registration absent"] : []),
71
79
  ...(status.message ? [status.message] : []),
72
80
  ...runtimeProblems,
81
+ ...upgradeProblems,
73
82
  ...remindProblem,
74
83
  ]
75
84
  .join("; ")
@@ -110,6 +110,9 @@ export async function readRegisteredWindowsRuntimePaths(homeDir) {
110
110
  return {
111
111
  node_executable: /\$nodeExecutable = '([^']*)'/u.exec(script)?.[1] ?? null,
112
112
  cli_entry_point: /\$cliEntryPoint = '([^']*)'/u.exec(script)?.[1] ?? null,
113
+ optimize_for_size: /& \$nodeExecutable --optimize-for-size \$cliEntryPoint sync /u.test(script)
114
+ ? true
115
+ : /& \$nodeExecutable \$cliEntryPoint sync /u.test(script) ? false : null,
113
116
  };
114
117
  }
115
118
  /**
@@ -125,15 +128,22 @@ export async function syncScriptProblem(options) {
125
128
  if (!options.repoRoots || options.repoRoots.length === 0)
126
129
  return null;
127
130
  const settings = resolveAutostartSettings(options, "win32");
128
- const expected = `${UTF8_BOM}${renderWindowsSyncScript({
131
+ const onDisk = await readFile(scriptPath, "utf8").catch(() => null);
132
+ const expectedFor = async (optimizeForSize) => `${UTF8_BOM}${renderWindowsSyncScript({
129
133
  discoveryArgs: await savedDiscoveryLimitArgs(options.homeDir),
130
134
  workDirs: settings.work_dirs,
131
135
  dashboardUrl: settings.dashboard_url,
132
136
  nodeExecutable: (await schedulerNodeExecutable(options, "win32")).path,
133
137
  cliEntryPoint: settings.cli_entry_point,
134
138
  syncLogPath: windowsSyncLogPath(options.homeDir),
139
+ optimizeForSize,
135
140
  })}`;
136
- const difference = renderedFileDifference(await readFile(scriptPath, "utf8").catch(() => null), expected);
141
+ // BLI-23317: the script with --optimize-for-size is what is written; the older one without it is still accepted.
142
+ const difference = renderedFileDifference(onDisk, await expectedFor(true)) === null
143
+ ? null
144
+ : renderedFileDifference(onDisk, await expectedFor(false)) === null
145
+ ? null
146
+ : renderedFileDifference(onDisk, await expectedFor(true));
137
147
  return difference
138
148
  ? `sync script does not match the current roots or Tower runtime (${describeDifference(difference)})`
139
149
  : null;
@@ -271,13 +281,14 @@ export function renderWindowsLauncherScript(options) {
271
281
  ].join("\r\n");
272
282
  }
273
283
  function renderWindowsSyncScript(options) {
284
+ const nodeFlag = options.optimizeForSize === false ? "" : " --optimize-for-size";
274
285
  const dashboardArgs = isDefaultDashboardUrl(options.dashboardUrl)
275
286
  ? ""
276
287
  : ` --dashboard-url ${powershellLiteral(options.dashboardUrl)}`;
277
288
  const discoveryArgs = options.discoveryArgs.length > 0 ? ` ${options.discoveryArgs.join(" ")}` : "";
278
289
  const commands = options.workDirs.flatMap((root) => [
279
290
  "try {",
280
- ` & $nodeExecutable $cliEntryPoint sync --workspace ${powershellLiteral(root)}${dashboardArgs}${discoveryArgs} --json 2>&1 | ForEach-Object { "$_" } | Add-Content -LiteralPath $syncLogPath -Encoding UTF8`,
291
+ ` & $nodeExecutable${nodeFlag} $cliEntryPoint sync --workspace ${powershellLiteral(root)}${dashboardArgs}${discoveryArgs} --json 2>&1 | ForEach-Object { "$_" } | Add-Content -LiteralPath $syncLogPath -Encoding UTF8`,
281
292
  " if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE }",
282
293
  "} catch {",
283
294
  " Add-Content -LiteralPath $syncLogPath -Encoding UTF8 -Value $_.Exception.Message",
package/dist/autostart.js CHANGED
@@ -75,7 +75,20 @@ export async function readRegisteredRuntimePaths(options) {
75
75
  if (platform === "win32") {
76
76
  return readRegisteredWindowsRuntimePaths(options.homeDir);
77
77
  }
78
- return { node_executable: null, cli_entry_point: null };
78
+ return { node_executable: null, cli_entry_point: null, optimize_for_size: null };
79
+ }
80
+ /**
81
+ * BLI-23317: a registration written before `--optimize-for-size` still RUNS, so it is not a broken
82
+ * registration and `registeredRuntimePathProblems` stays silent about it. The scheduled tick's
83
+ * self-heal asks this separately so the existing fleet is rewritten to the new form on its normal
84
+ * cadence instead of waiting for a hand-run `cockpit doctor`.
85
+ */
86
+ export async function registeredLauncherUpgradeProblems(options) {
87
+ const platform = options.platform ?? process.platform;
88
+ if (platform !== (options.hostPlatform ?? process.platform))
89
+ return [];
90
+ const registered = await readRegisteredRuntimePaths(options);
91
+ return registered.optimize_for_size === false ? ["registration predates --optimize-for-size"] : [];
79
92
  }
80
93
  /**
81
94
  * Named problems with the registered runtime, or an empty list. Path-free
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // BLI-23317: first import, so the V8 flag is set before the heavy modules below load.
3
+ import "./optimize-for-size-entry.js";
2
4
  import { runCockpitCli } from "./commands/public-root.js";
3
5
  import {
4
6
  EXIT_DIED_MID_RUN,
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.265");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.266");
25
25
  return 0;
26
26
  }
27
27
 
@@ -207,7 +207,12 @@ async function describeAndReplace(run, input, finds) {
207
207
  const deferredOnly = counts.over_budget + counts.backing_off > 0
208
208
  && counts.described_now + counts.known + counts.failed + counts.too_large + counts.settled_earlier === 0;
209
209
  const replaced = mode === "describe_and_drop" ? finds.swapped(replace) : { replacedCount: 0, charsSaved: 0 };
210
- const pendingImages = [...unique.keys()].filter((sha256) => !described.has(imageStateKey(sha256)));
210
+ // BLI-23317: an image over the describe limit is never sent, so it can never gain a description and
211
+ // never changes the output. Listing it made the describer look like it still had room, and the
212
+ // transcript was rebuilt on every tick (rebuilt_describer_has_room). Counted in `too_large` above.
213
+ const pendingImages = [...unique.values()]
214
+ .filter((image) => !described.has(imageStateKey(image.sha256)) && image.byteSize <= IMAGE_DESCRIBE_MAX_BYTES)
215
+ .map((image) => image.sha256);
211
216
  console.error(TAG + " transcript prepared", JSON.stringify({
212
217
  mode,
213
218
  source: input.source,
@@ -0,0 +1,3 @@
1
+ import { applyOptimizeForSize } from "./optimize-for-size.js";
2
+ // Side-effect module: imported FIRST by the CLI entry so the flag is set before the heavy imports load (BLI-23317).
3
+ applyOptimizeForSize();
@@ -0,0 +1,25 @@
1
+ import v8 from "node:v8";
2
+ /**
3
+ * BLI-23317: run V8 with `--optimize-for-size`. Measured 15-30% lower peak memory with no slowdown on
4
+ * Node 20-26 (one 200 MB transcript 848 -> 721 MB; CLI floor 113 -> 94 MB). `NODE_OPTIONS` rejects the
5
+ * flag, so the scheduled launchers pass it on the node command line and a hand-run `cockpit` applies it
6
+ * here, as the first statement of the entry (`optimize-for-size-entry.ts`, imported before anything heavy).
7
+ *
8
+ * Only a refusal is logged: this runs at the start of every command a person types, and a stderr line
9
+ * on each success would be noise in their terminal. A refusal is logged and ignored, never fatal: the flag is a memory saving, not a requirement.
10
+ */
11
+ export const OPTIMIZE_FOR_SIZE_FLAG = "--optimize-for-size";
12
+ export function applyOptimizeForSize(options = {}) {
13
+ const execArgv = options.execArgv ?? process.execArgv;
14
+ if (execArgv.includes(OPTIMIZE_FOR_SIZE_FLAG)) {
15
+ return "already_set_by_launcher";
16
+ }
17
+ try {
18
+ (options.setFlags ?? v8.setFlagsFromString)(OPTIMIZE_FOR_SIZE_FLAG);
19
+ return "applied";
20
+ }
21
+ catch (error) {
22
+ console.error("[optimize-for-size] could not apply the flag; continuing without it", JSON.stringify({ reason: "set_flags_refused", error: error instanceof Error ? error.name : "unknown" }));
23
+ return "refused";
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.265",
3
+ "version": "0.2.266",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {