@bli-cockpit/cli 0.2.13 → 0.2.14

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
@@ -270,6 +270,13 @@ async function windowsTaskStatus(options) {
270
270
  const queryMessage = `${query.stderr}\n${query.stdout}`;
271
271
  const taskIsKnownAbsent = query.code === 1 &&
272
272
  /cannot find|not found|does not exist/iu.test(queryMessage);
273
+ // Metadata only: schtasks stderr can carry the state directory, so the exit
274
+ // code and the classification travel, never the text.
275
+ console.error("[autostart] windows task query failed", JSON.stringify({
276
+ task_name: WINDOWS_AUTOSTART_TASK_NAME,
277
+ exit_code: query.code,
278
+ status: taskIsKnownAbsent ? "absent" : "not_loaded",
279
+ }));
273
280
  return {
274
281
  status: taskIsKnownAbsent ? "absent" : "not_loaded",
275
282
  label: AUTOSTART_LABEL,
@@ -305,6 +312,23 @@ async function windowsTaskStatus(options) {
305
312
  registrationProblems.push("sync script does not match the current roots or Cockpit runtime");
306
313
  }
307
314
  }
315
+ // Both branches log. A line that only fires on failure cannot answer "did
316
+ // background collection validate at all today?", which is the question that
317
+ // would have caught a validator rejecting every healthy Windows task.
318
+ // Problem labels carry no paths, so they are safe to emit verbatim.
319
+ if (registrationProblems.length > 0) {
320
+ console.error("[autostart] windows task needs repair", JSON.stringify({
321
+ task_name: WINDOWS_AUTOSTART_TASK_NAME,
322
+ problem_count: registrationProblems.length,
323
+ problems: registrationProblems,
324
+ }));
325
+ }
326
+ else {
327
+ console.error("[autostart] windows task validated", JSON.stringify({
328
+ task_name: WINDOWS_AUTOSTART_TASK_NAME,
329
+ interval_minutes: intervalMinutes,
330
+ }));
331
+ }
308
332
  return {
309
333
  status: registrationProblems.length > 0 ? "not_loaded" : "loaded",
310
334
  label: AUTOSTART_LABEL,
@@ -415,7 +439,7 @@ function windowsTaskRegistrationProblems(taskXml, expected) {
415
439
  const actionArguments = exactExecBlock
416
440
  .match(/<Arguments>\s*([^<]*?)\s*<\/Arguments>/iu)?.[1]
417
441
  ?.trim() ?? "";
418
- const expectedActionArguments = `-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${expected.scriptPath}"`;
442
+ const expectedArgumentFlags = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File";
419
443
  const requirements = [
420
444
  [
421
445
  /<DisallowStartIfOnBatteries>\s*false\s*<\/DisallowStartIfOnBatteries>/iu,
@@ -444,8 +468,18 @@ function windowsTaskRegistrationProblems(taskXml, expected) {
444
468
  if (!/<LogonType>\s*InteractiveToken\s*<\/LogonType>/iu.test(exactPrincipalBlock)) {
445
469
  problems.push("logon type is not InteractiveToken");
446
470
  }
447
- if (!/<RunLevel>\s*LeastPrivilege\s*<\/RunLevel>/iu.test(exactPrincipalBlock)) {
448
- problems.push("task does not run with limited privileges");
471
+ // Windows omits <RunLevel> entirely when a task runs at the default
472
+ // LeastPrivilege, so `schtasks /Create ... /RL LIMITED` stores a principal
473
+ // block with no RunLevel element at all. Demanding the element asks Windows
474
+ // to state a default it never states, which is why this check failed on
475
+ // every Windows machine in the fleet and passed on none (0 of 4
476
+ // autostart-alive events ok, 2026-08-12). Absent means LeastPrivilege; only
477
+ // an explicit elevated value is a real problem.
478
+ const runLevel = exactPrincipalBlock
479
+ .match(/<RunLevel>\s*([^<]*?)\s*<\/RunLevel>/iu)?.[1]
480
+ ?.trim() || "LeastPrivilege";
481
+ if (!/^LeastPrivilege$/iu.test(runLevel)) {
482
+ problems.push(`task does not run with limited privileges (RunLevel=${runLevel})`);
449
483
  }
450
484
  }
451
485
  if (actionContext && actionContext !== principalId) {
@@ -477,8 +511,9 @@ function windowsTaskRegistrationProblems(taskXml, expected) {
477
511
  if (!sameWindowsPath(command, expected.powershellPath)) {
478
512
  problems.push("task action does not run the expected Windows PowerShell");
479
513
  }
480
- if (actionArguments !== expectedActionArguments) {
481
- problems.push("task action arguments do not exactly run the Cockpit sync script");
514
+ const actionArgumentProblem = windowsActionArgumentProblem(actionArguments, expectedArgumentFlags, expected.scriptPath);
515
+ if (actionArgumentProblem) {
516
+ problems.push(actionArgumentProblem);
482
517
  }
483
518
  if (/<Duration>/iu.test(repetition)) {
484
519
  problems.push("repetition has a finite duration");
@@ -489,6 +524,38 @@ function windowsPowerShellPath(env = process.env) {
489
524
  const windowsRoot = env.SystemRoot ?? env.WINDIR ?? "C:\\Windows";
490
525
  return path.win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
491
526
  }
527
+ /**
528
+ * Task Scheduler stores a normalized form of the action it was given, not the
529
+ * string we passed: `schtasks /Create /TR "<powershell> <args>"` is split into
530
+ * <Command> plus <Arguments>, and the quotes around a space-free script path
531
+ * are dropped. Comparing <Arguments> against the exact string we built
532
+ * therefore fails on a task that is registered perfectly — observed on
533
+ * DESKTOP-G2UO1GK (CLI 0.2.13, 2026-08-12), where the stored value differed
534
+ * from the expected one only by those quotes.
535
+ *
536
+ * Compare the flags exactly, because those are ours, and compare the script as
537
+ * a path, because that is what Windows normalizes. Each failure returns its own
538
+ * reason so a repair message says which half is wrong rather than "arguments
539
+ * differ".
540
+ */
541
+ function windowsActionArgumentProblem(actionArguments, expectedFlags, expectedScriptPath) {
542
+ if (!actionArguments.startsWith(expectedFlags)) {
543
+ return "task action does not run PowerShell with the expected Cockpit sync flags";
544
+ }
545
+ const scriptArgument = unquoteWindowsArgument(actionArguments.slice(expectedFlags.length));
546
+ if (!scriptArgument) {
547
+ return "task action names no sync script to run";
548
+ }
549
+ if (!sameWindowsPath(scriptArgument, expectedScriptPath)) {
550
+ return "task action runs a script other than the Cockpit sync script";
551
+ }
552
+ return null;
553
+ }
554
+ function unquoteWindowsArgument(value) {
555
+ const trimmed = value.trim();
556
+ const quoted = trimmed.match(/^"([\s\S]*)"$/u) ?? trimmed.match(/^'([\s\S]*)'$/u);
557
+ return (quoted?.[1] ?? trimmed).trim();
558
+ }
492
559
  function sameWindowsPath(left, right) {
493
560
  if (!left || !right)
494
561
  return false;
@@ -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.13");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.14");
19
19
  return 0;
20
20
  }
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {