@bli-cockpit/cli 0.2.18 → 0.2.20

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.
package/dist/autostart.js CHANGED
@@ -10,6 +10,16 @@ const WINDOWS_AUTOSTART_SCRIPT_NAME = "autostart-sync.ps1";
10
10
  const WINDOWS_AUTOSTART_REGISTRATION_SCRIPT_NAME = "autostart-register.ps1";
11
11
  const UTF8_BOM = "\uFEFF";
12
12
  export const DEFAULT_AUTOSTART_INTERVAL_SECONDS = 15 * 60;
13
+ /**
14
+ * macOS rewrites this file on every network transition (join, leave, DNS
15
+ * change), so watching it retries spooled uploads the moment connectivity
16
+ * returns instead of waiting out the StartInterval \u2014 the lid-closed-mid-upload
17
+ * machine hopping caf\u00E9s is where most historical upload failures came from
18
+ * (BLI-2604). The real file, not the /etc/resolv.conf symlink: launchd watches
19
+ * the path it is given, and the symlink itself never changes. Burst fires are
20
+ * cheap \u2014 the collection lock turns overlap into a named no-op.
21
+ */
22
+ export const DARWIN_NETWORK_CHANGE_SIGNAL = "/private/var/run/resolv.conf";
13
23
  const UNSUPPORTED_MESSAGE = "autostart is supported on macOS and Windows only";
14
24
  function plistPathFor(homeDir) {
15
25
  return path.join(homeDir, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`);
@@ -49,8 +59,13 @@ export async function installAutostartAgent(options) {
49
59
  const stderrPath = path.join(paths.state_dir, "sync.err.log");
50
60
  // launchd will not reliably watch a path that does not exist at load time, so
51
61
  // only feed it the transcript dirs that are present right now. A missing dir
52
- // is fine — the StartInterval floor still covers it.
53
- const watchPaths = await existingWatchPaths(homeDir);
62
+ // is fine — the StartInterval floor still covers it. The network signal is
63
+ // appended unconditionally: it always exists on the Macs this plist targets,
64
+ // and an existence filter would drop it when rendering on another host.
65
+ const watchPaths = [
66
+ ...(await existingWatchPaths(homeDir)),
67
+ DARWIN_NETWORK_CHANGE_SIGNAL,
68
+ ];
54
69
  await mkdir(path.dirname(plistPath), { recursive: true });
55
70
  await mkdir(paths.state_dir, { recursive: true });
56
71
  await writeFile(plistPath, renderPlist({
@@ -353,8 +368,16 @@ function renderWindowsRegistrationScript(options) {
353
368
  `$taskName = ${powershellLiteral(options.taskName)}`,
354
369
  `$syncScriptPath = ${powershellLiteral(options.scriptPath)}`,
355
370
  "$powershellPath = Join-Path $PSHOME 'powershell.exe'",
356
- "$actionArgs = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"' + $syncScriptPath + '\"'",
357
- "$taskCommand = '\"' + $powershellPath + '\" ' + $actionArgs",
371
+ // Windows PowerShell 5.1 passes a native argument's embedded quotes to
372
+ // CommandLineToArgvW unescaped, so a /TR value with bare quotes falls out
373
+ // of the quoted region at the first space inside a quoted path:
374
+ // `C:\Users\Brandon Chiem\...` reached schtasks as a stray `Chiem\...`
375
+ // argument and /Create exited 0x80004005 on every CLI version (BLI-2598).
376
+ // Escaping the embedded quotes as \" keeps the whole /TR value one
377
+ // argument; Task Scheduler still normalizes what it stores (BLI-2541),
378
+ // which the read-back validator already compares path-wise.
379
+ "$actionArgs = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \\\"' + $syncScriptPath + '\\\"'",
380
+ "$taskCommand = '\\\"' + $powershellPath + '\\\" ' + $actionArgs",
358
381
  `& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC MINUTE /MO ${options.intervalMinutes} /IT /RL LIMITED /F | Out-Null`,
359
382
  "if ($LASTEXITCODE -ne 0) { throw \"schtasks /Create exited $LASTEXITCODE\" }",
360
383
  "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew",
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.18");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.20");
19
19
  return 0;
20
20
  }
21
21
 
@@ -27,9 +27,24 @@ import { redactSecretLikeContent } from "@bli-cockpit/telemetry-core";
27
27
  export const HEALTH_DETAIL_MAX_CHARS = 600;
28
28
  const WINDOWS_ABSOLUTE_PATH = /[A-Za-z]:\\[^\s"';]+/gu;
29
29
  const UNC_PATH = /\\\\[^\s"';]+/gu;
30
+ // A quoted Windows path may contain spaces; the quotes bound it, so the whole
31
+ // quoted region is the path. Unquoted, the space-blind pattern above stops at
32
+ // the first space — which is how `C:\Users\Brandon Chiem\...` masked to
33
+ // `[path] Chiem\...` and an operator surname reached the server (BLI-2599).
34
+ const QUOTED_WINDOWS_PATH = /"[A-Za-z]:\\[^"]*"/gu;
35
+ const SINGLE_QUOTED_WINDOWS_PATH = /'[A-Za-z]:\\[^']*'/gu;
36
+ // A backslash-joined fragment of three or more segments is path material even
37
+ // without a drive anchor: schtasks echoes a severed path tail as its own token
38
+ // (`Chiem\.local\state\...`, the BLI-2598 receipt). Over-matching here is the
39
+ // safe direction — this boundary exists to keep local identifiers on the
40
+ // machine, not to keep messages pretty.
41
+ const BACKSLASH_PATH_FRAGMENT = /[^\s"';:,()]+(?:\\[^\s"';:,()]+){2,}/gu;
30
42
  // An absolute POSIX path of at least two segments. One segment ("/tmp") carries
31
43
  // nothing identifying and masking it would make messages harder to read.
32
44
  const POSIX_ABSOLUTE_PATH = /\/[\w.@-]+(?:\/[\w.@ -]+)+/gu;
45
+ // The tail a space split off a masked path: `[path] Chiem\...` collapses back
46
+ // into the placeholder it was severed from.
47
+ const PATH_PLACEHOLDER_RUN = /\[path\](?:\s+\[path\])+/gu;
33
48
  export function redactedHealthDetail(message) {
34
49
  const collapsed = message.replace(/\s+/gu, " ").trim();
35
50
  if (!collapsed)
@@ -43,20 +58,28 @@ export function redactedHealthDetail(message) {
43
58
  : masked;
44
59
  }
45
60
  /**
46
- * Paths first, because a path usually contains the account name; whatever
47
- * mentions of the host or account survive on their own are masked after.
61
+ * Identifiers first, so the account name is caught even when it sits inside a
62
+ * path the path patterns would only partially cover (BLI-2599: the path
63
+ * pattern stopped at the space in `Brandon Chiem` and the surname survived).
64
+ * Then quoted paths before unquoted ones, because the quotes are what bound a
65
+ * space-containing path; then the drive-anchored and fragment patterns; then
66
+ * adjacent placeholders collapse so a severed path reads as one `[path]`.
48
67
  */
49
68
  export function maskLocalIdentifiers(value) {
50
- let text = value
51
- .replace(UNC_PATH, "[path]")
52
- .replace(WINDOWS_ABSOLUTE_PATH, "[path]")
53
- .replace(POSIX_ABSOLUTE_PATH, "[path]");
69
+ let text = value;
54
70
  for (const [identifier, placeholder] of localIdentifiers()) {
55
71
  if (!identifier)
56
72
  continue;
57
73
  text = text.replace(literalPattern(identifier), placeholder);
58
74
  }
59
- return text;
75
+ return text
76
+ .replace(QUOTED_WINDOWS_PATH, "[path]")
77
+ .replace(SINGLE_QUOTED_WINDOWS_PATH, "[path]")
78
+ .replace(UNC_PATH, "[path]")
79
+ .replace(WINDOWS_ABSOLUTE_PATH, "[path]")
80
+ .replace(BACKSLASH_PATH_FRAGMENT, "[path]")
81
+ .replace(POSIX_ABSOLUTE_PATH, "[path]")
82
+ .replace(PATH_PLACEHOLDER_RUN, "[path]");
60
83
  }
61
84
  function localIdentifiers() {
62
85
  const identifiers = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {