happy-coder 0.9.0-5 → 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.cjs +971 -1078
- package/dist/index.mjs +705 -794
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +7 -3
- package/dist/lib.d.mts +7 -3
- package/dist/lib.mjs +4 -4
- package/dist/{types-DJOX-XG-.mjs → types-Cezp_n6O.mjs} +552 -204
- package/dist/{types-a-nJyP-e.cjs → types-CyOnnZ8M.cjs} +448 -69
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
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,
|
|
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
|
|
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
|
|
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;
|
|
@@ -2639,6 +2638,11 @@ async function claudeRemoteLauncher(session) {
|
|
|
2639
2638
|
onReady: () => {
|
|
2640
2639
|
if (!pending && session.queue.size() === 0) {
|
|
2641
2640
|
session.client.sendSessionEvent({ type: "ready" });
|
|
2641
|
+
session.api.push().sendToAllDevices(
|
|
2642
|
+
"It's ready!",
|
|
2643
|
+
`Claude is waiting for your command`,
|
|
2644
|
+
{ sessionId: session.client.sessionId }
|
|
2645
|
+
);
|
|
2642
2646
|
}
|
|
2643
2647
|
},
|
|
2644
2648
|
signal: abortController.signal
|
|
@@ -2693,7 +2697,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2693
2697
|
}
|
|
2694
2698
|
|
|
2695
2699
|
async function loop(opts) {
|
|
2696
|
-
const logPath =
|
|
2700
|
+
const logPath = logger.logFilePath;
|
|
2697
2701
|
let session = new Session({
|
|
2698
2702
|
api: opts.api,
|
|
2699
2703
|
client: opts.session,
|
|
@@ -2738,133 +2742,6 @@ async function loop(opts) {
|
|
|
2738
2742
|
}
|
|
2739
2743
|
}
|
|
2740
2744
|
|
|
2741
|
-
var name = "happy-coder";
|
|
2742
|
-
var version = "0.9.0-5";
|
|
2743
|
-
var description = "Claude Code session sharing CLI";
|
|
2744
|
-
var author = "Kirill Dubovitskiy";
|
|
2745
|
-
var license = "MIT";
|
|
2746
|
-
var type = "module";
|
|
2747
|
-
var homepage = "https://github.com/slopus/happy-cli";
|
|
2748
|
-
var bugs = "https://github.com/slopus/happy-cli/issues";
|
|
2749
|
-
var repository = "slopus/happy-cli";
|
|
2750
|
-
var bin = {
|
|
2751
|
-
happy: "./bin/happy.mjs"
|
|
2752
|
-
};
|
|
2753
|
-
var main = "./dist/index.cjs";
|
|
2754
|
-
var module = "./dist/index.mjs";
|
|
2755
|
-
var types = "./dist/index.d.cts";
|
|
2756
|
-
var exports = {
|
|
2757
|
-
".": {
|
|
2758
|
-
require: {
|
|
2759
|
-
types: "./dist/index.d.cts",
|
|
2760
|
-
"default": "./dist/index.cjs"
|
|
2761
|
-
},
|
|
2762
|
-
"import": {
|
|
2763
|
-
types: "./dist/index.d.mts",
|
|
2764
|
-
"default": "./dist/index.mjs"
|
|
2765
|
-
}
|
|
2766
|
-
},
|
|
2767
|
-
"./lib": {
|
|
2768
|
-
require: {
|
|
2769
|
-
types: "./dist/lib.d.cts",
|
|
2770
|
-
"default": "./dist/lib.cjs"
|
|
2771
|
-
},
|
|
2772
|
-
"import": {
|
|
2773
|
-
types: "./dist/lib.d.mts",
|
|
2774
|
-
"default": "./dist/lib.mjs"
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
};
|
|
2778
|
-
var files = [
|
|
2779
|
-
"dist",
|
|
2780
|
-
"bin",
|
|
2781
|
-
"scripts",
|
|
2782
|
-
"ripgrep",
|
|
2783
|
-
"package.json"
|
|
2784
|
-
];
|
|
2785
|
-
var scripts = {
|
|
2786
|
-
"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",
|
|
2787
|
-
typecheck: "tsc --noEmit",
|
|
2788
|
-
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2789
|
-
test: "yarn build && vitest run",
|
|
2790
|
-
"test:watch": "vitest",
|
|
2791
|
-
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2792
|
-
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2793
|
-
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2794
|
-
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2795
|
-
prepublishOnly: "yarn build && yarn test",
|
|
2796
|
-
release: "release-it"
|
|
2797
|
-
};
|
|
2798
|
-
var dependencies = {
|
|
2799
|
-
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2800
|
-
"@anthropic-ai/sdk": "^0.56.0",
|
|
2801
|
-
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2802
|
-
"@stablelib/base64": "^2.0.1",
|
|
2803
|
-
"@types/http-proxy": "^1.17.16",
|
|
2804
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
2805
|
-
"@types/react": "^19.1.9",
|
|
2806
|
-
axios: "^1.10.0",
|
|
2807
|
-
chalk: "^5.4.1",
|
|
2808
|
-
"expo-server-sdk": "^3.15.0",
|
|
2809
|
-
fastify: "^5.5.0",
|
|
2810
|
-
"fastify-type-provider-zod": "4.0.2",
|
|
2811
|
-
"http-proxy": "^1.18.1",
|
|
2812
|
-
"http-proxy-middleware": "^3.0.5",
|
|
2813
|
-
ink: "^6.1.0",
|
|
2814
|
-
open: "^10.2.0",
|
|
2815
|
-
"qrcode-terminal": "^0.12.0",
|
|
2816
|
-
react: "^19.1.1",
|
|
2817
|
-
"socket.io-client": "^4.8.1",
|
|
2818
|
-
tweetnacl: "^1.0.3",
|
|
2819
|
-
zod: "^3.23.8"
|
|
2820
|
-
};
|
|
2821
|
-
var devDependencies = {
|
|
2822
|
-
"@eslint/compat": "^1",
|
|
2823
|
-
"@types/node": ">=20",
|
|
2824
|
-
"cross-env": "^10.0.0",
|
|
2825
|
-
eslint: "^9",
|
|
2826
|
-
"eslint-config-prettier": "^10",
|
|
2827
|
-
pkgroll: "^2.14.2",
|
|
2828
|
-
"release-it": "^19.0.4",
|
|
2829
|
-
shx: "^0.3.3",
|
|
2830
|
-
"ts-node": "^10",
|
|
2831
|
-
tsx: "^4.20.3",
|
|
2832
|
-
typescript: "^5",
|
|
2833
|
-
vitest: "^3.2.4"
|
|
2834
|
-
};
|
|
2835
|
-
var resolutions = {
|
|
2836
|
-
"whatwg-url": "14.2.0",
|
|
2837
|
-
"parse-path": "7.0.3",
|
|
2838
|
-
"@types/parse-path": "7.0.3"
|
|
2839
|
-
};
|
|
2840
|
-
var publishConfig = {
|
|
2841
|
-
registry: "https://registry.npmjs.org"
|
|
2842
|
-
};
|
|
2843
|
-
var packageManager = "yarn@1.22.22";
|
|
2844
|
-
var packageJson = {
|
|
2845
|
-
name: name,
|
|
2846
|
-
version: version,
|
|
2847
|
-
description: description,
|
|
2848
|
-
author: author,
|
|
2849
|
-
license: license,
|
|
2850
|
-
type: type,
|
|
2851
|
-
homepage: homepage,
|
|
2852
|
-
bugs: bugs,
|
|
2853
|
-
repository: repository,
|
|
2854
|
-
bin: bin,
|
|
2855
|
-
main: main,
|
|
2856
|
-
module: module,
|
|
2857
|
-
types: types,
|
|
2858
|
-
exports: exports,
|
|
2859
|
-
files: files,
|
|
2860
|
-
scripts: scripts,
|
|
2861
|
-
dependencies: dependencies,
|
|
2862
|
-
devDependencies: devDependencies,
|
|
2863
|
-
resolutions: resolutions,
|
|
2864
|
-
publishConfig: publishConfig,
|
|
2865
|
-
packageManager: packageManager
|
|
2866
|
-
};
|
|
2867
|
-
|
|
2868
2745
|
function run(args, options) {
|
|
2869
2746
|
const RUNNER_PATH = resolve$1(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
2870
2747
|
return new Promise((resolve2, reject) => {
|
|
@@ -3124,129 +3001,6 @@ function registerHandlers(session) {
|
|
|
3124
3001
|
});
|
|
3125
3002
|
}
|
|
3126
3003
|
|
|
3127
|
-
const defaultSettings = {
|
|
3128
|
-
onboardingCompleted: false
|
|
3129
|
-
};
|
|
3130
|
-
async function readSettings() {
|
|
3131
|
-
if (!existsSync(configuration.settingsFile)) {
|
|
3132
|
-
return { ...defaultSettings };
|
|
3133
|
-
}
|
|
3134
|
-
try {
|
|
3135
|
-
const content = await readFile(configuration.settingsFile, "utf8");
|
|
3136
|
-
return JSON.parse(content);
|
|
3137
|
-
} catch {
|
|
3138
|
-
return { ...defaultSettings };
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
async function updateSettings(updater) {
|
|
3142
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
3143
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
3144
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
3145
|
-
const lockFile = configuration.settingsFile + ".lock";
|
|
3146
|
-
const tmpFile = configuration.settingsFile + ".tmp";
|
|
3147
|
-
let fileHandle;
|
|
3148
|
-
let attempts = 0;
|
|
3149
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
3150
|
-
try {
|
|
3151
|
-
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
3152
|
-
break;
|
|
3153
|
-
} catch (err) {
|
|
3154
|
-
if (err.code === "EEXIST") {
|
|
3155
|
-
attempts++;
|
|
3156
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
3157
|
-
try {
|
|
3158
|
-
const stats = await stat$1(lockFile);
|
|
3159
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
3160
|
-
await unlink(lockFile).catch(() => {
|
|
3161
|
-
});
|
|
3162
|
-
}
|
|
3163
|
-
} catch {
|
|
3164
|
-
}
|
|
3165
|
-
} else {
|
|
3166
|
-
throw err;
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
if (!fileHandle) {
|
|
3171
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
3172
|
-
}
|
|
3173
|
-
try {
|
|
3174
|
-
const current = await readSettings() || { ...defaultSettings };
|
|
3175
|
-
const updated = await updater(current);
|
|
3176
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3177
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3178
|
-
}
|
|
3179
|
-
await writeFile$1(tmpFile, JSON.stringify(updated, null, 2));
|
|
3180
|
-
await rename(tmpFile, configuration.settingsFile);
|
|
3181
|
-
return updated;
|
|
3182
|
-
} finally {
|
|
3183
|
-
await fileHandle.close();
|
|
3184
|
-
await unlink(lockFile).catch(() => {
|
|
3185
|
-
});
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
3188
|
-
const credentialsSchema = z.object({
|
|
3189
|
-
secret: z.string().base64(),
|
|
3190
|
-
token: z.string()
|
|
3191
|
-
});
|
|
3192
|
-
async function readCredentials() {
|
|
3193
|
-
if (!existsSync(configuration.privateKeyFile)) {
|
|
3194
|
-
return null;
|
|
3195
|
-
}
|
|
3196
|
-
try {
|
|
3197
|
-
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
3198
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
3199
|
-
return {
|
|
3200
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
3201
|
-
token: credentials.token
|
|
3202
|
-
};
|
|
3203
|
-
} catch {
|
|
3204
|
-
return null;
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
async function writeCredentials(credentials) {
|
|
3208
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3209
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3210
|
-
}
|
|
3211
|
-
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
3212
|
-
secret: encodeBase64(credentials.secret),
|
|
3213
|
-
token: credentials.token
|
|
3214
|
-
}, null, 2));
|
|
3215
|
-
}
|
|
3216
|
-
async function clearCredentials() {
|
|
3217
|
-
if (existsSync(configuration.privateKeyFile)) {
|
|
3218
|
-
await unlink(configuration.privateKeyFile);
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
async function clearMachineId() {
|
|
3222
|
-
await updateSettings((settings) => ({
|
|
3223
|
-
...settings,
|
|
3224
|
-
machineId: void 0
|
|
3225
|
-
}));
|
|
3226
|
-
}
|
|
3227
|
-
async function readDaemonState() {
|
|
3228
|
-
try {
|
|
3229
|
-
if (!existsSync(configuration.daemonStateFile)) {
|
|
3230
|
-
return null;
|
|
3231
|
-
}
|
|
3232
|
-
const content = await readFile(configuration.daemonStateFile, "utf-8");
|
|
3233
|
-
return JSON.parse(content);
|
|
3234
|
-
} catch (error) {
|
|
3235
|
-
return null;
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
async function writeDaemonState(state) {
|
|
3239
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3240
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3241
|
-
}
|
|
3242
|
-
await writeFile$1(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
3243
|
-
}
|
|
3244
|
-
async function clearDaemonState() {
|
|
3245
|
-
if (existsSync(configuration.daemonStateFile)) {
|
|
3246
|
-
await unlink(configuration.daemonStateFile);
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
3004
|
class MessageQueue2 {
|
|
3251
3005
|
queue = [];
|
|
3252
3006
|
// Made public for testing
|
|
@@ -3614,8 +3368,9 @@ function startCaffeinate() {
|
|
|
3614
3368
|
}
|
|
3615
3369
|
}
|
|
3616
3370
|
let isStopping = false;
|
|
3617
|
-
function stopCaffeinate() {
|
|
3371
|
+
async function stopCaffeinate() {
|
|
3618
3372
|
if (isStopping) {
|
|
3373
|
+
logger.debug("[caffeinate] Already stopping, skipping");
|
|
3619
3374
|
return;
|
|
3620
3375
|
}
|
|
3621
3376
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
@@ -3623,14 +3378,13 @@ function stopCaffeinate() {
|
|
|
3623
3378
|
logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
3624
3379
|
try {
|
|
3625
3380
|
caffeinateProcess.kill("SIGTERM");
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
}, 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;
|
|
3634
3388
|
} catch (error) {
|
|
3635
3389
|
logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3636
3390
|
isStopping = false;
|
|
@@ -3654,12 +3408,10 @@ function setupCleanupHandlers() {
|
|
|
3654
3408
|
process.on("uncaughtException", (error) => {
|
|
3655
3409
|
logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
|
|
3656
3410
|
cleanup();
|
|
3657
|
-
process.exit(1);
|
|
3658
3411
|
});
|
|
3659
3412
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3660
3413
|
logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
|
|
3661
3414
|
cleanup();
|
|
3662
|
-
process.exit(1);
|
|
3663
3415
|
});
|
|
3664
3416
|
}
|
|
3665
3417
|
|
|
@@ -3708,37 +3460,97 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3708
3460
|
});
|
|
3709
3461
|
}
|
|
3710
3462
|
|
|
3711
|
-
async function
|
|
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
|
+
}
|
|
3712
3472
|
try {
|
|
3713
|
-
|
|
3714
|
-
if (!state) {
|
|
3715
|
-
return false;
|
|
3716
|
-
}
|
|
3717
|
-
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3718
|
-
if (!isRunning) {
|
|
3719
|
-
logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3720
|
-
await cleanupDaemonState();
|
|
3721
|
-
return false;
|
|
3722
|
-
}
|
|
3723
|
-
return true;
|
|
3473
|
+
process.kill(state.pid, 0);
|
|
3724
3474
|
} catch (error) {
|
|
3725
|
-
|
|
3726
|
-
|
|
3475
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
3476
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3477
|
+
return {
|
|
3478
|
+
error: errorMessage
|
|
3479
|
+
};
|
|
3727
3480
|
}
|
|
3728
|
-
}
|
|
3729
|
-
async function getDaemonState() {
|
|
3730
3481
|
try {
|
|
3731
|
-
|
|
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();
|
|
3732
3498
|
} catch (error) {
|
|
3733
|
-
|
|
3734
|
-
|
|
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
|
+
};
|
|
3735
3504
|
}
|
|
3736
3505
|
}
|
|
3737
|
-
async function
|
|
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
|
+
}
|
|
3738
3528
|
try {
|
|
3739
|
-
process.kill(pid, 0);
|
|
3529
|
+
process.kill(state.pid, 0);
|
|
3740
3530
|
return true;
|
|
3741
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);
|
|
3742
3554
|
return false;
|
|
3743
3555
|
}
|
|
3744
3556
|
}
|
|
@@ -3750,47 +3562,102 @@ async function cleanupDaemonState() {
|
|
|
3750
3562
|
logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3751
3563
|
}
|
|
3752
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
|
+
|
|
3753
3604
|
function findAllHappyProcesses() {
|
|
3754
3605
|
try {
|
|
3755
|
-
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3756
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3757
3606
|
const allProcesses = [];
|
|
3758
|
-
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3761
|
-
const
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
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 });
|
|
3774
3630
|
}
|
|
3775
|
-
|
|
3631
|
+
} catch {
|
|
3776
3632
|
}
|
|
3777
3633
|
try {
|
|
3778
|
-
const devOutput = execSync('ps aux | grep -E "
|
|
3634
|
+
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3779
3635
|
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3780
3636
|
for (const line of devLines) {
|
|
3781
3637
|
const parts = line.trim().split(/\s+/);
|
|
3782
3638
|
if (parts.length < 11) continue;
|
|
3783
3639
|
const pid = parseInt(parts[1]);
|
|
3784
3640
|
const command = parts.slice(10).join(" ");
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
const pwdOutput = execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3788
|
-
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3789
|
-
} catch {
|
|
3641
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3642
|
+
continue;
|
|
3790
3643
|
}
|
|
3791
|
-
|
|
3792
|
-
|
|
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";
|
|
3793
3659
|
}
|
|
3660
|
+
allProcesses.push({ pid, command, type });
|
|
3794
3661
|
}
|
|
3795
3662
|
} catch {
|
|
3796
3663
|
}
|
|
@@ -3801,18 +3668,39 @@ function findAllHappyProcesses() {
|
|
|
3801
3668
|
}
|
|
3802
3669
|
function findRunawayHappyProcesses() {
|
|
3803
3670
|
try {
|
|
3804
|
-
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3805
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3806
3671
|
const processes = [];
|
|
3807
|
-
|
|
3808
|
-
const
|
|
3809
|
-
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
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
|
+
}
|
|
3815
3702
|
}
|
|
3703
|
+
} catch {
|
|
3816
3704
|
}
|
|
3817
3705
|
return processes;
|
|
3818
3706
|
} catch (error) {
|
|
@@ -3822,10 +3710,10 @@ function findRunawayHappyProcesses() {
|
|
|
3822
3710
|
async function killRunawayHappyProcesses() {
|
|
3823
3711
|
const runawayProcesses = findRunawayHappyProcesses();
|
|
3824
3712
|
const errors = [];
|
|
3825
|
-
|
|
3826
|
-
for (const { pid, command } of runawayProcesses) {
|
|
3713
|
+
const killPromises = runawayProcesses.map(async ({ pid, command }) => {
|
|
3827
3714
|
try {
|
|
3828
3715
|
process.kill(pid, "SIGTERM");
|
|
3716
|
+
console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
|
|
3829
3717
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3830
3718
|
try {
|
|
3831
3719
|
process.kill(pid, 0);
|
|
@@ -3833,66 +3721,19 @@ async function killRunawayHappyProcesses() {
|
|
|
3833
3721
|
process.kill(pid, "SIGKILL");
|
|
3834
3722
|
} catch {
|
|
3835
3723
|
}
|
|
3836
|
-
killed
|
|
3837
|
-
|
|
3724
|
+
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3725
|
+
return { success: true, pid, command };
|
|
3838
3726
|
} catch (error) {
|
|
3839
|
-
|
|
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 };
|
|
3840
3731
|
}
|
|
3841
|
-
}
|
|
3732
|
+
});
|
|
3733
|
+
const results = await Promise.all(killPromises);
|
|
3734
|
+
const killed = results.filter((r) => r.success).length;
|
|
3842
3735
|
return { killed, errors };
|
|
3843
3736
|
}
|
|
3844
|
-
async function stopDaemon() {
|
|
3845
|
-
try {
|
|
3846
|
-
stopCaffeinate();
|
|
3847
|
-
logger.debug("Stopped sleep prevention");
|
|
3848
|
-
const state = await getDaemonState();
|
|
3849
|
-
if (!state) {
|
|
3850
|
-
logger.debug("No daemon state found");
|
|
3851
|
-
return;
|
|
3852
|
-
}
|
|
3853
|
-
logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3854
|
-
try {
|
|
3855
|
-
const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
|
|
3856
|
-
await stopDaemonHttp();
|
|
3857
|
-
await waitForProcessDeath(state.pid, 5e3);
|
|
3858
|
-
logger.debug("Daemon stopped gracefully via HTTP");
|
|
3859
|
-
return;
|
|
3860
|
-
} catch (error) {
|
|
3861
|
-
logger.debug("HTTP stop failed, will force kill", error);
|
|
3862
|
-
}
|
|
3863
|
-
try {
|
|
3864
|
-
process.kill(state.pid, "SIGKILL");
|
|
3865
|
-
logger.debug("Force killed daemon");
|
|
3866
|
-
} catch (error) {
|
|
3867
|
-
logger.debug("Daemon already dead");
|
|
3868
|
-
}
|
|
3869
|
-
} catch (error) {
|
|
3870
|
-
logger.debug("Error stopping daemon", error);
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
async function waitForProcessDeath(pid, timeout) {
|
|
3874
|
-
const start = Date.now();
|
|
3875
|
-
while (Date.now() - start < timeout) {
|
|
3876
|
-
try {
|
|
3877
|
-
process.kill(pid, 0);
|
|
3878
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3879
|
-
} catch {
|
|
3880
|
-
return;
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
throw new Error("Process did not die within timeout");
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
3887
|
-
__proto__: null,
|
|
3888
|
-
cleanupDaemonState: cleanupDaemonState,
|
|
3889
|
-
findAllHappyProcesses: findAllHappyProcesses,
|
|
3890
|
-
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3891
|
-
getDaemonState: getDaemonState,
|
|
3892
|
-
isDaemonRunning: isDaemonRunning,
|
|
3893
|
-
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3894
|
-
stopDaemon: stopDaemon
|
|
3895
|
-
});
|
|
3896
3737
|
|
|
3897
3738
|
function getEnvironmentInfo() {
|
|
3898
3739
|
return {
|
|
@@ -3907,7 +3748,15 @@ function getEnvironmentInfo() {
|
|
|
3907
3748
|
processArgv: process.argv,
|
|
3908
3749
|
happyDir: configuration?.happyHomeDir,
|
|
3909
3750
|
serverUrl: configuration?.serverUrl,
|
|
3910
|
-
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
|
|
3911
3760
|
};
|
|
3912
3761
|
}
|
|
3913
3762
|
function getLogFiles(logDir) {
|
|
@@ -3919,62 +3768,67 @@ function getLogFiles(logDir) {
|
|
|
3919
3768
|
const path = join(logDir, file);
|
|
3920
3769
|
const stats = statSync(path);
|
|
3921
3770
|
return { file, path, modified: stats.mtime };
|
|
3922
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
3771
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
3923
3772
|
} catch {
|
|
3924
3773
|
return [];
|
|
3925
3774
|
}
|
|
3926
3775
|
}
|
|
3927
|
-
async function runDoctorCommand() {
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3931
|
-
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3932
|
-
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3933
|
-
console.log("");
|
|
3934
|
-
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3935
|
-
const projectRoot = projectPath();
|
|
3936
|
-
const wrapperPath = join(projectRoot, "bin", "happy.mjs");
|
|
3937
|
-
const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
|
|
3938
|
-
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3939
|
-
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3940
|
-
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3941
|
-
console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3942
|
-
console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3943
|
-
console.log("");
|
|
3944
|
-
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3945
|
-
console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
|
|
3946
|
-
console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
|
|
3947
|
-
console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
|
|
3948
|
-
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3949
|
-
const env = getEnvironmentInfo();
|
|
3950
|
-
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3951
|
-
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3952
|
-
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3953
|
-
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3954
|
-
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3955
|
-
try {
|
|
3956
|
-
const settings = await readSettings();
|
|
3957
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3958
|
-
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3959
|
-
} catch (error) {
|
|
3960
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3961
|
-
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3776
|
+
async function runDoctorCommand(filter) {
|
|
3777
|
+
if (!filter) {
|
|
3778
|
+
filter = "all";
|
|
3962
3779
|
}
|
|
3963
|
-
console.log(chalk.bold("\n\u{
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
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"));
|
|
3970
3815
|
}
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
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"));
|
|
3975
3829
|
try {
|
|
3976
|
-
const isRunning = await
|
|
3977
|
-
const state = await
|
|
3830
|
+
const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3831
|
+
const state = await readDaemonState();
|
|
3978
3832
|
if (isRunning && state) {
|
|
3979
3833
|
console.log(chalk.green("\u2713 Daemon is running"));
|
|
3980
3834
|
console.log(` PID: ${state.pid}`);
|
|
@@ -4005,9 +3859,11 @@ async function runDoctorCommand() {
|
|
|
4005
3859
|
const typeLabels = {
|
|
4006
3860
|
"current": "\u{1F4CD} Current Process",
|
|
4007
3861
|
"daemon": "\u{1F916} Daemon",
|
|
3862
|
+
"daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
|
|
4008
3863
|
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
4009
3864
|
"user-session": "\u{1F464} User Sessions",
|
|
4010
3865
|
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3866
|
+
"dev-daemon-version-check": "\u{1F6E0}\uFE0F Dev Daemon Version Check (stuck)",
|
|
4011
3867
|
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
4012
3868
|
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
4013
3869
|
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
@@ -4021,190 +3877,54 @@ ${typeLabels[type] || type}:`));
|
|
|
4021
3877
|
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4022
3878
|
});
|
|
4023
3879
|
});
|
|
3880
|
+
} else {
|
|
3881
|
+
console.log(chalk.red("\u274C No happy processes found"));
|
|
4024
3882
|
}
|
|
4025
|
-
|
|
4026
|
-
if (runawayProcesses.length > 0) {
|
|
4027
|
-
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
4028
|
-
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
4029
|
-
runawayProcesses.forEach(({ pid, command }) => {
|
|
4030
|
-
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4031
|
-
});
|
|
4032
|
-
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
4033
|
-
}
|
|
4034
|
-
if (allProcesses.length > 1) {
|
|
3883
|
+
if (filter === "all" && allProcesses.length > 1) {
|
|
4035
3884
|
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
4036
|
-
console.log(chalk.gray("To
|
|
3885
|
+
console.log(chalk.gray("To clean up runaway processes: happy doctor clean"));
|
|
4037
3886
|
}
|
|
4038
3887
|
} catch (error) {
|
|
4039
3888
|
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
4040
3889
|
}
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
console.log(chalk.gray(` ${path}`));
|
|
4058
|
-
});
|
|
4059
|
-
} else {
|
|
4060
|
-
console.log(chalk.yellow("No daemon log files found"));
|
|
4061
|
-
}
|
|
4062
|
-
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
4063
|
-
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
4064
|
-
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
4065
|
-
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4066
|
-
}
|
|
4067
|
-
|
|
4068
|
-
async function daemonPost(path, body) {
|
|
4069
|
-
const state = await getDaemonState();
|
|
4070
|
-
if (!state?.httpPort) {
|
|
4071
|
-
throw new Error("No daemon running");
|
|
4072
|
-
}
|
|
4073
|
-
try {
|
|
4074
|
-
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4075
|
-
method: "POST",
|
|
4076
|
-
headers: { "Content-Type": "application/json" },
|
|
4077
|
-
body: JSON.stringify(body || {}),
|
|
4078
|
-
signal: AbortSignal.timeout(5e3)
|
|
4079
|
-
});
|
|
4080
|
-
if (!response.ok) {
|
|
4081
|
-
throw new Error(`HTTP ${response.status}`);
|
|
4082
|
-
}
|
|
4083
|
-
return await response.json();
|
|
4084
|
-
} catch (error) {
|
|
4085
|
-
logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
4086
|
-
throw error;
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4089
|
-
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
4090
|
-
await daemonPost("/session-started", {
|
|
4091
|
-
sessionId,
|
|
4092
|
-
metadata
|
|
4093
|
-
});
|
|
4094
|
-
}
|
|
4095
|
-
async function listDaemonSessions() {
|
|
4096
|
-
const result = await daemonPost("/list");
|
|
4097
|
-
return result.children || [];
|
|
4098
|
-
}
|
|
4099
|
-
async function stopDaemonSession(sessionId) {
|
|
4100
|
-
const result = await daemonPost("/stop-session", { sessionId });
|
|
4101
|
-
return result.success || false;
|
|
4102
|
-
}
|
|
4103
|
-
async function stopDaemonHttp() {
|
|
4104
|
-
await daemonPost("/stop");
|
|
4105
|
-
}
|
|
4106
|
-
|
|
4107
|
-
var controlClient = /*#__PURE__*/Object.freeze({
|
|
4108
|
-
__proto__: null,
|
|
4109
|
-
listDaemonSessions: listDaemonSessions,
|
|
4110
|
-
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
4111
|
-
stopDaemonHttp: stopDaemonHttp,
|
|
4112
|
-
stopDaemonSession: stopDaemonSession
|
|
4113
|
-
});
|
|
4114
|
-
|
|
4115
|
-
function startDaemonControlServer({
|
|
4116
|
-
getChildren,
|
|
4117
|
-
stopSession,
|
|
4118
|
-
spawnSession,
|
|
4119
|
-
requestShutdown,
|
|
4120
|
-
onHappySessionWebhook
|
|
4121
|
-
}) {
|
|
4122
|
-
return new Promise((resolve) => {
|
|
4123
|
-
const app = fastify({
|
|
4124
|
-
logger: false
|
|
4125
|
-
// We use our own logger
|
|
4126
|
-
});
|
|
4127
|
-
app.setValidatorCompiler(validatorCompiler);
|
|
4128
|
-
app.setSerializerCompiler(serializerCompiler);
|
|
4129
|
-
const typed = app.withTypeProvider();
|
|
4130
|
-
typed.post("/session-started", {
|
|
4131
|
-
schema: {
|
|
4132
|
-
body: z$1.object({
|
|
4133
|
-
sessionId: z$1.string(),
|
|
4134
|
-
metadata: z$1.any()
|
|
4135
|
-
// Metadata type from API
|
|
4136
|
-
})
|
|
4137
|
-
}
|
|
4138
|
-
}, async (request, reply) => {
|
|
4139
|
-
const { sessionId, metadata } = request.body;
|
|
4140
|
-
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4141
|
-
onHappySessionWebhook(sessionId, metadata);
|
|
4142
|
-
return { status: "ok" };
|
|
4143
|
-
});
|
|
4144
|
-
typed.post("/list", async (request, reply) => {
|
|
4145
|
-
const children = getChildren();
|
|
4146
|
-
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4147
|
-
return { children };
|
|
4148
|
-
});
|
|
4149
|
-
typed.post("/stop-session", {
|
|
4150
|
-
schema: {
|
|
4151
|
-
body: z$1.object({
|
|
4152
|
-
sessionId: z$1.string()
|
|
4153
|
-
})
|
|
4154
|
-
}
|
|
4155
|
-
}, async (request, reply) => {
|
|
4156
|
-
const { sessionId } = request.body;
|
|
4157
|
-
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4158
|
-
const success = stopSession(sessionId);
|
|
4159
|
-
return { success };
|
|
4160
|
-
});
|
|
4161
|
-
typed.post("/spawn-session", {
|
|
4162
|
-
schema: {
|
|
4163
|
-
body: z$1.object({
|
|
4164
|
-
directory: z$1.string(),
|
|
4165
|
-
sessionId: z$1.string().optional()
|
|
4166
|
-
})
|
|
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
|
+
}
|
|
4167
3906
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
}
|
|
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
|
+
}
|
|
4178
3917
|
} else {
|
|
4179
|
-
|
|
4180
|
-
return { error: "Failed to spawn session" };
|
|
3918
|
+
console.log(chalk.yellow("\nNo daemon log files found"));
|
|
4181
3919
|
}
|
|
4182
|
-
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
});
|
|
4191
|
-
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4192
|
-
if (err) {
|
|
4193
|
-
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4194
|
-
throw err;
|
|
4195
|
-
}
|
|
4196
|
-
const port = parseInt(address.split(":").pop());
|
|
4197
|
-
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4198
|
-
resolve({
|
|
4199
|
-
port,
|
|
4200
|
-
stop: async () => {
|
|
4201
|
-
logger.debug("[CONTROL SERVER] Stopping server");
|
|
4202
|
-
await app.close();
|
|
4203
|
-
logger.debug("[CONTROL SERVER] Server stopped");
|
|
4204
|
-
}
|
|
4205
|
-
});
|
|
4206
|
-
});
|
|
4207
|
-
});
|
|
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"));
|
|
4208
3928
|
}
|
|
4209
3929
|
|
|
4210
3930
|
function displayQRCode(url) {
|
|
@@ -4231,7 +3951,7 @@ async function openBrowser(url) {
|
|
|
4231
3951
|
return false;
|
|
4232
3952
|
}
|
|
4233
3953
|
logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
4234
|
-
await open
|
|
3954
|
+
await open(url);
|
|
4235
3955
|
logger.debug("[browser] Browser opened successfully");
|
|
4236
3956
|
return true;
|
|
4237
3957
|
} catch (error) {
|
|
@@ -4447,16 +4167,140 @@ function spawnHappyCLI(args, options = {}) {
|
|
|
4447
4167
|
directory = process.cwd();
|
|
4448
4168
|
}
|
|
4449
4169
|
const fullCommand = `happy ${args.join(" ")}`;
|
|
4450
|
-
logger.debug(`[
|
|
4170
|
+
logger.debug(`[SPAWN HAPPY CLI] Spawning: ${fullCommand} in ${directory}`);
|
|
4451
4171
|
const nodeArgs = [
|
|
4452
4172
|
"--no-warnings",
|
|
4453
4173
|
"--no-deprecation",
|
|
4454
4174
|
entrypoint,
|
|
4455
4175
|
...args
|
|
4456
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
|
+
}
|
|
4457
4182
|
return spawn$1("node", nodeArgs, options);
|
|
4458
4183
|
}
|
|
4459
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
|
+
|
|
4460
4304
|
const initialMachineMetadata = {
|
|
4461
4305
|
host: os.hostname(),
|
|
4462
4306
|
platform: os.platform(),
|
|
@@ -4465,37 +4309,79 @@ const initialMachineMetadata = {
|
|
|
4465
4309
|
happyHomeDir: configuration.happyHomeDir
|
|
4466
4310
|
};
|
|
4467
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
|
+
});
|
|
4468
4350
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4469
4351
|
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4470
|
-
const
|
|
4471
|
-
if (
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
logger.debug("[DAEMON RUN] Stale state found, cleaning up");
|
|
4479
|
-
await cleanupDaemonState();
|
|
4480
|
-
}
|
|
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);
|
|
4481
4360
|
}
|
|
4482
|
-
const
|
|
4483
|
-
if (
|
|
4484
|
-
logger.debug("[DAEMON RUN]
|
|
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);
|
|
4485
4365
|
}
|
|
4486
4366
|
try {
|
|
4367
|
+
const caffeinateStarted = startCaffeinate();
|
|
4368
|
+
if (caffeinateStarted) {
|
|
4369
|
+
logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
4370
|
+
}
|
|
4487
4371
|
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4488
4372
|
logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4489
4373
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4490
4374
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4491
4375
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4492
4376
|
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4377
|
+
logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
4493
4378
|
const pid = sessionMetadata.hostPid;
|
|
4494
4379
|
if (!pid) {
|
|
4495
|
-
logger.debug(`[DAEMON RUN] Session webhook missing hostPid for
|
|
4380
|
+
logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
|
|
4496
4381
|
return;
|
|
4497
4382
|
}
|
|
4498
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(", ")}`);
|
|
4499
4385
|
const existingSession = pidToTrackedSession.get(pid);
|
|
4500
4386
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4501
4387
|
existingSession.happySessionId = sessionId;
|
|
@@ -4519,8 +4405,37 @@ async function startDaemon() {
|
|
|
4519
4405
|
}
|
|
4520
4406
|
};
|
|
4521
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
|
+
}
|
|
4522
4438
|
try {
|
|
4523
|
-
const happyBinPath = join$1(projectPath(), "bin", "happy.mjs");
|
|
4524
4439
|
const args = [
|
|
4525
4440
|
"--happy-starting-mode",
|
|
4526
4441
|
"remote",
|
|
@@ -4535,12 +4450,14 @@ async function startDaemon() {
|
|
|
4535
4450
|
// Capture stdout/stderr for debugging
|
|
4536
4451
|
// env is inherited automatically from parent process
|
|
4537
4452
|
});
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
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
|
+
}
|
|
4544
4461
|
if (!happyProcess.pid) {
|
|
4545
4462
|
logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4546
4463
|
return null;
|
|
@@ -4549,7 +4466,9 @@ async function startDaemon() {
|
|
|
4549
4466
|
const trackedSession = {
|
|
4550
4467
|
startedBy: "daemon",
|
|
4551
4468
|
pid: happyProcess.pid,
|
|
4552
|
-
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
|
|
4553
4472
|
};
|
|
4554
4473
|
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4555
4474
|
happyProcess.on("exit", (code, signal) => {
|
|
@@ -4565,16 +4484,16 @@ async function startDaemon() {
|
|
|
4565
4484
|
}
|
|
4566
4485
|
});
|
|
4567
4486
|
logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4568
|
-
return new Promise((
|
|
4487
|
+
return new Promise((resolve2, reject) => {
|
|
4569
4488
|
const timeout = setTimeout(() => {
|
|
4570
4489
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4571
4490
|
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4572
|
-
|
|
4491
|
+
resolve2(trackedSession);
|
|
4573
4492
|
}, 1e4);
|
|
4574
4493
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4575
4494
|
clearTimeout(timeout);
|
|
4576
4495
|
logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4577
|
-
|
|
4496
|
+
resolve2(completedSession);
|
|
4578
4497
|
});
|
|
4579
4498
|
});
|
|
4580
4499
|
} catch (error) {
|
|
@@ -4613,10 +4532,6 @@ async function startDaemon() {
|
|
|
4613
4532
|
logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4614
4533
|
pidToTrackedSession.delete(pid);
|
|
4615
4534
|
};
|
|
4616
|
-
let requestShutdown;
|
|
4617
|
-
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4618
|
-
requestShutdown = resolve;
|
|
4619
|
-
});
|
|
4620
4535
|
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4621
4536
|
getChildren: getCurrentChildren,
|
|
4622
4537
|
stopSession,
|
|
@@ -4627,10 +4542,11 @@ async function startDaemon() {
|
|
|
4627
4542
|
const fileState = {
|
|
4628
4543
|
pid: process.pid,
|
|
4629
4544
|
httpPort: controlPort,
|
|
4630
|
-
startTime: (/* @__PURE__ */ new Date()).
|
|
4631
|
-
startedWithCliVersion: packageJson.version
|
|
4545
|
+
startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4546
|
+
startedWithCliVersion: packageJson.version,
|
|
4547
|
+
daemonLogPath: logger.logFilePath
|
|
4632
4548
|
};
|
|
4633
|
-
|
|
4549
|
+
writeDaemonState(fileState);
|
|
4634
4550
|
logger.debug("[DAEMON RUN] Daemon state written");
|
|
4635
4551
|
const initialDaemonState = {
|
|
4636
4552
|
status: "offline",
|
|
@@ -4652,56 +4568,89 @@ async function startDaemon() {
|
|
|
4652
4568
|
requestShutdown: () => requestShutdown("happy-app")
|
|
4653
4569
|
});
|
|
4654
4570
|
apiMachine.connect();
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
status: "shutting-down",
|
|
4661
|
-
shutdownRequestedAt: Date.now(),
|
|
4662
|
-
shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
|
|
4663
|
-
}));
|
|
4664
|
-
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;
|
|
4665
4576
|
}
|
|
4666
|
-
|
|
4667
|
-
|
|
4577
|
+
heartbeatRunning = true;
|
|
4578
|
+
if (process.env.DEBUG) {
|
|
4579
|
+
logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
4668
4580
|
}
|
|
4669
|
-
|
|
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();
|
|
4670
4642
|
await stopControlServer();
|
|
4671
|
-
logger.debug("[DAEMON RUN] Control server stopped");
|
|
4672
4643
|
await cleanupDaemonState();
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
logger.debug("[DAEMON RUN]
|
|
4644
|
+
await stopCaffeinate();
|
|
4645
|
+
await releaseDaemonLock(daemonLockHandle);
|
|
4646
|
+
logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
|
|
4676
4647
|
process.exit(0);
|
|
4677
4648
|
};
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
});
|
|
4682
|
-
process.on("SIGTERM", () => {
|
|
4683
|
-
logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4684
|
-
cleanupAndShutdown("os-signal");
|
|
4685
|
-
});
|
|
4686
|
-
process.on("uncaughtException", (error) => {
|
|
4687
|
-
logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4688
|
-
cleanupAndShutdown("unknown");
|
|
4689
|
-
});
|
|
4690
|
-
process.on("unhandledRejection", (reason) => {
|
|
4691
|
-
logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4692
|
-
cleanupAndShutdown("unknown");
|
|
4693
|
-
});
|
|
4694
|
-
process.on("exit", () => {
|
|
4695
|
-
logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
4696
|
-
});
|
|
4697
|
-
logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4698
|
-
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4699
|
-
logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4700
|
-
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);
|
|
4701
4652
|
} catch (error) {
|
|
4702
|
-
logger.debug("[DAEMON RUN] Failed
|
|
4703
|
-
await cleanupDaemonState();
|
|
4704
|
-
stopCaffeinate();
|
|
4653
|
+
logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
4705
4654
|
process.exit(1);
|
|
4706
4655
|
}
|
|
4707
4656
|
}
|
|
@@ -4729,7 +4678,7 @@ async function startHappyServer(client) {
|
|
|
4729
4678
|
description: "Change the title of the current chat session",
|
|
4730
4679
|
title: "Change Chat Title",
|
|
4731
4680
|
inputSchema: {
|
|
4732
|
-
title: z
|
|
4681
|
+
title: z.string().describe("The new title for the chat session")
|
|
4733
4682
|
}
|
|
4734
4683
|
}, async (args) => {
|
|
4735
4684
|
const response = await handler(args.title);
|
|
@@ -4802,10 +4751,14 @@ async function start(credentials, options = {}) {
|
|
|
4802
4751
|
const settings = await readSettings();
|
|
4803
4752
|
let machineId = settings?.machineId;
|
|
4804
4753
|
if (!machineId) {
|
|
4805
|
-
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it
|
|
4806
|
-
|
|
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);
|
|
4807
4756
|
}
|
|
4808
4757
|
logger.debug(`Using machineId: ${machineId}`);
|
|
4758
|
+
await api.createMachineOrGetExistingAsIs({
|
|
4759
|
+
machineId,
|
|
4760
|
+
metadata: initialMachineMetadata
|
|
4761
|
+
});
|
|
4809
4762
|
let metadata = {
|
|
4810
4763
|
path: workingDirectory,
|
|
4811
4764
|
host: os$1.hostname(),
|
|
@@ -4820,14 +4773,12 @@ async function start(credentials, options = {}) {
|
|
|
4820
4773
|
};
|
|
4821
4774
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4822
4775
|
logger.debug(`Session created: ${response.id}`);
|
|
4823
|
-
await api.createMachineOrGetExistingAsIs({
|
|
4824
|
-
machineId,
|
|
4825
|
-
metadata: initialMachineMetadata
|
|
4826
|
-
});
|
|
4827
4776
|
try {
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
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 {
|
|
4831
4782
|
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
4832
4783
|
}
|
|
4833
4784
|
} catch (error) {
|
|
@@ -4849,7 +4800,7 @@ async function start(credentials, options = {}) {
|
|
|
4849
4800
|
const session = api.sessionSyncClient(response);
|
|
4850
4801
|
const happyServer = await startHappyServer(session);
|
|
4851
4802
|
logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4852
|
-
const logPath =
|
|
4803
|
+
const logPath = logger.logFilePath;
|
|
4853
4804
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
4854
4805
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
4855
4806
|
session.updateAgentState((currentState) => ({
|
|
@@ -5362,8 +5313,7 @@ async function handleAuthStatus() {
|
|
|
5362
5313
|
console.log(chalk.gray(`
|
|
5363
5314
|
Data directory: ${configuration.happyHomeDir}`));
|
|
5364
5315
|
try {
|
|
5365
|
-
const
|
|
5366
|
-
const running = await isDaemonRunning();
|
|
5316
|
+
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
5367
5317
|
if (running) {
|
|
5368
5318
|
console.log(chalk.green("\u2713 Daemon running"));
|
|
5369
5319
|
} else {
|
|
@@ -5404,9 +5354,19 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5404
5354
|
|
|
5405
5355
|
(async () => {
|
|
5406
5356
|
const args = process.argv.slice(2);
|
|
5407
|
-
|
|
5357
|
+
if (!args.includes("--version")) {
|
|
5358
|
+
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
5359
|
+
}
|
|
5408
5360
|
const subcommand = args[0];
|
|
5409
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
|
+
}
|
|
5410
5370
|
await runDoctorCommand();
|
|
5411
5371
|
return;
|
|
5412
5372
|
} else if (subcommand === "auth") {
|
|
@@ -5449,16 +5409,10 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5449
5409
|
try {
|
|
5450
5410
|
const sessions = await listDaemonSessions();
|
|
5451
5411
|
if (sessions.length === 0) {
|
|
5452
|
-
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)");
|
|
5453
5413
|
} else {
|
|
5454
5414
|
console.log("Active sessions:");
|
|
5455
|
-
|
|
5456
|
-
pid: s.pid,
|
|
5457
|
-
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
5458
|
-
startedBy: s.startedBy,
|
|
5459
|
-
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
5460
|
-
}));
|
|
5461
|
-
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
5415
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
5462
5416
|
}
|
|
5463
5417
|
} catch (error) {
|
|
5464
5418
|
console.log("No daemon running");
|
|
@@ -5486,7 +5440,7 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5486
5440
|
child.unref();
|
|
5487
5441
|
let started = false;
|
|
5488
5442
|
for (let i = 0; i < 50; i++) {
|
|
5489
|
-
if (await
|
|
5443
|
+
if (await checkIfDaemonRunningAndCleanupStaleState()) {
|
|
5490
5444
|
started = true;
|
|
5491
5445
|
break;
|
|
5492
5446
|
}
|
|
@@ -5506,28 +5460,14 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5506
5460
|
await stopDaemon();
|
|
5507
5461
|
process.exit(0);
|
|
5508
5462
|
} else if (daemonSubcommand === "status") {
|
|
5509
|
-
|
|
5510
|
-
if (!state) {
|
|
5511
|
-
console.log("Daemon is not running");
|
|
5512
|
-
} else {
|
|
5513
|
-
const isRunning = await isDaemonRunning();
|
|
5514
|
-
if (isRunning) {
|
|
5515
|
-
console.log("Daemon is running");
|
|
5516
|
-
console.log(` PID: ${state.pid}`);
|
|
5517
|
-
console.log(` Port: ${state.httpPort}`);
|
|
5518
|
-
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
5519
|
-
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
5520
|
-
} else {
|
|
5521
|
-
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
5522
|
-
}
|
|
5523
|
-
}
|
|
5463
|
+
await runDoctorCommand("daemon");
|
|
5524
5464
|
process.exit(0);
|
|
5525
|
-
} else if (daemonSubcommand === "
|
|
5526
|
-
const
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
console.log(
|
|
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);
|
|
5531
5471
|
}
|
|
5532
5472
|
process.exit(0);
|
|
5533
5473
|
} else if (daemonSubcommand === "install") {
|
|
@@ -5551,14 +5491,15 @@ ${chalk.bold("happy daemon")} - Daemon management
|
|
|
5551
5491
|
${chalk.bold("Usage:")}
|
|
5552
5492
|
happy daemon start Start the daemon (detached)
|
|
5553
5493
|
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5554
|
-
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5555
5494
|
happy daemon status Show daemon status
|
|
5556
5495
|
happy daemon list List active sessions
|
|
5557
|
-
|
|
5558
|
-
|
|
5496
|
+
|
|
5497
|
+
If you want to kill all happy related processes run
|
|
5498
|
+
${chalk.cyan("happy doctor clean")}
|
|
5559
5499
|
|
|
5560
5500
|
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5561
|
-
|
|
5501
|
+
|
|
5502
|
+
${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor clean")}
|
|
5562
5503
|
`);
|
|
5563
5504
|
}
|
|
5564
5505
|
return;
|
|
@@ -5566,8 +5507,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5566
5507
|
const options = {};
|
|
5567
5508
|
let showHelp = false;
|
|
5568
5509
|
let showVersion = false;
|
|
5569
|
-
let forceAuth = false;
|
|
5570
|
-
let forceAuthNew = false;
|
|
5571
5510
|
const unknownArgs = [];
|
|
5572
5511
|
for (let i = 0; i < args.length; i++) {
|
|
5573
5512
|
const arg = args[i];
|
|
@@ -5577,12 +5516,8 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5577
5516
|
} else if (arg === "-v" || arg === "--version") {
|
|
5578
5517
|
showVersion = true;
|
|
5579
5518
|
unknownArgs.push(arg);
|
|
5580
|
-
} else if (arg === "--auth" || arg === "--login") {
|
|
5581
|
-
|
|
5582
|
-
} else if (arg === "--force-auth") {
|
|
5583
|
-
forceAuthNew = true;
|
|
5584
|
-
} else if (arg === "--happy-starting-mode") {
|
|
5585
|
-
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]);
|
|
5586
5521
|
} else if (arg === "--yolo") {
|
|
5587
5522
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
5588
5523
|
} else if (arg === "--started-by") {
|
|
@@ -5602,29 +5537,23 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5602
5537
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
5603
5538
|
|
|
5604
5539
|
${chalk.bold("Usage:")}
|
|
5605
|
-
happy [options]
|
|
5606
|
-
happy auth Manage authentication
|
|
5607
|
-
happy
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
${chalk.bold("Happy Options:")}
|
|
5611
|
-
--help Show this help message
|
|
5612
|
-
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5613
|
-
--force-auth Force re-authentication
|
|
5614
|
-
|
|
5615
|
-
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5616
|
-
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
|
|
5617
5545
|
|
|
5618
5546
|
${chalk.bold("Examples:")}
|
|
5619
|
-
happy
|
|
5620
|
-
happy --yolo
|
|
5621
|
-
happy --verbose
|
|
5622
|
-
happy
|
|
5623
|
-
happy
|
|
5624
|
-
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
|
|
5625
5552
|
|
|
5626
5553
|
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5627
|
-
|
|
5554
|
+
|
|
5555
|
+
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5556
|
+
Use any claude flag exactly as you normally would.
|
|
5628
5557
|
|
|
5629
5558
|
${chalk.gray("\u2500".repeat(60))}
|
|
5630
5559
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
@@ -5642,30 +5571,9 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5642
5571
|
console.log(packageJson.version);
|
|
5643
5572
|
process.exit(0);
|
|
5644
5573
|
}
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
try {
|
|
5649
|
-
await stopDaemon();
|
|
5650
|
-
} catch {
|
|
5651
|
-
}
|
|
5652
|
-
await clearCredentials();
|
|
5653
|
-
await clearMachineId();
|
|
5654
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5655
|
-
credentials = result.credentials;
|
|
5656
|
-
} else if (forceAuth) {
|
|
5657
|
-
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5658
|
-
const res = await doAuth();
|
|
5659
|
-
if (!res) {
|
|
5660
|
-
process.exit(1);
|
|
5661
|
-
}
|
|
5662
|
-
await writeCredentials(res);
|
|
5663
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5664
|
-
credentials = result.credentials;
|
|
5665
|
-
} else {
|
|
5666
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5667
|
-
credentials = result.credentials;
|
|
5668
|
-
}
|
|
5574
|
+
const {
|
|
5575
|
+
credentials
|
|
5576
|
+
} = await authAndSetupMachineIfNeeded();
|
|
5669
5577
|
let settings = await readSettings();
|
|
5670
5578
|
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5671
5579
|
const shouldAutoStart = await new Promise((resolve) => {
|
|
@@ -5694,15 +5602,18 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5694
5602
|
}
|
|
5695
5603
|
}
|
|
5696
5604
|
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5697
|
-
logger.debug("
|
|
5698
|
-
if (!await
|
|
5605
|
+
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5606
|
+
if (!await isDaemonRunningSameVersion()) {
|
|
5607
|
+
logger.debug("Starting Happy background service...");
|
|
5699
5608
|
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5700
5609
|
detached: true,
|
|
5701
5610
|
stdio: "ignore",
|
|
5702
5611
|
env: process.env
|
|
5703
5612
|
});
|
|
5704
5613
|
daemonProcess.unref();
|
|
5705
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5614
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5615
|
+
} else {
|
|
5616
|
+
logger.debug("Happy background service is running & matches our version");
|
|
5706
5617
|
}
|
|
5707
5618
|
}
|
|
5708
5619
|
try {
|