@bli-cockpit/cli 0.2.26 → 0.2.27

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
@@ -226,6 +226,22 @@ async function installWindowsTask(options) {
226
226
  "-File",
227
227
  registrationPath,
228
228
  ]);
229
+ // BLI-2996: before this, a nonzero exit here (schtasks rejected the
230
+ // registration script, or Set-ScheduledTask threw) skipped straight to the
231
+ // return with `verified: null` and never logged anything — the doctor row
232
+ // said "needs repair" every run with no way to tell "the rewrite itself
233
+ // never ran" apart from "it ran and Windows still disagrees" (BLI-2996,
234
+ // Brandon's machine: same silent non-convergence under two different
235
+ // validator messages across CLI versions). Metadata only: exit code and
236
+ // whether the process produced any output, never the stderr/stdout text
237
+ // (it can carry the state directory or a workspace path).
238
+ if (created.code !== 0) {
239
+ console.error("[autostart] windows task repair rewrite failed", JSON.stringify({
240
+ task_name: WINDOWS_AUTOSTART_TASK_NAME,
241
+ exit_code: created.code,
242
+ had_output: Boolean(created.stderr.trim() || created.stdout.trim()),
243
+ }));
244
+ }
229
245
  const verified = created.code === 0
230
246
  ? await windowsTaskStatus({
231
247
  ...options,
@@ -238,6 +254,14 @@ async function installWindowsTask(options) {
238
254
  })
239
255
  : null;
240
256
  const loaded = verified?.status === "loaded";
257
+ // windowsTaskStatus() above already logs the needs-repair case (with the
258
+ // problem list) when the rewrite ran but the read-back still disagrees; log
259
+ // the converged branch here too, tagged as a repair outcome and by name, so
260
+ // "did the rewrite actually fix it this run" never depends on inferring it
261
+ // from a routine status log emitted for an unrelated reason.
262
+ if (created.code === 0 && loaded) {
263
+ console.error("[autostart] windows task repair converged", JSON.stringify({ task_name: WINDOWS_AUTOSTART_TASK_NAME }));
264
+ }
241
265
  return {
242
266
  status: "installed",
243
267
  label: AUTOSTART_LABEL,
@@ -549,10 +573,17 @@ function windowsTaskRegistrationProblems(taskXml, expected) {
549
573
  .replace(/<Exec(?:\s[^>]*)?>[\s\S]*?<\/Exec>/giu, "")
550
574
  .trim();
551
575
  const exactExecBlock = execBlocks.length === 1 && actionRemainder === "" ? execBlocks[0] : "";
552
- const command = exactExecBlock
576
+ // Strip a wrapping quote pair defensively: every real capture we have shows
577
+ // Task Scheduler storing <Command> unquoted, but the writer builds the /TR
578
+ // executable segment with the same \"-escaped quoting it uses for the
579
+ // launcher argument (BLI-2598), and there is no verified capture proving
580
+ // schtasks' own TR-splitting heuristic always discards those quotes rather
581
+ // than leaving them as literal text (BLI-2996 canary-gated gap). A stray
582
+ // pair of quotes should never be the reason a correctly-registered task
583
+ // reads as broken.
584
+ const command = stripSurroundingQuotes(exactExecBlock
553
585
  ?.match(/<Command>\s*([^<]*?)\s*<\/Command>/iu)?.[1]
554
- ?.trim() ??
555
- "";
586
+ ?.trim() ?? "");
556
587
  const actionArguments = exactExecBlock
557
588
  .match(/<Arguments>\s*([^<]*?)\s*<\/Arguments>/iu)?.[1]
558
589
  ?.trim() ?? "";
@@ -654,16 +685,35 @@ function windowsWScriptPath(env = process.env) {
654
685
  * DESKTOP-G2UO1GK (CLI 0.2.13, 2026-08-12), where the stored value differed
655
686
  * from the expected one only by those quotes (BLI-2541).
656
687
  *
657
- * Compare the flags exactly, because those are ours, and compare the launcher
658
- * as a path, because that is what Windows normalizes. Each failure returns its
659
- * own reason so a repair message says which half is wrong rather than
660
- * "arguments differ".
688
+ * BLI-2996: that lesson was only ever applied to the pre-BLI-2677 `-File`
689
+ * action. The wscript launcher pair (`//B "<launcher>"`) has NO verified
690
+ * real-Windows capture `windowlessWindowsTaskXml()` in autostart.test.ts is
691
+ * SYNTHETIC, built by hand-editing the one real capture we have rather than
692
+ * exported from a machine actually running this action. A repair that rewrites
693
+ * the task correctly but is judged against a guessed shape can fail forever
694
+ * without ever being wrong (Brandon's machine: same non-convergence under two
695
+ * different validator messages across two CLI versions, neither of which ever
696
+ * repaired anything). So this comparison is deliberately structural, not
697
+ * positional: tokenize the way CommandLineToArgvW groups quoted/unquoted
698
+ * arguments, then check flag-token-then-path-token semantically (a real path
699
+ * comparison, case- and quote-insensitive) instead of demanding the exact
700
+ * byte layout we happened to send. Whatever the wscript action's real
701
+ * normalization turns out to be once a canary captures it, this only fails on
702
+ * an actual different flag or different script — not on Windows' own
703
+ * reformatting.
661
704
  */
662
705
  function windowsActionArgumentProblem(actionArguments, expectedFlags, expectedLauncherPath) {
663
- if (!actionArguments.startsWith(expectedFlags)) {
706
+ const tokens = splitWindowsArgumentTokens(actionArguments);
707
+ if (tokens.length === 0) {
708
+ return "task action names no sync launcher to run";
709
+ }
710
+ if ((tokens[0] ?? "").toUpperCase() !== expectedFlags.toUpperCase()) {
664
711
  return "task action does not run the launcher with the expected batch-mode flag";
665
712
  }
666
- const launcherArgument = unquoteWindowsArgument(actionArguments.slice(expectedFlags.length));
713
+ if (tokens.length > 2) {
714
+ return "task action passes unexpected extra arguments to the sync launcher";
715
+ }
716
+ const launcherArgument = tokens[1];
667
717
  if (!launcherArgument) {
668
718
  return "task action names no sync launcher to run";
669
719
  }
@@ -672,10 +722,46 @@ function windowsActionArgumentProblem(actionArguments, expectedFlags, expectedLa
672
722
  }
673
723
  return null;
674
724
  }
675
- function unquoteWindowsArgument(value) {
676
- const trimmed = value.trim();
677
- const quoted = trimmed.match(/^"([\s\S]*)"$/u) ?? trimmed.match(/^'([\s\S]*)'$/u);
678
- return (quoted?.[1] ?? trimmed).trim();
725
+ /**
726
+ * Quote-aware whitespace tokenizer for an already-XML-decoded `<Arguments>`
727
+ * value: a `"..."` run is one token (quotes stripped, whitespace inside kept),
728
+ * everything else splits on whitespace. Windows paths never contain a quote
729
+ * character, so this does not need CommandLineToArgvW's backslash-escaping
730
+ * rules — only its quoting rule — to tell "one argument with a space in it"
731
+ * apart from "two arguments".
732
+ */
733
+ function splitWindowsArgumentTokens(value) {
734
+ const tokens = [];
735
+ let current = "";
736
+ let inQuotes = false;
737
+ let hasToken = false;
738
+ for (const char of value) {
739
+ if (char === '"') {
740
+ inQuotes = !inQuotes;
741
+ hasToken = true;
742
+ continue;
743
+ }
744
+ if (!inQuotes && /\s/u.test(char)) {
745
+ if (hasToken) {
746
+ tokens.push(current);
747
+ current = "";
748
+ hasToken = false;
749
+ }
750
+ continue;
751
+ }
752
+ current += char;
753
+ hasToken = true;
754
+ }
755
+ if (hasToken)
756
+ tokens.push(current);
757
+ return tokens;
758
+ }
759
+ /** Removes one wrapping pair of double quotes, if present. Used only to
760
+ * normalize before a path comparison — never to reconstruct a shell-safe
761
+ * value. */
762
+ function stripSurroundingQuotes(value) {
763
+ const match = value.match(/^"([\s\S]*)"$/u);
764
+ return match ? match[1] : value;
679
765
  }
680
766
  function sameWindowsPath(left, right) {
681
767
  if (!left || !right)
@@ -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.26");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.27");
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.26",
3
+ "version": "0.2.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {