@ouro.bot/cli 0.1.0-alpha.551 → 0.1.0-alpha.553

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/changelog.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.553",
6
+ "changes": [
7
+ "Daemon startup on macOS now starts through launchd before opening the daemon socket when the LaunchAgent is available, preventing `ouro up` from leaving both a manual daemon and a launchd daemon alive after current-session bootstrap."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.552",
12
+ "changes": [
13
+ "`ouro up` now bootstraps the daemon LaunchAgent into the current login session when it is not already loaded, waits for the launchd-owned daemon socket to settle, and keeps the launchd adoption non-fatal if macOS refuses the bootstrap."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.551",
6
18
  "changes": [
@@ -41,6 +41,8 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.defaultStartDaemonProcess = defaultStartDaemonProcess;
43
43
  exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
44
+ exports.isDaemonLaunchAgentLoaded = isDaemonLaunchAgentLoaded;
45
+ exports.waitForBootstrappedDaemonSocket = waitForBootstrappedDaemonSocket;
44
46
  exports.defaultListDiscoveredAgents = defaultListDiscoveredAgents;
45
47
  exports.defaultRunSerpentGuide = defaultRunSerpentGuide;
46
48
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
@@ -75,7 +77,10 @@ const cli_parse_1 = require("./cli-parse");
75
77
  const provider_discovery_1 = require("./provider-discovery");
76
78
  const provider_credentials_1 = require("../provider-credentials");
77
79
  // ── Default implementations ──
78
- function defaultStartDaemonProcess(socketPath) {
80
+ async function defaultStartDaemonProcess(socketPath) {
81
+ const launchdStarted = await startDaemonProcessViaLaunchd(socketPath);
82
+ if (launchdStarted)
83
+ return launchdStarted;
79
84
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
80
85
  // Redirect stdio to /dev/null via file descriptors — using 'ignore' causes EPIPE
81
86
  // when the daemon's logging system writes to stderr after the parent exits.
@@ -87,7 +92,7 @@ function defaultStartDaemonProcess(socketPath) {
87
92
  });
88
93
  child.unref();
89
94
  // Don't close fds — the child process needs them. They'll be cleaned up when the parent exits.
90
- return Promise.resolve({ pid: child.pid ?? null });
95
+ return { pid: child.pid ?? null };
91
96
  }
92
97
  function defaultWriteStdout(text) {
93
98
  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
@@ -216,10 +221,13 @@ function defaultFallbackPendingMessage(command) {
216
221
  });
217
222
  return pendingPath;
218
223
  }
219
- function defaultEnsureDaemonBootPersistence(socketPath) {
220
- if (process.platform !== "darwin") {
221
- return;
222
- }
224
+ function currentUserUid() {
225
+ return process.getuid?.() ?? 0;
226
+ }
227
+ function launchAgentDomain(userUid = currentUserUid()) {
228
+ return `gui/${userUid}`;
229
+ }
230
+ function writeDaemonBootPlist(socketPath) {
223
231
  const homeDir = os.homedir();
224
232
  const writeDeps = {
225
233
  writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
@@ -237,20 +245,123 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
237
245
  meta: { entryPath },
238
246
  });
239
247
  }
240
- const logDir = (0, identity_1.getAgentDaemonLogsDir)();
241
- // Write plist only — do NOT launchctl bootstrap.
242
- // The daemon is already running (started by ouro up). Bootstrapping would
243
- // start a SECOND daemon via launchd's RunAtLoad, causing a race where
244
- // killOrphanProcesses kills the first daemon and both end up dead.
245
- // The plist on disk is sufficient: launchd picks it up on login.
246
- (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
248
+ return (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
247
249
  nodePath: process.execPath,
248
250
  entryPath,
249
251
  socketPath,
250
- logDir,
252
+ logDir: (0, identity_1.getAgentDaemonLogsDir)(),
251
253
  envPath: process.env.PATH,
252
254
  });
253
255
  }
256
+ function isDaemonLaunchAgentLoaded(deps) {
257
+ const userUid = deps?.userUid ?? currentUserUid();
258
+ const exec = deps?.exec ?? ((cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); });
259
+ try {
260
+ exec(`launchctl print ${launchAgentDomain(userUid)}/${launchd_1.DAEMON_PLIST_LABEL}`);
261
+ return true;
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ }
267
+ function readDaemonLaunchAgentPid() {
268
+ try {
269
+ const output = (0, child_process_1.execSync)(`launchctl print ${launchAgentDomain()}/${launchd_1.DAEMON_PLIST_LABEL}`, { encoding: "utf-8" });
270
+ const match = output.match(/^\s*pid = (\d+)/m);
271
+ return match ? Number(match[1]) : null;
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ async function waitForBootstrappedDaemonSocket(socketPath, deps = {}) {
278
+ const checkSocketAlive = deps.checkSocketAlive ?? socket_client_1.checkDaemonSocketAlive;
279
+ const now = deps.now ?? Date.now;
280
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
281
+ const timeoutMs = deps.timeoutMs ?? 30_000;
282
+ const initialSettleMs = deps.initialSettleMs ?? 1_000;
283
+ const pollIntervalMs = deps.pollIntervalMs ?? 250;
284
+ const requiredConsecutiveAliveChecks = deps.requiredConsecutiveAliveChecks ?? 2;
285
+ const deadline = now() + timeoutMs;
286
+ const firstTrustworthyCheckAt = now() + initialSettleMs;
287
+ let consecutiveAliveChecks = 0;
288
+ while (now() < deadline) {
289
+ await sleep(pollIntervalMs);
290
+ if (now() < firstTrustworthyCheckAt)
291
+ continue;
292
+ if (await checkSocketAlive(socketPath)) {
293
+ consecutiveAliveChecks += 1;
294
+ if (consecutiveAliveChecks >= requiredConsecutiveAliveChecks)
295
+ return;
296
+ }
297
+ else {
298
+ consecutiveAliveChecks = 0;
299
+ }
300
+ }
301
+ (0, runtime_1.emitNervesEvent)({
302
+ level: "warn",
303
+ component: "daemon",
304
+ event: "daemon.launchd_bootstrap_socket_wait_timeout",
305
+ message: "launchd bootstrap finished but daemon socket did not settle before timeout",
306
+ meta: { socketPath },
307
+ });
308
+ }
309
+ async function startDaemonProcessViaLaunchd(socketPath) {
310
+ if (process.platform !== "darwin") {
311
+ return null;
312
+ }
313
+ const plistPath = writeDaemonBootPlist(socketPath);
314
+ const userUid = currentUserUid();
315
+ const domain = launchAgentDomain(userUid);
316
+ try {
317
+ (0, runtime_1.emitNervesEvent)({
318
+ component: "daemon",
319
+ event: "daemon.launchd_bootstrap_start",
320
+ message: "starting daemon launch agent for current login session",
321
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
322
+ });
323
+ if (isDaemonLaunchAgentLoaded({ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); }, userUid })) {
324
+ (0, child_process_1.execSync)(`launchctl kickstart -k ${domain}/${launchd_1.DAEMON_PLIST_LABEL}`, { stdio: "ignore" });
325
+ }
326
+ else {
327
+ (0, child_process_1.execSync)(`launchctl bootstrap ${domain} "${plistPath}"`, { stdio: "ignore" });
328
+ }
329
+ (0, runtime_1.emitNervesEvent)({
330
+ component: "daemon",
331
+ event: "daemon.launchd_bootstrap_end",
332
+ message: "daemon launch agent started for current login session",
333
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
334
+ });
335
+ await waitForBootstrappedDaemonSocket(socketPath);
336
+ return { pid: readDaemonLaunchAgentPid() };
337
+ }
338
+ catch (error) {
339
+ (0, runtime_1.emitNervesEvent)({
340
+ level: "warn",
341
+ component: "daemon",
342
+ event: "daemon.launchd_bootstrap_error",
343
+ message: "failed to start daemon launch agent for current login session",
344
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL, error: error instanceof Error ? error.message : String(error) },
345
+ });
346
+ return null;
347
+ }
348
+ }
349
+ async function defaultEnsureDaemonBootPersistence(socketPath) {
350
+ if (process.platform !== "darwin") {
351
+ return;
352
+ }
353
+ const plistPath = writeDaemonBootPlist(socketPath);
354
+ const userUid = currentUserUid();
355
+ if (isDaemonLaunchAgentLoaded({ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); }, userUid })) {
356
+ (0, runtime_1.emitNervesEvent)({
357
+ component: "daemon",
358
+ event: "daemon.launchd_bootstrap_skipped_loaded",
359
+ message: "daemon launch agent already loaded",
360
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
361
+ });
362
+ return;
363
+ }
364
+ }
254
365
  function defaultPrepareDaemonRuntimeReplacement() {
255
366
  if (process.platform !== "darwin") {
256
367
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.551",
3
+ "version": "0.1.0-alpha.553",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",