@indigoai-us/hq-cli 5.345.178 → 5.345.180

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 (117) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/command-catalog.generated.d.ts +197 -1
  3. package/dist/command-catalog.generated.js +241 -1
  4. package/dist/commands/bot-jobs.d.ts +11 -9
  5. package/dist/commands/bot-jobs.js +56 -21
  6. package/dist/commands/bot.d.ts +37 -1
  7. package/dist/commands/bot.js +289 -13
  8. package/dist/commands/channels.d.ts +18 -1
  9. package/dist/commands/channels.js +185 -1
  10. package/dist/commands/daemon.d.ts +2 -0
  11. package/dist/commands/daemon.js +15 -4
  12. package/dist/commands/integrations-core.d.ts +2 -0
  13. package/dist/commands/integrations-core.js +1 -0
  14. package/dist/commands/lanes.d.ts +1 -1
  15. package/dist/commands/lanes.js +6 -2
  16. package/dist/commands/mesh.js +19 -0
  17. package/dist/commands/sources.d.ts +89 -0
  18. package/dist/commands/sources.js +363 -1
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/bot/config.d.ts +8 -0
  22. package/dist/lib/bot/fleet-local.d.ts +1 -1
  23. package/dist/lib/bot/fleet-local.js +82 -30
  24. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  25. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  26. package/dist/lib/bot/local-jobs.d.ts +40 -4
  27. package/dist/lib/bot/local-jobs.js +72 -5
  28. package/dist/lib/bot/run.d.ts +15 -0
  29. package/dist/lib/bot/run.js +99 -1
  30. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  31. package/dist/lib/bot/runtime-auth.js +251 -0
  32. package/dist/lib/daemon/catalog.d.ts +1 -0
  33. package/dist/lib/daemon/catalog.js +1 -1
  34. package/dist/lib/daemon/handoff.d.ts +4 -0
  35. package/dist/lib/daemon/handoff.js +6 -2
  36. package/dist/lib/daemon/install.d.ts +70 -2
  37. package/dist/lib/daemon/install.js +185 -27
  38. package/dist/lib/daemon/legacy.d.ts +9 -3
  39. package/dist/lib/daemon/legacy.js +11 -4
  40. package/dist/lib/daemon/migrate.js +1 -1
  41. package/dist/lib/daemon/run.d.ts +5 -2
  42. package/dist/lib/daemon/run.js +74 -22
  43. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  44. package/dist/lib/doctor/checks/anywhere.js +35 -6
  45. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  46. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  47. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  48. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  49. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  50. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  51. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  52. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  53. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  54. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  55. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  56. package/dist/lib/hot-channels/adapters/index.js +39 -0
  57. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  58. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  59. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  60. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  61. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  62. package/dist/lib/hot-channels/adapters/meetings.js +525 -0
  63. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  64. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  65. package/dist/lib/hot-channels/alert.d.ts +78 -0
  66. package/dist/lib/hot-channels/alert.js +179 -0
  67. package/dist/lib/hot-channels/audience.d.ts +15 -0
  68. package/dist/lib/hot-channels/audience.js +25 -0
  69. package/dist/lib/hot-channels/contract.d.ts +146 -0
  70. package/dist/lib/hot-channels/contract.js +20 -0
  71. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  72. package/dist/lib/hot-channels/cursors.js +84 -0
  73. package/dist/lib/hot-channels/index.d.ts +10 -0
  74. package/dist/lib/hot-channels/index.js +10 -0
  75. package/dist/lib/hot-channels/landing.d.ts +83 -0
  76. package/dist/lib/hot-channels/landing.js +175 -0
  77. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  78. package/dist/lib/hot-channels/ledger.js +203 -0
  79. package/dist/lib/hot-channels/loop.d.ts +74 -0
  80. package/dist/lib/hot-channels/loop.js +221 -0
  81. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  82. package/dist/lib/hot-channels/registry.d.ts +28 -0
  83. package/dist/lib/hot-channels/registry.js +121 -0
  84. package/dist/lib/hot-channels/run.d.ts +137 -0
  85. package/dist/lib/hot-channels/run.js +324 -0
  86. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  87. package/dist/lib/hot-channels/signals-pass.js +581 -0
  88. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  89. package/dist/lib/hot-channels/sync-lock.js +224 -0
  90. package/dist/lib/install/hook-target.d.ts +6 -2
  91. package/dist/lib/install/hook-target.js +9 -4
  92. package/dist/lib/lanes/accounts.d.ts +1 -17
  93. package/dist/lib/lanes/accounts.js +5 -45
  94. package/dist/lib/lanes/loop.d.ts +10 -0
  95. package/dist/lib/lanes/loop.js +18 -1
  96. package/dist/lib/lanes/provider-home.d.ts +24 -0
  97. package/dist/lib/lanes/provider-home.js +52 -0
  98. package/dist/lib/lanes/spawn.d.ts +12 -0
  99. package/dist/lib/lanes/spawn.js +27 -0
  100. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  101. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  102. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  103. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  104. package/dist/lib/mesh/live/daemon/run.js +31 -0
  105. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  106. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  107. package/dist/lib/mesh/live/paths.d.ts +2 -0
  108. package/dist/lib/mesh/live/paths.js +2 -0
  109. package/dist/lib/sync-core/flag.d.ts +1 -1
  110. package/dist/lib/sync-core/flag.js +1 -1
  111. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  112. package/dist/lib/sync-core/reconcile.js +9 -7
  113. package/dist/lib/sync-core/release.d.ts +3 -3
  114. package/dist/lib/sync-core/release.js +4 -4
  115. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  116. package/dist/lib/sync-core/rollout-state.js +1 -0
  117. package/package.json +8 -8
@@ -148,7 +148,7 @@ function syncOwnerBlock(ctx) {
148
148
  }
149
149
  return undefined;
150
150
  }
151
- function daemonAloneRunsLegacySync(ctx) {
151
+ export function daemonAloneRunsLegacySync(ctx) {
152
152
  return ctx.syncOwner === "daemon" && ctx.legacySyncUnitManagesSync !== true && syncOwnerBlock(ctx) === undefined;
153
153
  }
154
154
  function planService(service, ctx) {
@@ -32,7 +32,11 @@ export interface DaemonUnitHost {
32
32
  platform: NodeJS.Platform;
33
33
  home: string;
34
34
  systemUnitDir?: string;
35
+ /** This user's own Scheduled Task name (daemonWindowsTaskName); another user's task on the PC does not count. */
36
+ windowsTaskName: string;
35
37
  windowsTaskExists: (task: string) => boolean;
38
+ /** The shared task an older install registered runs as this user (it still counts until install moves it). */
39
+ windowsLegacyTaskIsMine?: () => boolean;
36
40
  }
37
41
  /** The `hq daemon` OS unit is installed on this machine (for this user, or as the box's system unit). */
38
42
  export declare function daemonUnitInstalled(host: DaemonUnitHost): boolean;
@@ -10,7 +10,8 @@ import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { DEFAULT_SERVICES, enabledServices, isServiceName } from "./catalog.js";
12
12
  import { readDaemonConfig, writeDaemonConfig } from "./config.js";
13
- import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from "./install.js";
13
+ import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, SYSTEM_UNIT_DIR, currentWindowsIdentity, legacyWindowsTask, windowsTaskName } from "./install.js";
14
+ import { defaultRunCommand } from "../service-manager/launchd.js";
14
15
  import { isDaemonProcess, runningDaemonPid } from "./lock.js";
15
16
  import { detectMachineType } from "./machine-type.js";
16
17
  import { daemonPaths } from "./paths.js";
@@ -96,7 +97,7 @@ export function daemonUnitInstalled(host) {
96
97
  case "darwin":
97
98
  return fs.existsSync(path.join(host.home, "Library", "LaunchAgents", `${DAEMON_LAUNCHD_LABEL}.plist`));
98
99
  case "win32":
99
- return host.windowsTaskExists(DAEMON_WINDOWS_TASK);
100
+ return host.windowsTaskExists(host.windowsTaskName) || (host.windowsLegacyTaskIsMine?.() ?? false);
100
101
  default:
101
102
  return (fs.existsSync(path.join(host.home, ".config", "systemd", "user", DAEMON_SYSTEMD_UNIT)) ||
102
103
  fs.existsSync(path.join(host.systemUnitDir ?? SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)));
@@ -142,9 +143,12 @@ export function handOffInstallHere(services, env = process.env, opts = {}) {
142
143
  if (opts.ifStale && services.every((service) => config.disable.includes(service))) {
143
144
  return `hq daemon has ${services.join(", ")} disabled, so the older service will not be installed.`;
144
145
  }
146
+ const identity = process.platform === "win32" ? currentWindowsIdentity() : undefined;
145
147
  const installed = daemonUnitInstalled({
146
148
  platform: process.platform,
147
149
  home,
150
+ windowsTaskName: identity ? windowsTaskName(identity) : "",
151
+ windowsLegacyTaskIsMine: () => identity !== undefined && legacyWindowsTask(defaultRunCommand, identity).state === "mine",
148
152
  windowsTaskExists: (task) => spawnSync("schtasks.exe", ["/Query", "/TN", task], { encoding: "utf8", windowsHide: true }).status === 0,
149
153
  }) || hostedDaemonRunning(paths);
150
154
  if (!installed) {
@@ -7,7 +7,7 @@
7
7
  * - Linux, per-user: systemd user unit hq-daemon.service
8
8
  * - Linux, system (outposts and agent boxes): /etc/systemd/system/hq-daemon.service
9
9
  * running as the box's login user; needs root
10
- * - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon
10
+ * - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon-<user SID>
11
11
  * - anything else: no unit; the caller prints the command to run
12
12
  *
13
13
  * The unit never names services. `hq daemon run` reads the machine type and
@@ -20,8 +20,34 @@ import { type PidLockRecord } from "../mesh/live/daemon/pid-lock.js";
20
20
  import type { MachineType } from "./machine-type.js";
21
21
  export declare const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
22
22
  export declare const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
23
- export declare const DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
23
+ /**
24
+ * The one machine-wide task name older installs registered on Windows. Two
25
+ * users on one PC overwrote each other's daemon under it, so each user now
26
+ * gets their own name (daemonWindowsTaskName). Install moves the current
27
+ * user's task off this name; another user's task under it is left alone.
28
+ */
29
+ export declare const LEGACY_DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
24
30
  export declare const SYSTEM_UNIT_DIR = "/etc/systemd/system";
31
+ /**
32
+ * The Windows task name for one user: the shared prefix plus the user's SID,
33
+ * or the `DOMAIN\user` name with characters a task name cannot hold replaced
34
+ * when the SID could not be read. Never the bare legacy name.
35
+ */
36
+ export declare function daemonWindowsTaskName(who: {
37
+ sid?: string;
38
+ user: string;
39
+ }): string;
40
+ /** The current Windows user's SID from `whoami /user`, or undefined when it cannot be read. */
41
+ export declare function readWindowsSid(run: RunCommand): string | undefined;
42
+ /** The per-user task name for the user running this process. */
43
+ export declare function currentDaemonWindowsTaskName(run: RunCommand, user: string): string;
44
+ /**
45
+ * `DOMAIN\user` for the current account. Same answer as the mesh installer's
46
+ * resolveWindowsUser, kept here so lookups do not load the mesh installer.
47
+ */
48
+ export declare function currentWindowsUser(env?: NodeJS.ProcessEnv): string;
49
+ /** The current user as a DaemonHost names them on Windows. */
50
+ export declare function currentWindowsIdentity(run?: RunCommand, env?: NodeJS.ProcessEnv): Pick<DaemonHost, "windowsSid" | "windowsUser">;
25
51
  export type InstallScope = "user" | "system";
26
52
  export interface DaemonHost {
27
53
  platform: NodeJS.Platform;
@@ -34,6 +60,8 @@ export interface DaemonHost {
34
60
  isRoot: boolean;
35
61
  /** `DOMAIN\user` the Windows task runs as. */
36
62
  windowsUser: string;
63
+ /** That user's SID, when `whoami /user` could read it. Names the per-user task. */
64
+ windowsSid?: string;
37
65
  /** conhost.exe path when `--headless` is supported, else undefined. */
38
66
  windowsHeadlessLauncher: string | undefined;
39
67
  /** Where system units are written (tests point this at a temp dir). */
@@ -70,6 +98,8 @@ export interface DaemonUnitResult {
70
98
  startPending?: true;
71
99
  /** Windows: an older daemon may still be running; HQ could not verify it, so did not stop it. */
72
100
  warning?: string;
101
+ /** Windows: what happened to the shared task from an older install, when there is one. */
102
+ notice?: string;
73
103
  }
74
104
  export interface InstallDeps {
75
105
  run: RunCommand;
@@ -93,6 +123,43 @@ export declare function daemonRootRefusal(isRoot: boolean, machineType: MachineT
93
123
  /** Boxes run the daemon as a system unit; everything else per user. */
94
124
  export declare function installScopeFor(machineType: MachineType): InstallScope;
95
125
  export declare function planDaemonUnit(host: DaemonHost, options: InstallOptions): DaemonUnitPlan;
126
+ /**
127
+ * The registered per-user task names for `host`: the current name, and the
128
+ * user-name form an install used when it could not read the SID. The fallback
129
+ * name is only returned when its principal is this user.
130
+ */
131
+ export declare function windowsUserTaskNames(run: RunCommand, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): string[];
132
+ /** The per-user task name for `host`. */
133
+ export declare function windowsTaskName(host: Pick<DaemonHost, "windowsSid" | "windowsUser">): string;
134
+ /**
135
+ * Whether two Windows account names are the same user, ignoring case. Both
136
+ * names must have the same form: `DOMAIN\user` matches only the same
137
+ * `DOMAIN\user`, and a bare `user` only the same bare `user`. A bare name is
138
+ * never matched against a domain name, because `bob` could be `PC\bob` or
139
+ * `CORP\bob`, and a wrong match would end and delete another user's task.
140
+ */
141
+ export declare function sameWindowsUser(a: string, b: string): boolean;
142
+ /**
143
+ * The shared task an older install registered, and whose it is. "mine" means
144
+ * its principal is the current user, so HQ may move it. "other" means another
145
+ * user's, or a principal HQ cannot match to the current user; "unreadable"
146
+ * means schtasks refused to show it. Both are left alone.
147
+ */
148
+ export type LegacyWindowsTask = {
149
+ state: "none";
150
+ } | {
151
+ state: "mine";
152
+ owner: string;
153
+ } | {
154
+ state: "other";
155
+ owner: string;
156
+ } | {
157
+ state: "unreadable";
158
+ reason: string;
159
+ };
160
+ export declare function legacyWindowsTask(run: RunCommand, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): LegacyWindowsTask;
161
+ /** Whose task `task` is, read from `schtasks /Query /XML`. */
162
+ export declare function classifyWindowsTask(run: RunCommand, task: string, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): LegacyWindowsTask;
96
163
  /** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
97
164
  export declare function oldDaemonWarning(pid: number, reason: string): string;
98
165
  export declare function installDaemonUnit(host: DaemonHost, options: InstallOptions, deps: InstallDeps): DaemonUnitResult;
@@ -127,5 +194,6 @@ export declare function uninstallDaemonUnit(host: DaemonHost, options: InstallOp
127
194
  unitPath: string;
128
195
  removed: boolean;
129
196
  warning?: string;
197
+ notice?: string;
130
198
  };
131
199
  //# sourceMappingURL=install.d.ts.map
@@ -7,7 +7,7 @@
7
7
  * - Linux, per-user: systemd user unit hq-daemon.service
8
8
  * - Linux, system (outposts and agent boxes): /etc/systemd/system/hq-daemon.service
9
9
  * running as the box's login user; needs root
10
- * - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon
10
+ * - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon-<user SID>
11
11
  * - anything else: no unit; the caller prints the command to run
12
12
  *
13
13
  * The unit never names services. `hq daemon run` reads the machine type and
@@ -16,6 +16,7 @@
16
16
  * never touch launchctl, systemctl or schtasks.
17
17
  */
18
18
  import * as fs from "node:fs";
19
+ import * as os from "node:os";
19
20
  import * as path from "node:path";
20
21
  import { activateLaunchd, blockingSleep, deactivateLaunchd, defaultRunCommand, installLaunchdPlist, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
21
22
  import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdUnitPath, } from "../service-manager/systemd.js";
@@ -26,8 +27,58 @@ import { liveDaemonLock, verifyDaemonForKill } from "./lock.js";
26
27
  import { daemonPaths } from "./paths.js";
27
28
  export const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
28
29
  export const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
29
- export const DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
30
+ /**
31
+ * The one machine-wide task name older installs registered on Windows. Two
32
+ * users on one PC overwrote each other's daemon under it, so each user now
33
+ * gets their own name (daemonWindowsTaskName). Install moves the current
34
+ * user's task off this name; another user's task under it is left alone.
35
+ */
36
+ export const LEGACY_DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
30
37
  export const SYSTEM_UNIT_DIR = "/etc/systemd/system";
38
+ /**
39
+ * The Windows task name for one user: the shared prefix plus the user's SID,
40
+ * or the `DOMAIN\user` name with characters a task name cannot hold replaced
41
+ * when the SID could not be read. Never the bare legacy name.
42
+ */
43
+ export function daemonWindowsTaskName(who) {
44
+ const sid = who.sid?.trim();
45
+ if (sid)
46
+ return `${LEGACY_DAEMON_WINDOWS_TASK}-${sid}`;
47
+ const user = who.user.trim().replace(/[^A-Za-z0-9_.-]/g, "_");
48
+ return `${LEGACY_DAEMON_WINDOWS_TASK}-${user || "user"}`;
49
+ }
50
+ /** The current Windows user's SID from `whoami /user`, or undefined when it cannot be read. */
51
+ export function readWindowsSid(run) {
52
+ const r = run("whoami.exe", ["/user", "/FO", "CSV", "/NH"]);
53
+ if (r.status !== 0)
54
+ return undefined;
55
+ return /S-1-\d+(?:-\d+)+/.exec(r.stdout)?.[0];
56
+ }
57
+ /** The per-user task name for the user running this process. */
58
+ export function currentDaemonWindowsTaskName(run, user) {
59
+ return daemonWindowsTaskName({ sid: readWindowsSid(run), user });
60
+ }
61
+ /**
62
+ * `DOMAIN\user` for the current account. Same answer as the mesh installer's
63
+ * resolveWindowsUser, kept here so lookups do not load the mesh installer.
64
+ */
65
+ export function currentWindowsUser(env = process.env) {
66
+ let user = env.USERNAME?.trim() ?? "";
67
+ if (!user) {
68
+ try {
69
+ user = os.userInfo().username;
70
+ }
71
+ catch {
72
+ user = "";
73
+ }
74
+ }
75
+ const domain = env.USERDOMAIN?.trim();
76
+ return domain ? `${domain}\\${user}` : user;
77
+ }
78
+ /** The current user as a DaemonHost names them on Windows. */
79
+ export function currentWindowsIdentity(run = defaultRunCommand, env = process.env) {
80
+ return { windowsUser: currentWindowsUser(env), windowsSid: readWindowsSid(run) };
81
+ }
31
82
  /** How long install waits for the Windows task to start a daemon process. */
32
83
  export const WINDOWS_START_WAIT_MS = 10_000;
33
84
  const WINDOWS_START_POLL_MS = 500;
@@ -128,7 +179,7 @@ function renderWindowsTaskXml(host) {
128
179
  return `<?xml version="1.0" encoding="UTF-16"?>
129
180
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
130
181
  <RegistrationInfo>
131
- <URI>\\${DAEMON_WINDOWS_TASK}</URI>
182
+ <URI>\\${windowsTaskName(host)}</URI>
132
183
  <Description>HQ daemon: runs this machine's HQ background services</Description>
133
184
  </RegistrationInfo>
134
185
  <Triggers>
@@ -204,7 +255,7 @@ export function planDaemonUnit(host, options) {
204
255
  if (host.platform === "win32") {
205
256
  return {
206
257
  kind: "schtasks",
207
- unitPath: path.join(host.daemonDir, `${DAEMON_WINDOWS_TASK}.xml`),
258
+ unitPath: path.join(host.daemonDir, `${LEGACY_DAEMON_WINDOWS_TASK}.xml`),
208
259
  rendered: renderWindowsTaskXml(host),
209
260
  };
210
261
  }
@@ -224,8 +275,9 @@ function commandError(what, r) {
224
275
  * immediately before the kill and killed only when it is verifiably the
225
276
  * daemon. Returns a warning when a process was left running unverified.
226
277
  */
227
- function endWindowsDaemon(run, owner) {
228
- run("schtasks.exe", ["/End", "/TN", DAEMON_WINDOWS_TASK]);
278
+ function endWindowsDaemon(run, owner, tasks) {
279
+ for (const task of tasks)
280
+ run("schtasks.exe", ["/End", "/TN", task]);
229
281
  if (!owner || owner.pid === process.pid)
230
282
  return undefined;
231
283
  const check = verifyDaemonForKill(owner, run);
@@ -237,6 +289,79 @@ function endWindowsDaemon(run, owner) {
237
289
  return undefined;
238
290
  return oldDaemonWarning(owner.pid, check.reason);
239
291
  }
292
+ /**
293
+ * The registered per-user task names for `host`: the current name, and the
294
+ * user-name form an install used when it could not read the SID. The fallback
295
+ * name is only returned when its principal is this user.
296
+ */
297
+ export function windowsUserTaskNames(run, host) {
298
+ const names = [];
299
+ const current = windowsTaskName(host);
300
+ if (run("schtasks.exe", ["/Query", "/TN", current]).status === 0)
301
+ names.push(current);
302
+ const byUser = daemonWindowsTaskName({ user: host.windowsUser });
303
+ if (byUser !== current && classifyWindowsTask(run, byUser, host).state === "mine")
304
+ names.push(byUser);
305
+ return names;
306
+ }
307
+ /** The per-user task name for `host`. */
308
+ export function windowsTaskName(host) {
309
+ return daemonWindowsTaskName({ sid: host.windowsSid, user: host.windowsUser });
310
+ }
311
+ /**
312
+ * Whether two Windows account names are the same user, ignoring case. Both
313
+ * names must have the same form: `DOMAIN\user` matches only the same
314
+ * `DOMAIN\user`, and a bare `user` only the same bare `user`. A bare name is
315
+ * never matched against a domain name, because `bob` could be `PC\bob` or
316
+ * `CORP\bob`, and a wrong match would end and delete another user's task.
317
+ */
318
+ export function sameWindowsUser(a, b) {
319
+ const left = a.trim().toLowerCase();
320
+ const right = b.trim().toLowerCase();
321
+ const leftBare = (left.split("\\").pop() ?? "").trim();
322
+ const rightBare = (right.split("\\").pop() ?? "").trim();
323
+ if (!leftBare || !rightBare)
324
+ return false;
325
+ if (left.includes("\\") !== right.includes("\\"))
326
+ return false;
327
+ return left === right;
328
+ }
329
+ /** The principal a task XML runs as. */
330
+ function windowsTaskPrincipal(xml) {
331
+ return /<Principal\b[^>]*>[\s\S]*?<UserId>([^<]*)<\/UserId>/.exec(xml)?.[1]?.trim() || undefined;
332
+ }
333
+ export function legacyWindowsTask(run, host) {
334
+ return classifyWindowsTask(run, LEGACY_DAEMON_WINDOWS_TASK, host);
335
+ }
336
+ /** Whose task `task` is, read from `schtasks /Query /XML`. */
337
+ export function classifyWindowsTask(run, task, host) {
338
+ const r = run("schtasks.exe", ["/Query", "/TN", task, "/XML"]);
339
+ if (r.status !== 0) {
340
+ const reason = (r.stderr.trim() || r.stdout.trim()).replace(/\0/g, "");
341
+ return /access is denied/i.test(reason) ? { state: "unreadable", reason } : { state: "none" };
342
+ }
343
+ const owner = windowsTaskPrincipal(r.stdout.replace(/\0/g, ""));
344
+ if (!owner)
345
+ return { state: "unreadable", reason: "the task does not name the user it runs as" };
346
+ if (/^S-1-/i.test(owner)) {
347
+ const sid = host.windowsSid?.trim();
348
+ return sid && sid.toLowerCase() === owner.toLowerCase() ? { state: "mine", owner } : { state: "other", owner };
349
+ }
350
+ return sameWindowsUser(owner, host.windowsUser) ? { state: "mine", owner } : { state: "other", owner };
351
+ }
352
+ const REMOVE_LEGACY_TASK = `schtasks /Delete /TN ${LEGACY_DAEMON_WINDOWS_TASK} /F`;
353
+ /** What to tell the user about a shared task HQ left alone, or undefined when there is none. */
354
+ function legacyLeftNotice(legacy) {
355
+ if (legacy.state === "other") {
356
+ return (`Another Windows user's HQ daemon task (\\${LEGACY_DAEMON_WINDOWS_TASK}, run as ${legacy.owner}) was left untouched. ` +
357
+ `If it is in fact yours, remove it with: ${REMOVE_LEGACY_TASK}`);
358
+ }
359
+ if (legacy.state === "unreadable") {
360
+ return (`The shared HQ daemon task \\${LEGACY_DAEMON_WINDOWS_TASK} from an older install was left untouched because HQ could not read it (${legacy.reason}). ` +
361
+ `It may belong to another Windows user. If it is yours, remove it with: ${REMOVE_LEGACY_TASK}`);
362
+ }
363
+ return undefined;
364
+ }
240
365
  /** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
241
366
  export function oldDaemonWarning(pid, reason) {
242
367
  return [
@@ -250,10 +375,10 @@ function activate(plan, host, run, sleep, previous = null) {
250
375
  switch (plan.kind) {
251
376
  case "launchd":
252
377
  activateLaunchd(plan.unitPath, run, host.uid, { sleep });
253
- return;
378
+ return {};
254
379
  case "systemd-user":
255
380
  activateSystemd(DAEMON_SYSTEMD_UNIT, run);
256
- return;
381
+ return {};
257
382
  case "systemd-system": {
258
383
  const reload = run("systemctl", ["daemon-reload"]);
259
384
  if (reload.status !== 0)
@@ -262,23 +387,41 @@ function activate(plan, host, run, sleep, previous = null) {
262
387
  if (enable.status !== 0)
263
388
  throw commandError(`systemctl enable --now ${DAEMON_SYSTEMD_UNIT}`, enable);
264
389
  run("systemctl", ["restart", DAEMON_SYSTEMD_UNIT]);
265
- return;
390
+ return {};
266
391
  }
267
392
  case "schtasks": {
268
- const create = run("schtasks.exe", ["/Create", "/TN", DAEMON_WINDOWS_TASK, "/XML", plan.unitPath, "/F"]);
393
+ const task = windowsTaskName(host);
394
+ // Older installs registered one task name for the whole machine. Read
395
+ // whose it is before registering this user's own task.
396
+ const legacy = legacyWindowsTask(run, host);
397
+ const create = run("schtasks.exe", ["/Create", "/TN", task, "/XML", plan.unitPath, "/F"]);
269
398
  if (create.status !== 0)
270
399
  throw commandError("schtasks /Create", create);
400
+ const warnings = [];
401
+ let notice = legacyLeftNotice(legacy);
402
+ if (legacy.state === "mine") {
403
+ // Only the current user's shared task is moved; another user's is never ended or deleted.
404
+ run("schtasks.exe", ["/End", "/TN", LEGACY_DAEMON_WINDOWS_TASK]);
405
+ const del = run("schtasks.exe", ["/Delete", "/TN", LEGACY_DAEMON_WINDOWS_TASK, "/F"]);
406
+ if (del.status !== 0) {
407
+ warnings.push(`Could not remove the shared task \\${LEGACY_DAEMON_WINDOWS_TASK}: ${(del.stderr.trim() || del.stdout.trim() || `exit ${del.status}`).replace(/\0/g, "")}. ` +
408
+ `Remove it with: ${REMOVE_LEGACY_TASK}`);
409
+ }
410
+ notice = `Moved your HQ daemon from the shared task \\${LEGACY_DAEMON_WINDOWS_TASK} to \\${task}.`;
411
+ }
271
412
  // `/Create /F` leaves a running instance alone and `/Run` is ignored
272
413
  // while one runs, so a daemon from an earlier install would keep the old
273
414
  // settings. Stop it first so `/Run` starts a new one.
274
- const warning = previous ? endWindowsDaemon(run, previous) : undefined;
275
- const start = run("schtasks.exe", ["/Run", "/TN", DAEMON_WINDOWS_TASK]);
415
+ const old = previous ? endWindowsDaemon(run, previous, [task]) : undefined;
416
+ if (old)
417
+ warnings.push(old);
418
+ const start = run("schtasks.exe", ["/Run", "/TN", task]);
276
419
  if (start.status !== 0)
277
420
  throw commandError("schtasks /Run", start);
278
- return warning;
421
+ return { ...(warnings.length ? { warning: warnings.join("\n") } : {}), ...(notice ? { notice } : {}) };
279
422
  }
280
423
  case "manual":
281
- return undefined;
424
+ return {};
282
425
  }
283
426
  }
284
427
  export function installDaemonUnit(host, options, deps) {
@@ -330,8 +473,7 @@ export function installDaemonUnit(host, options, deps) {
330
473
  const lockPath = daemonPaths(host.home, { HQ_DAEMON_DIR: host.daemonDir }).lockPath;
331
474
  const runningDaemon = deps.runningDaemon ?? (() => liveDaemonLock(lockPath));
332
475
  const previous = plan.kind === "schtasks" ? runningDaemon() : null;
333
- const warning = plan.kind !== "launchd" ? activate(plan, host, deps.run, deps.sleep, previous) : undefined;
334
- const warn = warning ? { warning } : {};
476
+ const warn = plan.kind !== "launchd" ? activate(plan, host, deps.run, deps.sleep, previous) : {};
335
477
  if (plan.kind === "schtasks") {
336
478
  // schtasks /Run exits 0 even when the task cannot run: an
337
479
  // InteractiveToken task with no signed-in session stays at "has not yet
@@ -478,11 +620,22 @@ export function uninstallDaemonUnit(host, options, deps) {
478
620
  : planDaemonUnit(host, options);
479
621
  if (plan.kind === "manual")
480
622
  return { kind: plan.kind, unitPath: plan.unitPath, removed: false };
481
- // A registered task is removed even when its XML file has gone missing.
482
- const present = fs.existsSync(plan.unitPath) ||
483
- (plan.kind === "schtasks" && deps.run("schtasks.exe", ["/Query", "/TN", DAEMON_WINDOWS_TASK]).status === 0);
484
- if (!present)
485
- return { kind: plan.kind, unitPath: plan.unitPath, removed: false };
623
+ // On Windows the XML file is shared with older installs, so it does not show
624
+ // that this user's per-user task exists. Ask schtasks for each name this
625
+ // user's task may have: the SID name, and the user-name form used when the
626
+ // SID could not be read at install time.
627
+ const userTasks = plan.kind === "schtasks" ? windowsUserTaskNames(deps.run, host) : [];
628
+ const present = plan.kind === "schtasks" ? userTasks.length > 0 : fs.existsSync(plan.unitPath);
629
+ // The current user's shared task from an older install goes too; another user's stays.
630
+ const legacy = plan.kind === "schtasks" ? legacyWindowsTask(deps.run, host) : { state: "none" };
631
+ const notice = legacyLeftNotice(legacy);
632
+ const noted = notice ? { notice } : {};
633
+ if (!present && legacy.state !== "mine") {
634
+ // A task XML left by an older install with no task behind it is stale.
635
+ if (plan.kind === "schtasks")
636
+ fs.rmSync(plan.unitPath, { force: true });
637
+ return { kind: plan.kind, unitPath: plan.unitPath, removed: false, ...noted };
638
+ }
486
639
  let warning;
487
640
  switch (plan.kind) {
488
641
  case "launchd":
@@ -503,15 +656,20 @@ export function uninstallDaemonUnit(host, options, deps) {
503
656
  }
504
657
  case "schtasks": {
505
658
  // The lock may be stale; endWindowsDaemon kills only a verified daemon.
506
- warning = endWindowsDaemon(deps.run, readPidLock(path.join(host.daemonDir, "daemon.pid")));
507
- const del = deps.run("schtasks.exe", ["/Delete", "/TN", DAEMON_WINDOWS_TASK, "/F"]);
508
- // Keep the XML so a later uninstall still finds the task.
509
- if (del.status !== 0)
510
- throw commandError(`schtasks /Delete /TN ${DAEMON_WINDOWS_TASK}`, del);
659
+ const tasks = [...(legacy.state === "mine" ? [LEGACY_DAEMON_WINDOWS_TASK] : []), ...userTasks];
660
+ warning = endWindowsDaemon(deps.run, readPidLock(path.join(host.daemonDir, "daemon.pid")), tasks);
661
+ for (const name of tasks) {
662
+ const del = deps.run("schtasks.exe", ["/Delete", "/TN", name, "/F"]);
663
+ // A task that is already gone counts as removed. Otherwise keep the
664
+ // XML so a later uninstall still finds the task.
665
+ if (del.status !== 0 && deps.run("schtasks.exe", ["/Query", "/TN", name]).status === 0) {
666
+ throw commandError(`schtasks /Delete /TN ${name}`, del);
667
+ }
668
+ }
511
669
  break;
512
670
  }
513
671
  }
514
672
  fs.rmSync(plan.unitPath, { force: true });
515
- return { kind: plan.kind, unitPath: plan.unitPath, removed: true, ...(warning ? { warning } : {}) };
673
+ return { kind: plan.kind, unitPath: plan.unitPath, removed: true, ...(warning ? { warning } : {}), ...noted };
516
674
  }
517
675
  //# sourceMappingURL=install.js.map
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Detection reads unit files and checks for an orphaned loaded macOS mesh job
7
7
  * whose plist was removed. Windows scheduled tasks are probed through
8
- * schtasks. Nothing here stops or removes a unit.
8
+ * schtasks, and only a task that runs as the current user can be taken over.
9
+ * Nothing here stops or removes a unit.
9
10
  */
10
11
  import type { ServiceName } from "./catalog.js";
11
12
  export interface LegacyUnit {
@@ -15,9 +16,13 @@ export interface LegacyUnit {
15
16
  kind: "launchd" | "systemd-user" | "systemd-system" | "schtasks";
16
17
  name: string;
17
18
  path?: string;
18
- /** False for units owned by another product (the desktop app's own agent). */
19
+ /** False for units owned by another product (the desktop app's own agent) or another Windows user. */
19
20
  migratable: boolean;
21
+ /** Why a unit that is not the desktop app's is still left alone (another Windows user's task). */
22
+ notMineReason?: string;
20
23
  }
24
+ /** Whose Windows Scheduled Task a name refers to, as classifyWindowsTask reads it. */
25
+ export type WindowsTaskOwner = "none" | "mine" | "other" | "unreadable";
21
26
  export interface LegacyProbeDeps {
22
27
  platform: NodeJS.Platform;
23
28
  home: string;
@@ -25,7 +30,8 @@ export interface LegacyProbeDeps {
25
30
  listDir: (dir: string) => string[];
26
31
  /** True when launchctl can print the GUI-domain job despite a missing plist. */
27
32
  launchdJobLoaded: (label: string) => boolean;
28
- windowsTaskExists: (taskName: string) => boolean;
33
+ /** Whether the task exists and runs as the current user. Another user's task is never taken over. */
34
+ windowsTaskOwner: (taskName: string) => WindowsTaskOwner;
29
35
  }
30
36
  export declare function describeLegacyUnit(unit: LegacyUnit): string;
31
37
  export declare function findLegacyUnits(deps: LegacyProbeDeps): LegacyUnit[];
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Detection reads unit files and checks for an orphaned loaded macOS mesh job
7
7
  * whose plist was removed. Windows scheduled tasks are probed through
8
- * schtasks. Nothing here stops or removes a unit.
8
+ * schtasks, and only a task that runs as the current user can be taken over.
9
+ * Nothing here stops or removes a unit.
9
10
  */
10
11
  import * as path from "node:path";
11
12
  const KIT_SERVICES = ["sync", "inbox", "heartbeat"];
@@ -100,9 +101,15 @@ function linuxUnits(deps) {
100
101
  }
101
102
  function windowsUnits(deps) {
102
103
  const task = "ai.getindigo.hq-mesh-daemon";
103
- return deps.windowsTaskExists(task)
104
- ? [{ unitId: "mesh", service: "mesh", kind: "schtasks", name: task, migratable: true }]
105
- : [];
104
+ const owner = deps.windowsTaskOwner(task);
105
+ if (owner === "none")
106
+ return [];
107
+ const unit = { unitId: "mesh", service: "mesh", kind: "schtasks", name: task, migratable: owner === "mine" };
108
+ if (owner === "other")
109
+ unit.notMineReason = "it belongs to another Windows user";
110
+ if (owner === "unreadable")
111
+ unit.notMineReason = "HQ could not read which Windows user it belongs to";
112
+ return [unit];
106
113
  }
107
114
  export function findLegacyUnits(deps) {
108
115
  if (deps.platform === "darwin")
@@ -136,7 +136,7 @@ export function adoptLegacyUnits(units, deps) {
136
136
  };
137
137
  for (const unit of units) {
138
138
  if (!unit.migratable) {
139
- result.skipped.push({ unit, reason: "owned by the HQ desktop app" });
139
+ result.skipped.push({ unit, reason: unit.notMineReason ?? "owned by the HQ desktop app" });
140
140
  continue;
141
141
  }
142
142
  if (unit.kind === "systemd-system" && !deps.isRoot) {
@@ -6,7 +6,8 @@
6
6
  * signing in takes effect without restarting the daemon.
7
7
  */
8
8
  import { type PlanContext, type ServiceName, type UnitPlan } from "./catalog.js";
9
- import { type LegacyUnit } from "./legacy.js";
9
+ import { type SyncCoreVaultPlan } from "../sync-core/engine-home.js";
10
+ import { type LegacyUnit, type WindowsTaskOwner } from "./legacy.js";
10
11
  import { type DaemonLockResult } from "./lock.js";
11
12
  import { type MachineType } from "./machine-type.js";
12
13
  import { type PickedHqCloud } from "./hq-cloud.js";
@@ -14,6 +15,7 @@ import { type HostApp, type Supervisor } from "./supervisor.js";
14
15
  export declare const LOG_ROTATE_BYTES: number;
15
16
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
16
17
  export declare const RESTART_EXIT_CODE = 75;
18
+ export declare function syncCoreVaultPlans(home: string, hqRoot: string | undefined, platform: NodeJS.Platform): SyncCoreVaultPlan[];
17
19
  export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey" | "state"> | null | undefined): UnitPlan[];
18
20
  /** A bounded shutdown deadline that can account for sync push persistence. */
19
21
  export declare function createShutdownCap(): {
@@ -99,7 +101,8 @@ export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configu
99
101
  * background service, so it is not one of them.
100
102
  */
101
103
  export declare function listLocalBots(root?: string): string[];
102
- export declare function windowsTaskExists(taskName: string): boolean;
104
+ /** Whose task `taskName` is: only the current user's own task may be taken over. */
105
+ export declare function windowsTaskOwner(taskName: string): WindowsTaskOwner;
103
106
  export declare function probeLegacyUnits(platform: NodeJS.Platform, home: string): LegacyUnit[];
104
107
  /** Absolute path of `name` on PATH, or null. */
105
108
  export declare function findOnPath(name: string, env?: NodeJS.ProcessEnv): string | null;