happy-coder 0.9.0-6 → 0.9.0

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/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, c as configuration, f as encodeBase64, g as encodeBase64Url, h as decodeBase64, A as ApiClient } from './types-DJOX-XG-.mjs';
2
+ import os$1, { homedir } from 'node:os';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
+ import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState, c as configuration, f as clearDaemonState, p as packageJson, g as readSettings, h as readCredentials, i as encodeBase64, u as updateSettings, j as encodeBase64Url, k as decodeBase64, w as writeCredentials, m as acquireDaemonLock, n as writeDaemonState, A as ApiClient, o as releaseDaemonLock, q as clearCredentials, s as clearMachineId, t as getLatestDaemonLog } from './types-Cezp_n6O.mjs';
4
5
  import { spawn, execSync } from 'node:child_process';
5
6
  import { resolve, join, dirname as dirname$1 } from 'node:path';
6
7
  import { createInterface } from 'node:readline';
7
8
  import { fileURLToPath as fileURLToPath$1 } from 'node:url';
8
- import { existsSync, readFileSync, mkdirSync, watch, constants, readdirSync, statSync, rmSync } from 'node:fs';
9
- import os$1, { homedir } from 'node:os';
9
+ import { existsSync, readFileSync, mkdirSync, watch, readdirSync, statSync, rmSync } from 'node:fs';
10
10
  import { dirname, resolve as resolve$1, join as join$1 } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
- import { readFile, unlink, mkdir, writeFile as writeFile$1, open, stat as stat$1, rename } from 'node:fs/promises';
13
- import { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
12
+ import { readFile } from 'node:fs/promises';
13
+ import fs, { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
14
14
  import { useStdout, useInput, Box, Text, render } from 'ink';
15
15
  import React, { useState, useRef, useEffect, useCallback } from 'react';
16
16
  import axios from 'axios';
@@ -21,17 +21,16 @@ import 'expo-server-sdk';
21
21
  import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
22
22
  import { promisify } from 'util';
23
23
  import { createHash } from 'crypto';
24
- import * as z from 'zod';
25
- import { z as z$1 } from 'zod';
26
- import fastify from 'fastify';
27
- import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
28
24
  import os from 'os';
29
25
  import qrcode from 'qrcode-terminal';
30
- import open$1 from 'open';
26
+ import open from 'open';
27
+ import fastify from 'fastify';
28
+ import { z } from 'zod';
29
+ import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
30
+ import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
31
31
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
32
32
  import { createServer } from 'node:http';
33
33
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
34
- import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
35
34
 
36
35
  class Session {
37
36
  path;
@@ -2698,7 +2697,7 @@ async function claudeRemoteLauncher(session) {
2698
2697
  }
2699
2698
 
2700
2699
  async function loop(opts) {
2701
- const logPath = await logger.logFilePathPromise;
2700
+ const logPath = logger.logFilePath;
2702
2701
  let session = new Session({
2703
2702
  api: opts.api,
2704
2703
  client: opts.session,
@@ -2743,133 +2742,6 @@ async function loop(opts) {
2743
2742
  }
2744
2743
  }
2745
2744
 
2746
- var name = "happy-coder";
2747
- var version = "0.9.0-6";
2748
- var description = "Claude Code session sharing CLI";
2749
- var author = "Kirill Dubovitskiy";
2750
- var license = "MIT";
2751
- var type = "module";
2752
- var homepage = "https://github.com/slopus/happy-cli";
2753
- var bugs = "https://github.com/slopus/happy-cli/issues";
2754
- var repository = "slopus/happy-cli";
2755
- var bin = {
2756
- happy: "./bin/happy.mjs"
2757
- };
2758
- var main = "./dist/index.cjs";
2759
- var module = "./dist/index.mjs";
2760
- var types = "./dist/index.d.cts";
2761
- var exports = {
2762
- ".": {
2763
- require: {
2764
- types: "./dist/index.d.cts",
2765
- "default": "./dist/index.cjs"
2766
- },
2767
- "import": {
2768
- types: "./dist/index.d.mts",
2769
- "default": "./dist/index.mjs"
2770
- }
2771
- },
2772
- "./lib": {
2773
- require: {
2774
- types: "./dist/lib.d.cts",
2775
- "default": "./dist/lib.cjs"
2776
- },
2777
- "import": {
2778
- types: "./dist/lib.d.mts",
2779
- "default": "./dist/lib.mjs"
2780
- }
2781
- }
2782
- };
2783
- var files = [
2784
- "dist",
2785
- "bin",
2786
- "scripts",
2787
- "ripgrep",
2788
- "package.json"
2789
- ];
2790
- var scripts = {
2791
- "why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
2792
- typecheck: "tsc --noEmit",
2793
- build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
2794
- test: "yarn build && vitest run",
2795
- "test:watch": "vitest",
2796
- "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
2797
- dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
2798
- "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2799
- "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2800
- prepublishOnly: "yarn build && yarn test",
2801
- release: "release-it"
2802
- };
2803
- var dependencies = {
2804
- "@anthropic-ai/claude-code": "^1.0.89",
2805
- "@anthropic-ai/sdk": "^0.56.0",
2806
- "@modelcontextprotocol/sdk": "^1.15.1",
2807
- "@stablelib/base64": "^2.0.1",
2808
- "@types/http-proxy": "^1.17.16",
2809
- "@types/qrcode-terminal": "^0.12.2",
2810
- "@types/react": "^19.1.9",
2811
- axios: "^1.10.0",
2812
- chalk: "^5.4.1",
2813
- "expo-server-sdk": "^3.15.0",
2814
- fastify: "^5.5.0",
2815
- "fastify-type-provider-zod": "4.0.2",
2816
- "http-proxy": "^1.18.1",
2817
- "http-proxy-middleware": "^3.0.5",
2818
- ink: "^6.1.0",
2819
- open: "^10.2.0",
2820
- "qrcode-terminal": "^0.12.0",
2821
- react: "^19.1.1",
2822
- "socket.io-client": "^4.8.1",
2823
- tweetnacl: "^1.0.3",
2824
- zod: "^3.23.8"
2825
- };
2826
- var devDependencies = {
2827
- "@eslint/compat": "^1",
2828
- "@types/node": ">=20",
2829
- "cross-env": "^10.0.0",
2830
- eslint: "^9",
2831
- "eslint-config-prettier": "^10",
2832
- pkgroll: "^2.14.2",
2833
- "release-it": "^19.0.4",
2834
- shx: "^0.3.3",
2835
- "ts-node": "^10",
2836
- tsx: "^4.20.3",
2837
- typescript: "^5",
2838
- vitest: "^3.2.4"
2839
- };
2840
- var resolutions = {
2841
- "whatwg-url": "14.2.0",
2842
- "parse-path": "7.0.3",
2843
- "@types/parse-path": "7.0.3"
2844
- };
2845
- var publishConfig = {
2846
- registry: "https://registry.npmjs.org"
2847
- };
2848
- var packageManager = "yarn@1.22.22";
2849
- var packageJson = {
2850
- name: name,
2851
- version: version,
2852
- description: description,
2853
- author: author,
2854
- license: license,
2855
- type: type,
2856
- homepage: homepage,
2857
- bugs: bugs,
2858
- repository: repository,
2859
- bin: bin,
2860
- main: main,
2861
- module: module,
2862
- types: types,
2863
- exports: exports,
2864
- files: files,
2865
- scripts: scripts,
2866
- dependencies: dependencies,
2867
- devDependencies: devDependencies,
2868
- resolutions: resolutions,
2869
- publishConfig: publishConfig,
2870
- packageManager: packageManager
2871
- };
2872
-
2873
2745
  function run(args, options) {
2874
2746
  const RUNNER_PATH = resolve$1(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
2875
2747
  return new Promise((resolve2, reject) => {
@@ -3129,129 +3001,6 @@ function registerHandlers(session) {
3129
3001
  });
3130
3002
  }
3131
3003
 
3132
- const defaultSettings = {
3133
- onboardingCompleted: false
3134
- };
3135
- async function readSettings() {
3136
- if (!existsSync(configuration.settingsFile)) {
3137
- return { ...defaultSettings };
3138
- }
3139
- try {
3140
- const content = await readFile(configuration.settingsFile, "utf8");
3141
- return JSON.parse(content);
3142
- } catch {
3143
- return { ...defaultSettings };
3144
- }
3145
- }
3146
- async function updateSettings(updater) {
3147
- const LOCK_RETRY_INTERVAL_MS = 100;
3148
- const MAX_LOCK_ATTEMPTS = 50;
3149
- const STALE_LOCK_TIMEOUT_MS = 1e4;
3150
- const lockFile = configuration.settingsFile + ".lock";
3151
- const tmpFile = configuration.settingsFile + ".tmp";
3152
- let fileHandle;
3153
- let attempts = 0;
3154
- while (attempts < MAX_LOCK_ATTEMPTS) {
3155
- try {
3156
- fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
3157
- break;
3158
- } catch (err) {
3159
- if (err.code === "EEXIST") {
3160
- attempts++;
3161
- await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
3162
- try {
3163
- const stats = await stat$1(lockFile);
3164
- if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
3165
- await unlink(lockFile).catch(() => {
3166
- });
3167
- }
3168
- } catch {
3169
- }
3170
- } else {
3171
- throw err;
3172
- }
3173
- }
3174
- }
3175
- if (!fileHandle) {
3176
- throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
3177
- }
3178
- try {
3179
- const current = await readSettings() || { ...defaultSettings };
3180
- const updated = await updater(current);
3181
- if (!existsSync(configuration.happyHomeDir)) {
3182
- await mkdir(configuration.happyHomeDir, { recursive: true });
3183
- }
3184
- await writeFile$1(tmpFile, JSON.stringify(updated, null, 2));
3185
- await rename(tmpFile, configuration.settingsFile);
3186
- return updated;
3187
- } finally {
3188
- await fileHandle.close();
3189
- await unlink(lockFile).catch(() => {
3190
- });
3191
- }
3192
- }
3193
- const credentialsSchema = z.object({
3194
- secret: z.string().base64(),
3195
- token: z.string()
3196
- });
3197
- async function readCredentials() {
3198
- if (!existsSync(configuration.privateKeyFile)) {
3199
- return null;
3200
- }
3201
- try {
3202
- const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
3203
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
3204
- return {
3205
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
3206
- token: credentials.token
3207
- };
3208
- } catch {
3209
- return null;
3210
- }
3211
- }
3212
- async function writeCredentials(credentials) {
3213
- if (!existsSync(configuration.happyHomeDir)) {
3214
- await mkdir(configuration.happyHomeDir, { recursive: true });
3215
- }
3216
- await writeFile$1(configuration.privateKeyFile, JSON.stringify({
3217
- secret: encodeBase64(credentials.secret),
3218
- token: credentials.token
3219
- }, null, 2));
3220
- }
3221
- async function clearCredentials() {
3222
- if (existsSync(configuration.privateKeyFile)) {
3223
- await unlink(configuration.privateKeyFile);
3224
- }
3225
- }
3226
- async function clearMachineId() {
3227
- await updateSettings((settings) => ({
3228
- ...settings,
3229
- machineId: void 0
3230
- }));
3231
- }
3232
- async function readDaemonState() {
3233
- try {
3234
- if (!existsSync(configuration.daemonStateFile)) {
3235
- return null;
3236
- }
3237
- const content = await readFile(configuration.daemonStateFile, "utf-8");
3238
- return JSON.parse(content);
3239
- } catch (error) {
3240
- return null;
3241
- }
3242
- }
3243
- async function writeDaemonState(state) {
3244
- if (!existsSync(configuration.happyHomeDir)) {
3245
- await mkdir(configuration.happyHomeDir, { recursive: true });
3246
- }
3247
- await writeFile$1(configuration.daemonStateFile, JSON.stringify(state, null, 2));
3248
- }
3249
- async function clearDaemonState() {
3250
- if (existsSync(configuration.daemonStateFile)) {
3251
- await unlink(configuration.daemonStateFile);
3252
- }
3253
- }
3254
-
3255
3004
  class MessageQueue2 {
3256
3005
  queue = [];
3257
3006
  // Made public for testing
@@ -3619,8 +3368,9 @@ function startCaffeinate() {
3619
3368
  }
3620
3369
  }
3621
3370
  let isStopping = false;
3622
- function stopCaffeinate() {
3371
+ async function stopCaffeinate() {
3623
3372
  if (isStopping) {
3373
+ logger.debug("[caffeinate] Already stopping, skipping");
3624
3374
  return;
3625
3375
  }
3626
3376
  if (caffeinateProcess && !caffeinateProcess.killed) {
@@ -3628,14 +3378,13 @@ function stopCaffeinate() {
3628
3378
  logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
3629
3379
  try {
3630
3380
  caffeinateProcess.kill("SIGTERM");
3631
- setTimeout(() => {
3632
- if (caffeinateProcess && !caffeinateProcess.killed) {
3633
- logger.debug("[caffeinate] Force killing caffeinate process");
3634
- caffeinateProcess.kill("SIGKILL");
3635
- }
3636
- caffeinateProcess = null;
3637
- isStopping = false;
3638
- }, 1e3);
3381
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3382
+ if (caffeinateProcess && !caffeinateProcess.killed) {
3383
+ logger.debug("[caffeinate] Force killing caffeinate process");
3384
+ caffeinateProcess.kill("SIGKILL");
3385
+ }
3386
+ caffeinateProcess = null;
3387
+ isStopping = false;
3639
3388
  } catch (error) {
3640
3389
  logger.debug("[caffeinate] Error stopping caffeinate:", error);
3641
3390
  isStopping = false;
@@ -3659,12 +3408,10 @@ function setupCleanupHandlers() {
3659
3408
  process.on("uncaughtException", (error) => {
3660
3409
  logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
3661
3410
  cleanup();
3662
- process.exit(1);
3663
3411
  });
3664
3412
  process.on("unhandledRejection", (reason, promise) => {
3665
3413
  logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
3666
3414
  cleanup();
3667
- process.exit(1);
3668
3415
  });
3669
3416
  }
3670
3417
 
@@ -3713,37 +3460,97 @@ function extractSDKMetadataAsync(onComplete) {
3713
3460
  });
3714
3461
  }
3715
3462
 
3716
- async function isDaemonRunning() {
3463
+ async function daemonPost(path, body) {
3464
+ const state = await readDaemonState();
3465
+ if (!state?.httpPort) {
3466
+ const errorMessage = "No daemon running, no state file found";
3467
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
3468
+ return {
3469
+ error: errorMessage
3470
+ };
3471
+ }
3717
3472
  try {
3718
- const state = await getDaemonState();
3719
- if (!state) {
3720
- return false;
3721
- }
3722
- const isRunning = await isDaemonProcessRunning(state.pid);
3723
- if (!isRunning) {
3724
- logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
3725
- await cleanupDaemonState();
3726
- return false;
3727
- }
3728
- return true;
3473
+ process.kill(state.pid, 0);
3729
3474
  } catch (error) {
3730
- logger.debug("[DAEMON RUN] Error checking daemon status", error);
3731
- return false;
3475
+ const errorMessage = "Daemon is not running, file is stale";
3476
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
3477
+ return {
3478
+ error: errorMessage
3479
+ };
3732
3480
  }
3733
- }
3734
- async function getDaemonState() {
3735
3481
  try {
3736
- return await readDaemonState();
3482
+ const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
3483
+ const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
3484
+ method: "POST",
3485
+ headers: { "Content-Type": "application/json" },
3486
+ body: JSON.stringify(body || {}),
3487
+ // Mostly increased for stress test
3488
+ signal: AbortSignal.timeout(timeout)
3489
+ });
3490
+ if (!response.ok) {
3491
+ const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
3492
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
3493
+ return {
3494
+ error: errorMessage
3495
+ };
3496
+ }
3497
+ return await response.json();
3737
3498
  } catch (error) {
3738
- logger.debug("[DAEMON RUN] Error reading daemon metadata", error);
3739
- return null;
3499
+ const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
3500
+ logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
3501
+ return {
3502
+ error: errorMessage
3503
+ };
3740
3504
  }
3741
3505
  }
3742
- async function isDaemonProcessRunning(pid) {
3506
+ async function notifyDaemonSessionStarted(sessionId, metadata) {
3507
+ return await daemonPost("/session-started", {
3508
+ sessionId,
3509
+ metadata
3510
+ });
3511
+ }
3512
+ async function listDaemonSessions() {
3513
+ const result = await daemonPost("/list");
3514
+ return result.children || [];
3515
+ }
3516
+ async function stopDaemonSession(sessionId) {
3517
+ const result = await daemonPost("/stop-session", { sessionId });
3518
+ return result.success || false;
3519
+ }
3520
+ async function stopDaemonHttp() {
3521
+ await daemonPost("/stop");
3522
+ }
3523
+ async function checkIfDaemonRunningAndCleanupStaleState() {
3524
+ const state = await readDaemonState();
3525
+ if (!state) {
3526
+ return false;
3527
+ }
3743
3528
  try {
3744
- process.kill(pid, 0);
3529
+ process.kill(state.pid, 0);
3745
3530
  return true;
3746
3531
  } catch {
3532
+ logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
3533
+ await cleanupDaemonState();
3534
+ return false;
3535
+ }
3536
+ }
3537
+ async function isDaemonRunningSameVersion() {
3538
+ logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
3539
+ const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
3540
+ if (!runningDaemon) {
3541
+ logger.debug("[DAEMON CONTROL] No daemon running, returning false");
3542
+ return false;
3543
+ }
3544
+ const state = await readDaemonState();
3545
+ if (!state) {
3546
+ logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
3547
+ return false;
3548
+ }
3549
+ try {
3550
+ logger.debug(`[DAEMON CONTROL] Current CLI version: ${configuration.currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
3551
+ return configuration.currentCliVersion === state.startedWithCliVersion;
3552
+ } catch (error) {
3553
+ logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
3747
3554
  return false;
3748
3555
  }
3749
3556
  }
@@ -3755,47 +3562,102 @@ async function cleanupDaemonState() {
3755
3562
  logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
3756
3563
  }
3757
3564
  }
3565
+ async function stopDaemon() {
3566
+ try {
3567
+ const state = await readDaemonState();
3568
+ if (!state) {
3569
+ logger.debug("No daemon state found");
3570
+ return;
3571
+ }
3572
+ logger.debug(`Stopping daemon with PID ${state.pid}`);
3573
+ try {
3574
+ await stopDaemonHttp();
3575
+ await waitForProcessDeath(state.pid, 2e3);
3576
+ logger.debug("Daemon stopped gracefully via HTTP");
3577
+ return;
3578
+ } catch (error) {
3579
+ logger.debug("HTTP stop failed, will force kill", error);
3580
+ }
3581
+ try {
3582
+ process.kill(state.pid, "SIGKILL");
3583
+ logger.debug("Force killed daemon");
3584
+ } catch (error) {
3585
+ logger.debug("Daemon already dead");
3586
+ }
3587
+ } catch (error) {
3588
+ logger.debug("Error stopping daemon", error);
3589
+ }
3590
+ }
3591
+ async function waitForProcessDeath(pid, timeout) {
3592
+ const start = Date.now();
3593
+ while (Date.now() - start < timeout) {
3594
+ try {
3595
+ process.kill(pid, 0);
3596
+ await new Promise((resolve) => setTimeout(resolve, 100));
3597
+ } catch {
3598
+ return;
3599
+ }
3600
+ }
3601
+ throw new Error("Process did not die within timeout");
3602
+ }
3603
+
3758
3604
  function findAllHappyProcesses() {
3759
3605
  try {
3760
- const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
3761
- const lines = output.trim().split("\n").filter((line) => line.trim());
3762
3606
  const allProcesses = [];
3763
- for (const line of lines) {
3764
- const parts = line.trim().split(/\s+/);
3765
- if (parts.length < 11) continue;
3766
- const pid = parseInt(parts[1]);
3767
- const command = parts.slice(10).join(" ");
3768
- let type = "unknown";
3769
- if (pid === process.pid) {
3770
- type = "current";
3771
- } else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
3772
- type = "daemon";
3773
- } else if (command.includes("--started-by daemon")) {
3774
- type = "daemon-spawned-session";
3775
- } else if (command.includes("doctor")) {
3776
- type = "doctor";
3777
- } else {
3778
- type = "user-session";
3607
+ try {
3608
+ const happyOutput = execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
3609
+ const happyLines = happyOutput.trim().split("\n").filter((line) => line.trim());
3610
+ for (const line of happyLines) {
3611
+ const parts = line.trim().split(/\s+/);
3612
+ if (parts.length < 11) continue;
3613
+ const pid = parseInt(parts[1]);
3614
+ const command = parts.slice(10).join(" ");
3615
+ let type = "unknown";
3616
+ if (pid === process.pid) {
3617
+ type = "current";
3618
+ } else if (command.includes("--version")) {
3619
+ type = "daemon-version-check";
3620
+ } else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
3621
+ type = "daemon";
3622
+ } else if (command.includes("--started-by daemon")) {
3623
+ type = "daemon-spawned-session";
3624
+ } else if (command.includes("doctor")) {
3625
+ type = "doctor";
3626
+ } else {
3627
+ type = "user-session";
3628
+ }
3629
+ allProcesses.push({ pid, command, type });
3779
3630
  }
3780
- allProcesses.push({ pid, command, type });
3631
+ } catch {
3781
3632
  }
3782
3633
  try {
3783
- const devOutput = execSync('ps aux | grep -E "(tsx.*src/index.ts|yarn.*tsx)" | grep -v grep', { encoding: "utf8" });
3634
+ const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
3784
3635
  const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
3785
3636
  for (const line of devLines) {
3786
3637
  const parts = line.trim().split(/\s+/);
3787
3638
  if (parts.length < 11) continue;
3788
3639
  const pid = parseInt(parts[1]);
3789
3640
  const command = parts.slice(10).join(" ");
3790
- let workingDir = "";
3791
- try {
3792
- const pwdOutput = execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
3793
- workingDir = pwdOutput.replace(`${pid}:`, "").trim();
3794
- } catch {
3641
+ if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
3642
+ continue;
3795
3643
  }
3796
- if (workingDir.includes("happy-cli")) {
3797
- allProcesses.push({ pid, command, type: "dev-session" });
3644
+ let type = "unknown";
3645
+ if (pid === process.pid) {
3646
+ type = "current";
3647
+ } else if (command.includes("--version")) {
3648
+ type = "dev-daemon-version-check";
3649
+ } else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
3650
+ type = "dev-daemon";
3651
+ } else if (command.includes("--started-by daemon")) {
3652
+ type = "dev-daemon-spawned";
3653
+ } else if (command.includes("doctor")) {
3654
+ type = "dev-doctor";
3655
+ } else if (command.includes("--yolo")) {
3656
+ type = "dev-session";
3657
+ } else {
3658
+ type = "dev-related";
3798
3659
  }
3660
+ allProcesses.push({ pid, command, type });
3799
3661
  }
3800
3662
  } catch {
3801
3663
  }
@@ -3806,18 +3668,39 @@ function findAllHappyProcesses() {
3806
3668
  }
3807
3669
  function findRunawayHappyProcesses() {
3808
3670
  try {
3809
- const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
3810
- const lines = output.trim().split("\n").filter((line) => line.trim());
3811
3671
  const processes = [];
3812
- for (const line of lines) {
3813
- const parts = line.trim().split(/\s+/);
3814
- if (parts.length < 11) continue;
3815
- const pid = parseInt(parts[1]);
3816
- const command = parts.slice(10).join(" ");
3817
- if (pid === process.pid) continue;
3818
- if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start")) {
3819
- processes.push({ pid, command });
3672
+ try {
3673
+ const output = execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
3674
+ const lines = output.trim().split("\n").filter((line) => line.trim());
3675
+ for (const line of lines) {
3676
+ const parts = line.trim().split(/\s+/);
3677
+ if (parts.length < 11) continue;
3678
+ const pid = parseInt(parts[1]);
3679
+ const command = parts.slice(10).join(" ");
3680
+ if (pid === process.pid) continue;
3681
+ if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
3682
+ processes.push({ pid, command });
3683
+ }
3684
+ }
3685
+ } catch {
3686
+ }
3687
+ try {
3688
+ const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
3689
+ const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
3690
+ for (const line of devLines) {
3691
+ const parts = line.trim().split(/\s+/);
3692
+ if (parts.length < 11) continue;
3693
+ const pid = parseInt(parts[1]);
3694
+ const command = parts.slice(10).join(" ");
3695
+ if (pid === process.pid) continue;
3696
+ if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
3697
+ continue;
3698
+ }
3699
+ if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
3700
+ processes.push({ pid, command });
3701
+ }
3820
3702
  }
3703
+ } catch {
3821
3704
  }
3822
3705
  return processes;
3823
3706
  } catch (error) {
@@ -3827,10 +3710,10 @@ function findRunawayHappyProcesses() {
3827
3710
  async function killRunawayHappyProcesses() {
3828
3711
  const runawayProcesses = findRunawayHappyProcesses();
3829
3712
  const errors = [];
3830
- let killed = 0;
3831
- for (const { pid, command } of runawayProcesses) {
3713
+ const killPromises = runawayProcesses.map(async ({ pid, command }) => {
3832
3714
  try {
3833
3715
  process.kill(pid, "SIGTERM");
3716
+ console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
3834
3717
  await new Promise((resolve) => setTimeout(resolve, 1e3));
3835
3718
  try {
3836
3719
  process.kill(pid, 0);
@@ -3838,66 +3721,19 @@ async function killRunawayHappyProcesses() {
3838
3721
  process.kill(pid, "SIGKILL");
3839
3722
  } catch {
3840
3723
  }
3841
- killed++;
3842
- console.log(`Killed runaway process PID ${pid}: ${command}`);
3724
+ console.log(`Successfully killed runaway process PID ${pid}`);
3725
+ return { success: true, pid, command };
3843
3726
  } catch (error) {
3844
- errors.push({ pid, error: error.message });
3727
+ const errorMessage = error.message;
3728
+ errors.push({ pid, error: errorMessage });
3729
+ console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
3730
+ return { success: false, pid, command };
3845
3731
  }
3846
- }
3732
+ });
3733
+ const results = await Promise.all(killPromises);
3734
+ const killed = results.filter((r) => r.success).length;
3847
3735
  return { killed, errors };
3848
3736
  }
3849
- async function stopDaemon() {
3850
- try {
3851
- stopCaffeinate();
3852
- logger.debug("Stopped sleep prevention");
3853
- const state = await getDaemonState();
3854
- if (!state) {
3855
- logger.debug("No daemon state found");
3856
- return;
3857
- }
3858
- logger.debug(`Stopping daemon with PID ${state.pid}`);
3859
- try {
3860
- const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
3861
- await stopDaemonHttp();
3862
- await waitForProcessDeath(state.pid, 5e3);
3863
- logger.debug("Daemon stopped gracefully via HTTP");
3864
- return;
3865
- } catch (error) {
3866
- logger.debug("HTTP stop failed, will force kill", error);
3867
- }
3868
- try {
3869
- process.kill(state.pid, "SIGKILL");
3870
- logger.debug("Force killed daemon");
3871
- } catch (error) {
3872
- logger.debug("Daemon already dead");
3873
- }
3874
- } catch (error) {
3875
- logger.debug("Error stopping daemon", error);
3876
- }
3877
- }
3878
- async function waitForProcessDeath(pid, timeout) {
3879
- const start = Date.now();
3880
- while (Date.now() - start < timeout) {
3881
- try {
3882
- process.kill(pid, 0);
3883
- await new Promise((resolve) => setTimeout(resolve, 100));
3884
- } catch {
3885
- return;
3886
- }
3887
- }
3888
- throw new Error("Process did not die within timeout");
3889
- }
3890
-
3891
- var utils = /*#__PURE__*/Object.freeze({
3892
- __proto__: null,
3893
- cleanupDaemonState: cleanupDaemonState,
3894
- findAllHappyProcesses: findAllHappyProcesses,
3895
- findRunawayHappyProcesses: findRunawayHappyProcesses,
3896
- getDaemonState: getDaemonState,
3897
- isDaemonRunning: isDaemonRunning,
3898
- killRunawayHappyProcesses: killRunawayHappyProcesses,
3899
- stopDaemon: stopDaemon
3900
- });
3901
3737
 
3902
3738
  function getEnvironmentInfo() {
3903
3739
  return {
@@ -3912,7 +3748,15 @@ function getEnvironmentInfo() {
3912
3748
  processArgv: process.argv,
3913
3749
  happyDir: configuration?.happyHomeDir,
3914
3750
  serverUrl: configuration?.serverUrl,
3915
- logsDir: configuration?.logsDir
3751
+ logsDir: configuration?.logsDir,
3752
+ processPid: process.pid,
3753
+ nodeVersion: process.version,
3754
+ platform: process.platform,
3755
+ arch: process.arch,
3756
+ user: process.env.USER,
3757
+ home: process.env.HOME,
3758
+ shell: process.env.SHELL,
3759
+ terminal: process.env.TERM
3916
3760
  };
3917
3761
  }
3918
3762
  function getLogFiles(logDir) {
@@ -3924,62 +3768,67 @@ function getLogFiles(logDir) {
3924
3768
  const path = join(logDir, file);
3925
3769
  const stats = statSync(path);
3926
3770
  return { file, path, modified: stats.mtime };
3927
- }).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10);
3771
+ }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
3928
3772
  } catch {
3929
3773
  return [];
3930
3774
  }
3931
3775
  }
3932
- async function runDoctorCommand() {
3933
- console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
3934
- console.log(chalk.bold("\u{1F4CB} Basic Information"));
3935
- console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
3936
- console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
3937
- console.log(`Node.js Version: ${chalk.green(process.version)}`);
3938
- console.log("");
3939
- console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
3940
- const projectRoot = projectPath();
3941
- const wrapperPath = join(projectRoot, "bin", "happy.mjs");
3942
- const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
3943
- console.log(`Project Root: ${chalk.blue(projectRoot)}`);
3944
- console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
3945
- console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
3946
- console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3947
- console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3948
- console.log("");
3949
- console.log(chalk.bold("\u2699\uFE0F Configuration"));
3950
- console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
3951
- console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
3952
- console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
3953
- console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
3954
- const env = getEnvironmentInfo();
3955
- console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
3956
- console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
3957
- console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
3958
- console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
3959
- console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
3960
- try {
3961
- const settings = await readSettings();
3962
- console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
3963
- console.log(chalk.gray(JSON.stringify(settings, null, 2)));
3964
- } catch (error) {
3965
- console.log(chalk.bold("\n\u{1F4C4} Settings:"));
3966
- console.log(chalk.red("\u274C Failed to read settings"));
3776
+ async function runDoctorCommand(filter) {
3777
+ if (!filter) {
3778
+ filter = "all";
3967
3779
  }
3968
- console.log(chalk.bold("\n\u{1F510} Authentication"));
3969
- try {
3970
- const credentials = await readCredentials();
3971
- if (credentials) {
3972
- console.log(chalk.green("\u2713 Authenticated (credentials found)"));
3973
- } else {
3974
- console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
3780
+ console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
3781
+ if (filter === "all") {
3782
+ console.log(chalk.bold("\u{1F4CB} Basic Information"));
3783
+ console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
3784
+ console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
3785
+ console.log(`Node.js Version: ${chalk.green(process.version)}`);
3786
+ console.log("");
3787
+ console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
3788
+ const projectRoot = projectPath();
3789
+ const wrapperPath = join(projectRoot, "bin", "happy.mjs");
3790
+ const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
3791
+ console.log(`Project Root: ${chalk.blue(projectRoot)}`);
3792
+ console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
3793
+ console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
3794
+ console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3795
+ console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3796
+ console.log("");
3797
+ console.log(chalk.bold("\u2699\uFE0F Configuration"));
3798
+ console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
3799
+ console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
3800
+ console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
3801
+ console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
3802
+ const env = getEnvironmentInfo();
3803
+ console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
3804
+ console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
3805
+ console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
3806
+ console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
3807
+ console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
3808
+ try {
3809
+ const settings = await readSettings();
3810
+ console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
3811
+ console.log(chalk.gray(JSON.stringify(settings, null, 2)));
3812
+ } catch (error) {
3813
+ console.log(chalk.bold("\n\u{1F4C4} Settings:"));
3814
+ console.log(chalk.red("\u274C Failed to read settings"));
3975
3815
  }
3976
- } catch (error) {
3977
- console.log(chalk.red("\u274C Error reading credentials"));
3978
- }
3979
- console.log(chalk.bold("\n\u{1F916} Daemon Status"));
3816
+ console.log(chalk.bold("\n\u{1F510} Authentication"));
3817
+ try {
3818
+ const credentials = await readCredentials();
3819
+ if (credentials) {
3820
+ console.log(chalk.green("\u2713 Authenticated (credentials found)"));
3821
+ } else {
3822
+ console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
3823
+ }
3824
+ } catch (error) {
3825
+ console.log(chalk.red("\u274C Error reading credentials"));
3826
+ }
3827
+ }
3828
+ console.log(chalk.bold("\n\u{1F916} Daemon Status"));
3980
3829
  try {
3981
- const isRunning = await isDaemonRunning();
3982
- const state = await getDaemonState();
3830
+ const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
3831
+ const state = await readDaemonState();
3983
3832
  if (isRunning && state) {
3984
3833
  console.log(chalk.green("\u2713 Daemon is running"));
3985
3834
  console.log(` PID: ${state.pid}`);
@@ -4010,9 +3859,11 @@ async function runDoctorCommand() {
4010
3859
  const typeLabels = {
4011
3860
  "current": "\u{1F4CD} Current Process",
4012
3861
  "daemon": "\u{1F916} Daemon",
3862
+ "daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
4013
3863
  "daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
4014
3864
  "user-session": "\u{1F464} User Sessions",
4015
3865
  "dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
3866
+ "dev-daemon-version-check": "\u{1F6E0}\uFE0F Dev Daemon Version Check (stuck)",
4016
3867
  "dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
4017
3868
  "dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
4018
3869
  "dev-related": "\u{1F6E0}\uFE0F Dev Related",
@@ -4026,190 +3877,54 @@ ${typeLabels[type] || type}:`));
4026
3877
  console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
4027
3878
  });
4028
3879
  });
3880
+ } else {
3881
+ console.log(chalk.red("\u274C No happy processes found"));
4029
3882
  }
4030
- const runawayProcesses = findRunawayHappyProcesses();
4031
- if (runawayProcesses.length > 0) {
4032
- console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
4033
- console.log(chalk.gray("These processes were left running after daemon crashes."));
4034
- runawayProcesses.forEach(({ pid, command }) => {
4035
- console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
4036
- });
4037
- console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
4038
- }
4039
- if (allProcesses.length > 1) {
3883
+ if (filter === "all" && allProcesses.length > 1) {
4040
3884
  console.log(chalk.bold("\n\u{1F4A1} Process Management"));
4041
- console.log(chalk.gray("To kill runaway processes: happy daemon kill-runaway"));
3885
+ console.log(chalk.gray("To clean up runaway processes: happy doctor clean"));
4042
3886
  }
4043
3887
  } catch (error) {
4044
3888
  console.log(chalk.red("\u274C Error checking daemon status"));
4045
3889
  }
4046
- console.log(chalk.bold("\n\u{1F4DD} Log Files"));
4047
- const mainLogs = getLogFiles(configuration.logsDir);
4048
- if (mainLogs.length > 0) {
4049
- console.log(chalk.blue("\nMain Logs:"));
4050
- mainLogs.forEach(({ file, path, modified }) => {
4051
- console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
4052
- console.log(chalk.gray(` ${path}`));
4053
- });
4054
- } else {
4055
- console.log(chalk.yellow("No main log files found"));
4056
- }
4057
- const daemonLogs = mainLogs.filter(({ file }) => file.includes("daemon"));
4058
- if (daemonLogs.length > 0) {
4059
- console.log(chalk.blue("\nDaemon Logs:"));
4060
- daemonLogs.forEach(({ file, path, modified }) => {
4061
- console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
4062
- console.log(chalk.gray(` ${path}`));
4063
- });
4064
- } else {
4065
- console.log(chalk.yellow("No daemon log files found"));
4066
- }
4067
- console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
4068
- console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
4069
- console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
4070
- console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
4071
- }
4072
-
4073
- async function daemonPost(path, body) {
4074
- const state = await getDaemonState();
4075
- if (!state?.httpPort) {
4076
- throw new Error("No daemon running");
4077
- }
4078
- try {
4079
- const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
4080
- method: "POST",
4081
- headers: { "Content-Type": "application/json" },
4082
- body: JSON.stringify(body || {}),
4083
- signal: AbortSignal.timeout(5e3)
4084
- });
4085
- if (!response.ok) {
4086
- throw new Error(`HTTP ${response.status}`);
4087
- }
4088
- return await response.json();
4089
- } catch (error) {
4090
- logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
4091
- throw error;
4092
- }
4093
- }
4094
- async function notifyDaemonSessionStarted(sessionId, metadata) {
4095
- await daemonPost("/session-started", {
4096
- sessionId,
4097
- metadata
4098
- });
4099
- }
4100
- async function listDaemonSessions() {
4101
- const result = await daemonPost("/list");
4102
- return result.children || [];
4103
- }
4104
- async function stopDaemonSession(sessionId) {
4105
- const result = await daemonPost("/stop-session", { sessionId });
4106
- return result.success || false;
4107
- }
4108
- async function stopDaemonHttp() {
4109
- await daemonPost("/stop");
4110
- }
4111
-
4112
- var controlClient = /*#__PURE__*/Object.freeze({
4113
- __proto__: null,
4114
- listDaemonSessions: listDaemonSessions,
4115
- notifyDaemonSessionStarted: notifyDaemonSessionStarted,
4116
- stopDaemonHttp: stopDaemonHttp,
4117
- stopDaemonSession: stopDaemonSession
4118
- });
4119
-
4120
- function startDaemonControlServer({
4121
- getChildren,
4122
- stopSession,
4123
- spawnSession,
4124
- requestShutdown,
4125
- onHappySessionWebhook
4126
- }) {
4127
- return new Promise((resolve) => {
4128
- const app = fastify({
4129
- logger: false
4130
- // We use our own logger
4131
- });
4132
- app.setValidatorCompiler(validatorCompiler);
4133
- app.setSerializerCompiler(serializerCompiler);
4134
- const typed = app.withTypeProvider();
4135
- typed.post("/session-started", {
4136
- schema: {
4137
- body: z$1.object({
4138
- sessionId: z$1.string(),
4139
- metadata: z$1.any()
4140
- // Metadata type from API
4141
- })
4142
- }
4143
- }, async (request, reply) => {
4144
- const { sessionId, metadata } = request.body;
4145
- logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
4146
- onHappySessionWebhook(sessionId, metadata);
4147
- return { status: "ok" };
4148
- });
4149
- typed.post("/list", async (request, reply) => {
4150
- const children = getChildren();
4151
- logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
4152
- return { children };
4153
- });
4154
- typed.post("/stop-session", {
4155
- schema: {
4156
- body: z$1.object({
4157
- sessionId: z$1.string()
4158
- })
4159
- }
4160
- }, async (request, reply) => {
4161
- const { sessionId } = request.body;
4162
- logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
4163
- const success = stopSession(sessionId);
4164
- return { success };
4165
- });
4166
- typed.post("/spawn-session", {
4167
- schema: {
4168
- body: z$1.object({
4169
- directory: z$1.string(),
4170
- sessionId: z$1.string().optional()
4171
- })
3890
+ if (filter === "all") {
3891
+ console.log(chalk.bold("\n\u{1F4DD} Log Files"));
3892
+ const allLogs = getLogFiles(configuration.logsDir);
3893
+ if (allLogs.length > 0) {
3894
+ const daemonLogs = allLogs.filter(({ file }) => file.includes("daemon"));
3895
+ const regularLogs = allLogs.filter(({ file }) => !file.includes("daemon"));
3896
+ if (regularLogs.length > 0) {
3897
+ console.log(chalk.blue("\nRecent Logs:"));
3898
+ const logsToShow = regularLogs.slice(0, 10);
3899
+ logsToShow.forEach(({ file, path, modified }) => {
3900
+ console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
3901
+ console.log(chalk.gray(` ${path}`));
3902
+ });
3903
+ if (regularLogs.length > 10) {
3904
+ console.log(chalk.gray(` ... and ${regularLogs.length - 10} more log files`));
3905
+ }
4172
3906
  }
4173
- }, async (request, reply) => {
4174
- const { directory, sessionId } = request.body;
4175
- logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
4176
- const session = await spawnSession(directory, sessionId);
4177
- if (session) {
4178
- return {
4179
- success: true,
4180
- pid: session.pid,
4181
- sessionId: session.happySessionId || "pending"
4182
- };
3907
+ if (daemonLogs.length > 0) {
3908
+ console.log(chalk.blue("\nDaemon Logs:"));
3909
+ const daemonLogsToShow = daemonLogs.slice(0, 5);
3910
+ daemonLogsToShow.forEach(({ file, path, modified }) => {
3911
+ console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
3912
+ console.log(chalk.gray(` ${path}`));
3913
+ });
3914
+ if (daemonLogs.length > 5) {
3915
+ console.log(chalk.gray(` ... and ${daemonLogs.length - 5} more daemon log files`));
3916
+ }
4183
3917
  } else {
4184
- reply.code(500);
4185
- return { error: "Failed to spawn session" };
3918
+ console.log(chalk.yellow("\nNo daemon log files found"));
4186
3919
  }
4187
- });
4188
- typed.post("/stop", async (request, reply) => {
4189
- logger.debug("[CONTROL SERVER] Stop daemon request received");
4190
- setTimeout(() => {
4191
- logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
4192
- requestShutdown();
4193
- }, 50);
4194
- return { status: "stopping" };
4195
- });
4196
- app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
4197
- if (err) {
4198
- logger.debug("[CONTROL SERVER] Failed to start:", err);
4199
- throw err;
4200
- }
4201
- const port = parseInt(address.split(":").pop());
4202
- logger.debug(`[CONTROL SERVER] Started on port ${port}`);
4203
- resolve({
4204
- port,
4205
- stop: async () => {
4206
- logger.debug("[CONTROL SERVER] Stopping server");
4207
- await app.close();
4208
- logger.debug("[CONTROL SERVER] Server stopped");
4209
- }
4210
- });
4211
- });
4212
- });
3920
+ } else {
3921
+ console.log(chalk.yellow("No log files found"));
3922
+ }
3923
+ console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
3924
+ console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
3925
+ console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
3926
+ }
3927
+ console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
4213
3928
  }
4214
3929
 
4215
3930
  function displayQRCode(url) {
@@ -4236,7 +3951,7 @@ async function openBrowser(url) {
4236
3951
  return false;
4237
3952
  }
4238
3953
  logger.debug(`[browser] Attempting to open URL: ${url}`);
4239
- await open$1(url);
3954
+ await open(url);
4240
3955
  logger.debug("[browser] Browser opened successfully");
4241
3956
  return true;
4242
3957
  } catch (error) {
@@ -4452,16 +4167,140 @@ function spawnHappyCLI(args, options = {}) {
4452
4167
  directory = process.cwd();
4453
4168
  }
4454
4169
  const fullCommand = `happy ${args.join(" ")}`;
4455
- logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
4170
+ logger.debug(`[SPAWN HAPPY CLI] Spawning: ${fullCommand} in ${directory}`);
4456
4171
  const nodeArgs = [
4457
4172
  "--no-warnings",
4458
4173
  "--no-deprecation",
4459
4174
  entrypoint,
4460
4175
  ...args
4461
4176
  ];
4177
+ if (!existsSync(entrypoint)) {
4178
+ const errorMessage = `Entrypoint ${entrypoint} does not exist`;
4179
+ logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
4180
+ throw new Error(errorMessage);
4181
+ }
4462
4182
  return spawn$1("node", nodeArgs, options);
4463
4183
  }
4464
4184
 
4185
+ function startDaemonControlServer({
4186
+ getChildren,
4187
+ stopSession,
4188
+ spawnSession,
4189
+ requestShutdown,
4190
+ onHappySessionWebhook
4191
+ }) {
4192
+ return new Promise((resolve) => {
4193
+ const app = fastify({
4194
+ logger: false
4195
+ // We use our own logger
4196
+ });
4197
+ app.setValidatorCompiler(validatorCompiler);
4198
+ app.setSerializerCompiler(serializerCompiler);
4199
+ const typed = app.withTypeProvider();
4200
+ typed.post("/session-started", {
4201
+ schema: {
4202
+ body: z.object({
4203
+ sessionId: z.string(),
4204
+ metadata: z.any()
4205
+ // Metadata type from API
4206
+ })
4207
+ }
4208
+ }, async (request, reply) => {
4209
+ const { sessionId, metadata } = request.body;
4210
+ logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
4211
+ onHappySessionWebhook(sessionId, metadata);
4212
+ return { status: "ok" };
4213
+ });
4214
+ typed.post("/list", async (request, reply) => {
4215
+ const children = getChildren();
4216
+ logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
4217
+ return {
4218
+ children: children.map((child) => {
4219
+ delete child.childProcess;
4220
+ return child;
4221
+ })
4222
+ };
4223
+ });
4224
+ typed.post("/stop-session", {
4225
+ schema: {
4226
+ body: z.object({
4227
+ sessionId: z.string()
4228
+ })
4229
+ }
4230
+ }, async (request, reply) => {
4231
+ const { sessionId } = request.body;
4232
+ logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
4233
+ const success = stopSession(sessionId);
4234
+ return { success };
4235
+ });
4236
+ typed.post("/spawn-session", {
4237
+ schema: {
4238
+ body: z.object({
4239
+ directory: z.string(),
4240
+ sessionId: z.string().optional()
4241
+ })
4242
+ }
4243
+ }, async (request, reply) => {
4244
+ const { directory, sessionId } = request.body;
4245
+ logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
4246
+ const session = await spawnSession(directory, sessionId);
4247
+ if (session) {
4248
+ return {
4249
+ success: true,
4250
+ pid: session.pid,
4251
+ sessionId: session.happySessionId || "pending",
4252
+ message: session.message
4253
+ };
4254
+ } else {
4255
+ reply.code(500);
4256
+ return {
4257
+ success: false,
4258
+ error: "Failed to spawn session. Check the directory path and permissions."
4259
+ };
4260
+ }
4261
+ });
4262
+ typed.post("/stop", async (request, reply) => {
4263
+ logger.debug("[CONTROL SERVER] Stop daemon request received");
4264
+ setTimeout(() => {
4265
+ logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
4266
+ requestShutdown();
4267
+ }, 50);
4268
+ return { status: "stopping" };
4269
+ });
4270
+ typed.post("/dev-simulate-error", {
4271
+ schema: {
4272
+ body: z.object({
4273
+ error: z.string()
4274
+ })
4275
+ }
4276
+ }, async (request, reply) => {
4277
+ const { error } = request.body;
4278
+ logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
4279
+ setTimeout(() => {
4280
+ logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
4281
+ throw new Error(error);
4282
+ }, 100);
4283
+ return { status: "error will be thrown" };
4284
+ });
4285
+ app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
4286
+ if (err) {
4287
+ logger.debug("[CONTROL SERVER] Failed to start:", err);
4288
+ throw err;
4289
+ }
4290
+ const port = parseInt(address.split(":").pop());
4291
+ logger.debug(`[CONTROL SERVER] Started on port ${port}`);
4292
+ resolve({
4293
+ port,
4294
+ stop: async () => {
4295
+ logger.debug("[CONTROL SERVER] Stopping server");
4296
+ await app.close();
4297
+ logger.debug("[CONTROL SERVER] Server stopped");
4298
+ }
4299
+ });
4300
+ });
4301
+ });
4302
+ }
4303
+
4465
4304
  const initialMachineMetadata = {
4466
4305
  host: os.hostname(),
4467
4306
  platform: os.platform(),
@@ -4470,37 +4309,79 @@ const initialMachineMetadata = {
4470
4309
  happyHomeDir: configuration.happyHomeDir
4471
4310
  };
4472
4311
  async function startDaemon() {
4312
+ let requestShutdown;
4313
+ let resolvesWhenShutdownRequested = new Promise((resolve2) => {
4314
+ requestShutdown = (source, errorMessage) => {
4315
+ logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
4316
+ setTimeout(async () => {
4317
+ logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
4318
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
4319
+ process.exit(1);
4320
+ }, 1e3);
4321
+ resolve2({ source, errorMessage });
4322
+ };
4323
+ });
4324
+ process.on("SIGINT", () => {
4325
+ logger.debug("[DAEMON RUN] Received SIGINT");
4326
+ requestShutdown("os-signal");
4327
+ });
4328
+ process.on("SIGTERM", () => {
4329
+ logger.debug("[DAEMON RUN] Received SIGTERM");
4330
+ requestShutdown("os-signal");
4331
+ });
4332
+ process.on("uncaughtException", (error) => {
4333
+ logger.debug("[DAEMON RUN] FATAL: Uncaught exception", error);
4334
+ logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
4335
+ requestShutdown("exception", error.message);
4336
+ });
4337
+ process.on("unhandledRejection", (reason, promise) => {
4338
+ logger.debug("[DAEMON RUN] FATAL: Unhandled promise rejection", reason);
4339
+ logger.debug(`[DAEMON RUN] Rejected promise:`, promise);
4340
+ const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${reason}`);
4341
+ logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
4342
+ requestShutdown("exception", error.message);
4343
+ });
4344
+ process.on("exit", (code) => {
4345
+ logger.debug(`[DAEMON RUN] Process exiting with code: ${code}`);
4346
+ });
4347
+ process.on("beforeExit", (code) => {
4348
+ logger.debug(`[DAEMON RUN] Process about to exit with code: ${code}`);
4349
+ });
4473
4350
  logger.debug("[DAEMON RUN] Starting daemon process...");
4474
4351
  logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
4475
- const runningDaemon = await getDaemonState();
4476
- if (runningDaemon) {
4477
- try {
4478
- process.kill(runningDaemon.pid, 0);
4479
- logger.debug("[DAEMON RUN] Daemon already running");
4480
- console.log(`Daemon already running (PID: ${runningDaemon.pid})`);
4481
- process.exit(0);
4482
- } catch {
4483
- logger.debug("[DAEMON RUN] Stale state found, cleaning up");
4484
- await cleanupDaemonState();
4485
- }
4352
+ const runningDaemonVersionMatches = await isDaemonRunningSameVersion();
4353
+ if (!runningDaemonVersionMatches) {
4354
+ logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
4355
+ await stopDaemon();
4356
+ } else {
4357
+ logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
4358
+ console.log("Daemon already running with matching version");
4359
+ process.exit(0);
4486
4360
  }
4487
- const caffeinateStarted = startCaffeinate();
4488
- if (caffeinateStarted) {
4489
- logger.debug("[DAEMON RUN] Sleep prevention enabled");
4361
+ const daemonLockHandle = await acquireDaemonLock(5, 200);
4362
+ if (!daemonLockHandle) {
4363
+ logger.debug("[DAEMON RUN] Daemon lock file already held, another daemon is running");
4364
+ process.exit(0);
4490
4365
  }
4491
4366
  try {
4367
+ const caffeinateStarted = startCaffeinate();
4368
+ if (caffeinateStarted) {
4369
+ logger.debug("[DAEMON RUN] Sleep prevention enabled");
4370
+ }
4492
4371
  const { credentials, machineId } = await authAndSetupMachineIfNeeded();
4493
4372
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
4494
4373
  const pidToTrackedSession = /* @__PURE__ */ new Map();
4495
4374
  const pidToAwaiter = /* @__PURE__ */ new Map();
4496
4375
  const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
4497
4376
  const onHappySessionWebhook = (sessionId, sessionMetadata) => {
4377
+ logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
4498
4378
  const pid = sessionMetadata.hostPid;
4499
4379
  if (!pid) {
4500
- logger.debug(`[DAEMON RUN] Session webhook missing hostPid for session ${sessionId}`);
4380
+ logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
4501
4381
  return;
4502
4382
  }
4503
4383
  logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
4384
+ logger.debug(`[DAEMON RUN] Current tracked sessions before webhook: ${Array.from(pidToTrackedSession.keys()).join(", ")}`);
4504
4385
  const existingSession = pidToTrackedSession.get(pid);
4505
4386
  if (existingSession && existingSession.startedBy === "daemon") {
4506
4387
  existingSession.happySessionId = sessionId;
@@ -4524,8 +4405,37 @@ async function startDaemon() {
4524
4405
  }
4525
4406
  };
4526
4407
  const spawnSession = async (directory, sessionId) => {
4408
+ let directoryCreated = false;
4409
+ if (directory.startsWith("~")) {
4410
+ directory = resolve$1(os.homedir(), directory.replace("~", ""));
4411
+ }
4412
+ try {
4413
+ await fs.access(directory);
4414
+ logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
4415
+ } catch (error) {
4416
+ logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
4417
+ try {
4418
+ await fs.mkdir(directory, { recursive: true });
4419
+ logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
4420
+ directoryCreated = true;
4421
+ } catch (mkdirError) {
4422
+ let errorMessage = `Unable to create directory at '${directory}'. `;
4423
+ if (mkdirError.code === "EACCES") {
4424
+ errorMessage += `Permission denied. You don't have write access to create a folder at this location. Try using a different path or check your permissions.`;
4425
+ } else if (mkdirError.code === "ENOTDIR") {
4426
+ errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
4427
+ } else if (mkdirError.code === "ENOSPC") {
4428
+ errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
4429
+ } else if (mkdirError.code === "EROFS") {
4430
+ errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
4431
+ } else {
4432
+ errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
4433
+ }
4434
+ logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
4435
+ return null;
4436
+ }
4437
+ }
4527
4438
  try {
4528
- const happyBinPath = join$1(projectPath(), "bin", "happy.mjs");
4529
4439
  const args = [
4530
4440
  "--happy-starting-mode",
4531
4441
  "remote",
@@ -4540,12 +4450,14 @@ async function startDaemon() {
4540
4450
  // Capture stdout/stderr for debugging
4541
4451
  // env is inherited automatically from parent process
4542
4452
  });
4543
- happyProcess.stdout?.on("data", (data) => {
4544
- logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
4545
- });
4546
- happyProcess.stderr?.on("data", (data) => {
4547
- logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
4548
- });
4453
+ if (process.env.DEBUG) {
4454
+ happyProcess.stdout?.on("data", (data) => {
4455
+ logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
4456
+ });
4457
+ happyProcess.stderr?.on("data", (data) => {
4458
+ logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
4459
+ });
4460
+ }
4549
4461
  if (!happyProcess.pid) {
4550
4462
  logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
4551
4463
  return null;
@@ -4554,7 +4466,9 @@ async function startDaemon() {
4554
4466
  const trackedSession = {
4555
4467
  startedBy: "daemon",
4556
4468
  pid: happyProcess.pid,
4557
- childProcess: happyProcess
4469
+ childProcess: happyProcess,
4470
+ directoryCreated,
4471
+ message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
4558
4472
  };
4559
4473
  pidToTrackedSession.set(happyProcess.pid, trackedSession);
4560
4474
  happyProcess.on("exit", (code, signal) => {
@@ -4570,16 +4484,16 @@ async function startDaemon() {
4570
4484
  }
4571
4485
  });
4572
4486
  logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
4573
- return new Promise((resolve, reject) => {
4487
+ return new Promise((resolve2, reject) => {
4574
4488
  const timeout = setTimeout(() => {
4575
4489
  pidToAwaiter.delete(happyProcess.pid);
4576
4490
  logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
4577
- resolve(trackedSession);
4491
+ resolve2(trackedSession);
4578
4492
  }, 1e4);
4579
4493
  pidToAwaiter.set(happyProcess.pid, (completedSession) => {
4580
4494
  clearTimeout(timeout);
4581
4495
  logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
4582
- resolve(completedSession);
4496
+ resolve2(completedSession);
4583
4497
  });
4584
4498
  });
4585
4499
  } catch (error) {
@@ -4618,10 +4532,6 @@ async function startDaemon() {
4618
4532
  logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
4619
4533
  pidToTrackedSession.delete(pid);
4620
4534
  };
4621
- let requestShutdown;
4622
- let resolvesWhenShutdownRequested = new Promise((resolve) => {
4623
- requestShutdown = resolve;
4624
- });
4625
4535
  const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
4626
4536
  getChildren: getCurrentChildren,
4627
4537
  stopSession,
@@ -4632,10 +4542,11 @@ async function startDaemon() {
4632
4542
  const fileState = {
4633
4543
  pid: process.pid,
4634
4544
  httpPort: controlPort,
4635
- startTime: (/* @__PURE__ */ new Date()).toISOString(),
4636
- startedWithCliVersion: packageJson.version
4545
+ startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
4546
+ startedWithCliVersion: packageJson.version,
4547
+ daemonLogPath: logger.logFilePath
4637
4548
  };
4638
- await writeDaemonState(fileState);
4549
+ writeDaemonState(fileState);
4639
4550
  logger.debug("[DAEMON RUN] Daemon state written");
4640
4551
  const initialDaemonState = {
4641
4552
  status: "offline",
@@ -4657,56 +4568,89 @@ async function startDaemon() {
4657
4568
  requestShutdown: () => requestShutdown("happy-app")
4658
4569
  });
4659
4570
  apiMachine.connect();
4660
- const cleanupAndShutdown = async (source) => {
4661
- logger.debug(`[DAEMON RUN] Starting cleanup (source: ${source})...`);
4662
- if (apiMachine) {
4663
- await apiMachine.updateDaemonState((state) => ({
4664
- ...state,
4665
- status: "shutting-down",
4666
- shutdownRequestedAt: Date.now(),
4667
- shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
4668
- }));
4669
- await new Promise((resolve) => setTimeout(resolve, 100));
4571
+ const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
4572
+ let heartbeatRunning = false;
4573
+ const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
4574
+ if (heartbeatRunning) {
4575
+ return;
4670
4576
  }
4671
- if (apiMachine) {
4672
- apiMachine.shutdown();
4577
+ heartbeatRunning = true;
4578
+ if (process.env.DEBUG) {
4579
+ logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
4673
4580
  }
4674
- logger.debug("[DAEMON RUN] Machine session shutdown");
4581
+ for (const [pid, _] of pidToTrackedSession.entries()) {
4582
+ try {
4583
+ process.kill(pid, 0);
4584
+ } catch (error) {
4585
+ logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
4586
+ pidToTrackedSession.delete(pid);
4587
+ }
4588
+ }
4589
+ const projectVersion = JSON.parse(readFileSync$1(join$1(projectPath(), "package.json"), "utf-8")).version;
4590
+ if (projectVersion !== configuration.currentCliVersion) {
4591
+ logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
4592
+ clearInterval(restartOnStaleVersionAndHeartbeat);
4593
+ try {
4594
+ spawnHappyCLI(["daemon", "start"], {
4595
+ detached: true,
4596
+ stdio: "ignore"
4597
+ });
4598
+ } catch (error) {
4599
+ logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
4600
+ }
4601
+ logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
4602
+ await new Promise((resolve2) => setTimeout(resolve2, 1e4));
4603
+ process.exit(0);
4604
+ }
4605
+ const daemonState = await readDaemonState();
4606
+ if (daemonState && daemonState.pid !== process.pid) {
4607
+ logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
4608
+ requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
4609
+ }
4610
+ try {
4611
+ const updatedState = {
4612
+ pid: process.pid,
4613
+ httpPort: controlPort,
4614
+ startTime: fileState.startTime,
4615
+ startedWithCliVersion: packageJson.version,
4616
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
4617
+ daemonLogPath: fileState.daemonLogPath
4618
+ };
4619
+ writeDaemonState(updatedState);
4620
+ if (process.env.DEBUG) {
4621
+ logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
4622
+ }
4623
+ } catch (error) {
4624
+ logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
4625
+ }
4626
+ heartbeatRunning = false;
4627
+ }, heartbeatIntervalMs);
4628
+ const cleanupAndShutdown = async (source, errorMessage) => {
4629
+ logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
4630
+ if (restartOnStaleVersionAndHeartbeat) {
4631
+ clearInterval(restartOnStaleVersionAndHeartbeat);
4632
+ logger.debug("[DAEMON RUN] Health check interval cleared");
4633
+ }
4634
+ await apiMachine.updateDaemonState((state) => ({
4635
+ ...state,
4636
+ status: "shutting-down",
4637
+ shutdownRequestedAt: Date.now(),
4638
+ shutdownSource: source
4639
+ }));
4640
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
4641
+ apiMachine.shutdown();
4675
4642
  await stopControlServer();
4676
- logger.debug("[DAEMON RUN] Control server stopped");
4677
4643
  await cleanupDaemonState();
4678
- logger.debug("[DAEMON RUN] State cleaned up");
4679
- stopCaffeinate();
4680
- logger.debug("[DAEMON RUN] Caffeinate stopped");
4644
+ await stopCaffeinate();
4645
+ await releaseDaemonLock(daemonLockHandle);
4646
+ logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
4681
4647
  process.exit(0);
4682
4648
  };
4683
- process.on("SIGINT", () => {
4684
- logger.debug("[DAEMON RUN] Received SIGINT");
4685
- cleanupAndShutdown("os-signal");
4686
- });
4687
- process.on("SIGTERM", () => {
4688
- logger.debug("[DAEMON RUN] Received SIGTERM");
4689
- cleanupAndShutdown("os-signal");
4690
- });
4691
- process.on("uncaughtException", (error) => {
4692
- logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
4693
- cleanupAndShutdown("unknown");
4694
- });
4695
- process.on("unhandledRejection", (reason) => {
4696
- logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
4697
- cleanupAndShutdown("unknown");
4698
- });
4699
- process.on("exit", () => {
4700
- logger.debug("[DAEMON RUN] Process exit, not killing any children");
4701
- });
4702
- logger.debug("[DAEMON RUN] Daemon started successfully");
4703
- const shutdownSource = await resolvesWhenShutdownRequested;
4704
- logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
4705
- await cleanupAndShutdown(shutdownSource);
4649
+ logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
4650
+ const shutdownRequest = await resolvesWhenShutdownRequested;
4651
+ await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
4706
4652
  } catch (error) {
4707
- logger.debug("[DAEMON RUN] Failed to start daemon", error);
4708
- await cleanupDaemonState();
4709
- stopCaffeinate();
4653
+ logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
4710
4654
  process.exit(1);
4711
4655
  }
4712
4656
  }
@@ -4734,7 +4678,7 @@ async function startHappyServer(client) {
4734
4678
  description: "Change the title of the current chat session",
4735
4679
  title: "Change Chat Title",
4736
4680
  inputSchema: {
4737
- title: z$1.string().describe("The new title for the chat session")
4681
+ title: z.string().describe("The new title for the chat session")
4738
4682
  }
4739
4683
  }, async (args) => {
4740
4684
  const response = await handler(args.title);
@@ -4807,10 +4751,14 @@ async function start(credentials, options = {}) {
4807
4751
  const settings = await readSettings();
4808
4752
  let machineId = settings?.machineId;
4809
4753
  if (!machineId) {
4810
- console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it, using 'unknown' id instead`);
4811
- machineId = "unknown";
4754
+ console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
4755
+ process.exit(1);
4812
4756
  }
4813
4757
  logger.debug(`Using machineId: ${machineId}`);
4758
+ await api.createMachineOrGetExistingAsIs({
4759
+ machineId,
4760
+ metadata: initialMachineMetadata
4761
+ });
4814
4762
  let metadata = {
4815
4763
  path: workingDirectory,
4816
4764
  host: os$1.hostname(),
@@ -4825,14 +4773,12 @@ async function start(credentials, options = {}) {
4825
4773
  };
4826
4774
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
4827
4775
  logger.debug(`Session created: ${response.id}`);
4828
- await api.createMachineOrGetExistingAsIs({
4829
- machineId,
4830
- metadata: initialMachineMetadata
4831
- });
4832
4776
  try {
4833
- const daemonState = await getDaemonState();
4834
- if (daemonState?.httpPort) {
4835
- await notifyDaemonSessionStarted(response.id, metadata);
4777
+ logger.debug(`[START] Reporting session ${response.id} to daemon`);
4778
+ const result = await notifyDaemonSessionStarted(response.id, metadata);
4779
+ if (result.error) {
4780
+ logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
4781
+ } else {
4836
4782
  logger.debug(`[START] Reported session ${response.id} to daemon`);
4837
4783
  }
4838
4784
  } catch (error) {
@@ -4854,7 +4800,7 @@ async function start(credentials, options = {}) {
4854
4800
  const session = api.sessionSyncClient(response);
4855
4801
  const happyServer = await startHappyServer(session);
4856
4802
  logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
4857
- const logPath = await logger.logFilePathPromise;
4803
+ const logPath = logger.logFilePath;
4858
4804
  logger.infoDeveloper(`Session: ${response.id}`);
4859
4805
  logger.infoDeveloper(`Logs: ${logPath}`);
4860
4806
  session.updateAgentState((currentState) => ({
@@ -5367,8 +5313,7 @@ async function handleAuthStatus() {
5367
5313
  console.log(chalk.gray(`
5368
5314
  Data directory: ${configuration.happyHomeDir}`));
5369
5315
  try {
5370
- const { isDaemonRunning } = await Promise.resolve().then(function () { return utils; });
5371
- const running = await isDaemonRunning();
5316
+ const running = await checkIfDaemonRunningAndCleanupStaleState();
5372
5317
  if (running) {
5373
5318
  console.log(chalk.green("\u2713 Daemon running"));
5374
5319
  } else {
@@ -5409,9 +5354,19 @@ const DaemonPrompt = ({ onSelect }) => {
5409
5354
 
5410
5355
  (async () => {
5411
5356
  const args = process.argv.slice(2);
5412
- logger.debug("Starting happy CLI with args: ", process.argv);
5357
+ if (!args.includes("--version")) {
5358
+ logger.debug("Starting happy CLI with args: ", process.argv);
5359
+ }
5413
5360
  const subcommand = args[0];
5414
5361
  if (subcommand === "doctor") {
5362
+ if (args[1] === "clean") {
5363
+ const result = await killRunawayHappyProcesses();
5364
+ console.log(`Cleaned up ${result.killed} runaway processes`);
5365
+ if (result.errors.length > 0) {
5366
+ console.log("Errors:", result.errors);
5367
+ }
5368
+ process.exit(0);
5369
+ }
5415
5370
  await runDoctorCommand();
5416
5371
  return;
5417
5372
  } else if (subcommand === "auth") {
@@ -5454,16 +5409,10 @@ const DaemonPrompt = ({ onSelect }) => {
5454
5409
  try {
5455
5410
  const sessions = await listDaemonSessions();
5456
5411
  if (sessions.length === 0) {
5457
- console.log("No active sessions");
5412
+ console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
5458
5413
  } else {
5459
5414
  console.log("Active sessions:");
5460
- const cleanSessions = sessions.map((s) => ({
5461
- pid: s.pid,
5462
- sessionId: s.happySessionId || `PID-${s.pid}`,
5463
- startedBy: s.startedBy,
5464
- directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
5465
- }));
5466
- console.log(JSON.stringify(cleanSessions, null, 2));
5415
+ console.log(JSON.stringify(sessions, null, 2));
5467
5416
  }
5468
5417
  } catch (error) {
5469
5418
  console.log("No daemon running");
@@ -5491,7 +5440,7 @@ const DaemonPrompt = ({ onSelect }) => {
5491
5440
  child.unref();
5492
5441
  let started = false;
5493
5442
  for (let i = 0; i < 50; i++) {
5494
- if (await isDaemonRunning()) {
5443
+ if (await checkIfDaemonRunningAndCleanupStaleState()) {
5495
5444
  started = true;
5496
5445
  break;
5497
5446
  }
@@ -5511,28 +5460,14 @@ const DaemonPrompt = ({ onSelect }) => {
5511
5460
  await stopDaemon();
5512
5461
  process.exit(0);
5513
5462
  } else if (daemonSubcommand === "status") {
5514
- const state = await getDaemonState();
5515
- if (!state) {
5516
- console.log("Daemon is not running");
5517
- } else {
5518
- const isRunning = await isDaemonRunning();
5519
- if (isRunning) {
5520
- console.log("Daemon is running");
5521
- console.log(` PID: ${state.pid}`);
5522
- console.log(` Port: ${state.httpPort}`);
5523
- console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
5524
- console.log(` CLI Version: ${state.startedWithCliVersion}`);
5525
- } else {
5526
- console.log("Daemon state file exists but daemon is not running (stale)");
5527
- }
5528
- }
5463
+ await runDoctorCommand("daemon");
5529
5464
  process.exit(0);
5530
- } else if (daemonSubcommand === "kill-runaway") {
5531
- const { killRunawayHappyProcesses } = await Promise.resolve().then(function () { return utils; });
5532
- const result = await killRunawayHappyProcesses();
5533
- console.log(`Killed ${result.killed} runaway processes`);
5534
- if (result.errors.length > 0) {
5535
- console.log("Errors:", result.errors);
5465
+ } else if (daemonSubcommand === "logs") {
5466
+ const latest = await getLatestDaemonLog();
5467
+ if (!latest) {
5468
+ console.log("No daemon logs found");
5469
+ } else {
5470
+ console.log(latest.path);
5536
5471
  }
5537
5472
  process.exit(0);
5538
5473
  } else if (daemonSubcommand === "install") {
@@ -5556,14 +5491,15 @@ ${chalk.bold("happy daemon")} - Daemon management
5556
5491
  ${chalk.bold("Usage:")}
5557
5492
  happy daemon start Start the daemon (detached)
5558
5493
  happy daemon stop Stop the daemon (sessions stay alive)
5559
- happy daemon stop --kill-managed Stop daemon and kill managed sessions
5560
5494
  happy daemon status Show daemon status
5561
5495
  happy daemon list List active sessions
5562
- happy daemon stop-session <id> Stop a specific session
5563
- happy daemon kill-runaway Kill all runaway Happy processes
5496
+
5497
+ If you want to kill all happy related processes run
5498
+ ${chalk.cyan("happy doctor clean")}
5564
5499
 
5565
5500
  ${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
5566
- Sessions spawned by the daemon will continue running after daemon stops unless --kill-managed is used.
5501
+
5502
+ ${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor clean")}
5567
5503
  `);
5568
5504
  }
5569
5505
  return;
@@ -5571,8 +5507,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
5571
5507
  const options = {};
5572
5508
  let showHelp = false;
5573
5509
  let showVersion = false;
5574
- let forceAuth = false;
5575
- let forceAuthNew = false;
5576
5510
  const unknownArgs = [];
5577
5511
  for (let i = 0; i < args.length; i++) {
5578
5512
  const arg = args[i];
@@ -5582,12 +5516,8 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
5582
5516
  } else if (arg === "-v" || arg === "--version") {
5583
5517
  showVersion = true;
5584
5518
  unknownArgs.push(arg);
5585
- } else if (arg === "--auth" || arg === "--login") {
5586
- forceAuth = true;
5587
- } else if (arg === "--force-auth") {
5588
- forceAuthNew = true;
5589
- } else if (arg === "--happy-starting-mode") {
5590
- options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
5519
+ } else if (arg === "--auth" || arg === "--login") ; else if (arg === "--force-auth") ; else if (arg === "--happy-starting-mode") {
5520
+ options.startingMode = z.enum(["local", "remote"]).parse(args[++i]);
5591
5521
  } else if (arg === "--yolo") {
5592
5522
  unknownArgs.push("--dangerously-skip-permissions");
5593
5523
  } else if (arg === "--started-by") {
@@ -5607,29 +5537,23 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
5607
5537
  ${chalk.bold("happy")} - Claude Code On the Go
5608
5538
 
5609
5539
  ${chalk.bold("Usage:")}
5610
- happy [options] Start Claude with mobile control
5611
- happy auth Manage authentication
5612
- happy notify Send push notification
5613
- happy daemon Manage background service
5614
-
5615
- ${chalk.bold("Happy Options:")}
5616
- --help Show this help message
5617
- --yolo Skip all permissions (--dangerously-skip-permissions)
5618
- --force-auth Force re-authentication
5619
-
5620
- ${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
5621
- Use any claude flag exactly as you normally would.
5540
+ happy [options] Start Claude with mobile control
5541
+ happy auth Manage authentication
5542
+ happy daemon Manage background service that allows
5543
+ to spawn new sessions away from your computer
5544
+ happy doctor System diagnostics & troubleshooting
5622
5545
 
5623
5546
  ${chalk.bold("Examples:")}
5624
- happy Start session
5625
- happy --yolo Start without permissions
5626
- happy --verbose Enable verbose mode
5627
- happy -c Continue last conversation
5628
- happy auth login Authenticate
5629
- happy notify -p "Done!" Send notification
5547
+ happy Start session
5548
+ happy --yolo Bypass permissions
5549
+ happy --verbose Enable verbose mode
5550
+ happy auth login --force Authenticate
5551
+ happy doctor Run diagnostics
5630
5552
 
5631
5553
  ${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
5632
- ${chalk.bold('Use "happy daemon" for background service management.')}
5554
+
5555
+ ${chalk.bold("Happy supports ALL Claude options!")}
5556
+ Use any claude flag exactly as you normally would.
5633
5557
 
5634
5558
  ${chalk.gray("\u2500".repeat(60))}
5635
5559
  ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
@@ -5647,30 +5571,9 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5647
5571
  console.log(packageJson.version);
5648
5572
  process.exit(0);
5649
5573
  }
5650
- let credentials;
5651
- if (forceAuthNew) {
5652
- console.log(chalk.yellow("Force authentication requested..."));
5653
- try {
5654
- await stopDaemon();
5655
- } catch {
5656
- }
5657
- await clearCredentials();
5658
- await clearMachineId();
5659
- const result = await authAndSetupMachineIfNeeded();
5660
- credentials = result.credentials;
5661
- } else if (forceAuth) {
5662
- console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
5663
- const res = await doAuth();
5664
- if (!res) {
5665
- process.exit(1);
5666
- }
5667
- await writeCredentials(res);
5668
- const result = await authAndSetupMachineIfNeeded();
5669
- credentials = result.credentials;
5670
- } else {
5671
- const result = await authAndSetupMachineIfNeeded();
5672
- credentials = result.credentials;
5673
- }
5574
+ const {
5575
+ credentials
5576
+ } = await authAndSetupMachineIfNeeded();
5674
5577
  let settings = await readSettings();
5675
5578
  if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
5676
5579
  const shouldAutoStart = await new Promise((resolve) => {
@@ -5699,15 +5602,18 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5699
5602
  }
5700
5603
  }
5701
5604
  if (settings && settings.daemonAutoStartWhenRunningHappy) {
5702
- logger.debug("Starting Happy background service...");
5703
- if (!await isDaemonRunning()) {
5605
+ logger.debug("Ensuring Happy background service is running & matches our version...");
5606
+ if (!await isDaemonRunningSameVersion()) {
5607
+ logger.debug("Starting Happy background service...");
5704
5608
  const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
5705
5609
  detached: true,
5706
5610
  stdio: "ignore",
5707
5611
  env: process.env
5708
5612
  });
5709
5613
  daemonProcess.unref();
5710
- await new Promise((resolve) => setTimeout(resolve, 500));
5614
+ await new Promise((resolve) => setTimeout(resolve, 100));
5615
+ } else {
5616
+ logger.debug("Happy background service is running & matches our version");
5711
5617
  }
5712
5618
  }
5713
5619
  try {