happy-coder 0.6.4 → 0.7.1-beta.1
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 +1354 -692
- package/dist/index.mjs +1360 -698
- package/dist/lib.cjs +3 -11
- package/dist/lib.d.cts +162 -14
- package/dist/lib.d.mts +162 -14
- package/dist/lib.mjs +1 -1
- package/dist/{types-Dz5kZrVh.mjs → types-BZC9-exR.mjs} +413 -43
- package/dist/{types-BDtHM1DY.cjs → types-CzvFvJwf.cjs} +458 -89
- package/package.json +16 -8
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { l as logger,
|
|
2
|
+
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, c as configuration, e as encodeBase64, A as ApiClient, f as encodeBase64Url, g as decodeBase64 } from './types-BZC9-exR.mjs';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
4
|
import { spawn, execSync } from 'node:child_process';
|
|
5
5
|
import { resolve, join, dirname as dirname$1 } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
8
|
-
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync, watch, constants, readdirSync, statSync, rmSync } from 'node:fs';
|
|
9
9
|
import os, { homedir } from 'node:os';
|
|
10
10
|
import { dirname, resolve as resolve$1, join as join$1 } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
12
|
+
import { readFile, unlink, mkdir, writeFile as writeFile$1, open, stat as stat$1, rename } from 'node:fs/promises';
|
|
13
13
|
import { 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';
|
|
17
|
-
import
|
|
18
|
-
import
|
|
17
|
+
import 'node:events';
|
|
18
|
+
import 'socket.io-client';
|
|
19
19
|
import tweetnacl from 'tweetnacl';
|
|
20
20
|
import 'expo-server-sdk';
|
|
21
21
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -25,11 +25,13 @@ import * as z from 'zod';
|
|
|
25
25
|
import { z as z$1 } from 'zod';
|
|
26
26
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
27
27
|
import { promisify } from 'util';
|
|
28
|
-
import
|
|
28
|
+
import { createHash } from 'crypto';
|
|
29
29
|
import qrcode from 'qrcode-terminal';
|
|
30
|
-
import open from 'open';
|
|
31
|
-
import
|
|
32
|
-
import {
|
|
30
|
+
import open$1 from 'open';
|
|
31
|
+
import fastify from 'fastify';
|
|
32
|
+
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
33
|
+
import os$1 from 'os';
|
|
34
|
+
import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
33
35
|
|
|
34
36
|
class Session {
|
|
35
37
|
path;
|
|
@@ -83,7 +85,8 @@ class Session {
|
|
|
83
85
|
|
|
84
86
|
function getProjectPath(workingDirectory) {
|
|
85
87
|
const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
|
|
86
|
-
|
|
88
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
89
|
+
return join(claudeConfigDir, "projects", projectId);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
function claudeCheckSession(sessionId, path) {
|
|
@@ -2030,31 +2033,39 @@ class SDKToLogConverter {
|
|
|
2030
2033
|
}
|
|
2031
2034
|
|
|
2032
2035
|
async function claudeRemoteLauncher(session) {
|
|
2036
|
+
logger.debug("[claudeRemoteLauncher] Starting remote launcher");
|
|
2037
|
+
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
2038
|
+
logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
|
|
2033
2039
|
let messageBuffer = new MessageBuffer();
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2040
|
+
let inkInstance = null;
|
|
2041
|
+
if (hasTTY) {
|
|
2042
|
+
console.clear();
|
|
2043
|
+
inkInstance = render(React.createElement(RemoteModeDisplay, {
|
|
2044
|
+
messageBuffer,
|
|
2045
|
+
logPath: process.env.DEBUG ? session.logPath : void 0,
|
|
2046
|
+
onExit: async () => {
|
|
2047
|
+
logger.debug("[remote]: Exiting client via Ctrl-C");
|
|
2048
|
+
if (!exitReason) {
|
|
2049
|
+
exitReason = "exit";
|
|
2050
|
+
}
|
|
2051
|
+
await abort();
|
|
2052
|
+
},
|
|
2053
|
+
onSwitchToLocal: () => {
|
|
2054
|
+
logger.debug("[remote]: Switching to local mode via double space");
|
|
2055
|
+
doSwitch();
|
|
2042
2056
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2057
|
+
}), {
|
|
2058
|
+
exitOnCtrlC: false,
|
|
2059
|
+
patchConsole: false
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
if (hasTTY) {
|
|
2063
|
+
process.stdin.resume();
|
|
2064
|
+
if (process.stdin.isTTY) {
|
|
2065
|
+
process.stdin.setRawMode(true);
|
|
2048
2066
|
}
|
|
2049
|
-
|
|
2050
|
-
exitOnCtrlC: false,
|
|
2051
|
-
patchConsole: false
|
|
2052
|
-
});
|
|
2053
|
-
process.stdin.resume();
|
|
2054
|
-
if (process.stdin.isTTY) {
|
|
2055
|
-
process.stdin.setRawMode(true);
|
|
2067
|
+
process.stdin.setEncoding("utf8");
|
|
2056
2068
|
}
|
|
2057
|
-
process.stdin.setEncoding("utf8");
|
|
2058
2069
|
const scanner = await createSessionScanner({
|
|
2059
2070
|
sessionId: session.sessionId,
|
|
2060
2071
|
workingDirectory: session.path,
|
|
@@ -2274,7 +2285,9 @@ async function claudeRemoteLauncher(session) {
|
|
|
2274
2285
|
if (process.stdin.isTTY) {
|
|
2275
2286
|
process.stdin.setRawMode(false);
|
|
2276
2287
|
}
|
|
2277
|
-
inkInstance
|
|
2288
|
+
if (inkInstance) {
|
|
2289
|
+
inkInstance.unmount();
|
|
2290
|
+
}
|
|
2278
2291
|
messageBuffer.clear();
|
|
2279
2292
|
if (abortFuture) {
|
|
2280
2293
|
abortFuture.resolve(void 0);
|
|
@@ -2330,7 +2343,7 @@ async function loop(opts) {
|
|
|
2330
2343
|
}
|
|
2331
2344
|
|
|
2332
2345
|
var name = "happy-coder";
|
|
2333
|
-
var version = "0.
|
|
2346
|
+
var version = "0.7.1-beta.1";
|
|
2334
2347
|
var description = "Claude Code session sharing CLI";
|
|
2335
2348
|
var author = "Kirill Dubovitskiy";
|
|
2336
2349
|
var license = "MIT";
|
|
@@ -2374,15 +2387,21 @@ var files = [
|
|
|
2374
2387
|
"package.json"
|
|
2375
2388
|
];
|
|
2376
2389
|
var scripts = {
|
|
2377
|
-
|
|
2390
|
+
"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",
|
|
2391
|
+
typecheck: "tsc --noEmit",
|
|
2392
|
+
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2393
|
+
test: "yarn build && vitest run",
|
|
2378
2394
|
"test:watch": "vitest",
|
|
2379
|
-
|
|
2395
|
+
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2396
|
+
dev: "yarn build && npx tsx src/index.ts",
|
|
2397
|
+
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2398
|
+
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2380
2399
|
prepublishOnly: "yarn build && yarn test",
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
"
|
|
2384
|
-
"
|
|
2385
|
-
"publish
|
|
2400
|
+
"minor:publish": "yarn build && npm version minor && npm publish",
|
|
2401
|
+
"patch:publish": "yarn build && npm version patch && npm publish",
|
|
2402
|
+
"version:prerelease": "yarn build && npm version prerelease --preid=beta",
|
|
2403
|
+
"publish:prerelease": "npm publish --tag beta",
|
|
2404
|
+
"beta:publish": "yarn version:prerelease && yarn publish:prerelease"
|
|
2386
2405
|
};
|
|
2387
2406
|
var dependencies = {
|
|
2388
2407
|
"@anthropic-ai/claude-code": "^1.0.73",
|
|
@@ -2395,6 +2414,8 @@ var dependencies = {
|
|
|
2395
2414
|
axios: "^1.10.0",
|
|
2396
2415
|
chalk: "^5.4.1",
|
|
2397
2416
|
"expo-server-sdk": "^3.15.0",
|
|
2417
|
+
fastify: "^5.5.0",
|
|
2418
|
+
"fastify-type-provider-zod": "4.0.2",
|
|
2398
2419
|
"http-proxy": "^1.18.1",
|
|
2399
2420
|
"http-proxy-middleware": "^3.0.5",
|
|
2400
2421
|
ink: "^6.1.0",
|
|
@@ -2484,8 +2505,8 @@ function registerHandlers(session) {
|
|
|
2484
2505
|
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2485
2506
|
return {
|
|
2486
2507
|
success: true,
|
|
2487
|
-
stdout: stdout
|
|
2488
|
-
stderr: stderr
|
|
2508
|
+
stdout: stdout ? stdout.toString() : "",
|
|
2509
|
+
stderr: stderr ? stderr.toString() : "",
|
|
2489
2510
|
exitCode: 0
|
|
2490
2511
|
};
|
|
2491
2512
|
} catch (error) {
|
|
@@ -2501,8 +2522,8 @@ function registerHandlers(session) {
|
|
|
2501
2522
|
}
|
|
2502
2523
|
return {
|
|
2503
2524
|
success: false,
|
|
2504
|
-
stdout: execError.stdout
|
|
2505
|
-
stderr: execError.stderr
|
|
2525
|
+
stdout: execError.stdout ? execError.stdout.toString() : "",
|
|
2526
|
+
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
|
|
2506
2527
|
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
2507
2528
|
error: execError.message || "Command failed"
|
|
2508
2529
|
};
|
|
@@ -2669,8 +2690,8 @@ function registerHandlers(session) {
|
|
|
2669
2690
|
return {
|
|
2670
2691
|
success: true,
|
|
2671
2692
|
exitCode: result.exitCode,
|
|
2672
|
-
stdout: result.stdout,
|
|
2673
|
-
stderr: result.stderr
|
|
2693
|
+
stdout: result.stdout.toString(),
|
|
2694
|
+
stderr: result.stderr.toString()
|
|
2674
2695
|
};
|
|
2675
2696
|
} catch (error) {
|
|
2676
2697
|
logger.debug("Failed to run ripgrep:", error);
|
|
@@ -2680,6 +2701,26 @@ function registerHandlers(session) {
|
|
|
2680
2701
|
};
|
|
2681
2702
|
}
|
|
2682
2703
|
});
|
|
2704
|
+
session.setHandler("killSession", async () => {
|
|
2705
|
+
logger.debug("Kill session request received");
|
|
2706
|
+
try {
|
|
2707
|
+
const response = {
|
|
2708
|
+
success: true,
|
|
2709
|
+
message: "Session termination acknowledged, exiting in 100ms"
|
|
2710
|
+
};
|
|
2711
|
+
setTimeout(() => {
|
|
2712
|
+
logger.debug("[KILL SESSION] Exiting process as requested");
|
|
2713
|
+
process.exit(0);
|
|
2714
|
+
}, 100);
|
|
2715
|
+
return response;
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
logger.debug("Failed to kill session:", error);
|
|
2718
|
+
return {
|
|
2719
|
+
success: false,
|
|
2720
|
+
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2683
2724
|
}
|
|
2684
2725
|
|
|
2685
2726
|
const defaultSettings = {
|
|
@@ -2696,11 +2737,52 @@ async function readSettings() {
|
|
|
2696
2737
|
return { ...defaultSettings };
|
|
2697
2738
|
}
|
|
2698
2739
|
}
|
|
2699
|
-
async function
|
|
2700
|
-
|
|
2701
|
-
|
|
2740
|
+
async function updateSettings(updater) {
|
|
2741
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
2742
|
+
const MAX_LOCK_ATTEMPTS = 50;
|
|
2743
|
+
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
2744
|
+
const lockFile = configuration.settingsFile + ".lock";
|
|
2745
|
+
const tmpFile = configuration.settingsFile + ".tmp";
|
|
2746
|
+
let fileHandle;
|
|
2747
|
+
let attempts = 0;
|
|
2748
|
+
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
2749
|
+
try {
|
|
2750
|
+
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
2751
|
+
break;
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
if (err.code === "EEXIST") {
|
|
2754
|
+
attempts++;
|
|
2755
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
2756
|
+
try {
|
|
2757
|
+
const stats = await stat$1(lockFile);
|
|
2758
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
2759
|
+
await unlink(lockFile).catch(() => {
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
} else {
|
|
2765
|
+
throw err;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if (!fileHandle) {
|
|
2770
|
+
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
2771
|
+
}
|
|
2772
|
+
try {
|
|
2773
|
+
const current = await readSettings() || { ...defaultSettings };
|
|
2774
|
+
const updated = await updater(current);
|
|
2775
|
+
if (!existsSync(configuration.happyHomeDir)) {
|
|
2776
|
+
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
2777
|
+
}
|
|
2778
|
+
await writeFile$1(tmpFile, JSON.stringify(updated, null, 2));
|
|
2779
|
+
await rename(tmpFile, configuration.settingsFile);
|
|
2780
|
+
return updated;
|
|
2781
|
+
} finally {
|
|
2782
|
+
await fileHandle.close();
|
|
2783
|
+
await unlink(lockFile).catch(() => {
|
|
2784
|
+
});
|
|
2702
2785
|
}
|
|
2703
|
-
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
2704
2786
|
}
|
|
2705
2787
|
const credentialsSchema = z.object({
|
|
2706
2788
|
secret: z.string().base64(),
|
|
@@ -2722,14 +2804,47 @@ async function readCredentials() {
|
|
|
2722
2804
|
}
|
|
2723
2805
|
}
|
|
2724
2806
|
async function writeCredentials(credentials) {
|
|
2725
|
-
if (!existsSync(configuration.
|
|
2726
|
-
await mkdir(configuration.
|
|
2807
|
+
if (!existsSync(configuration.happyHomeDir)) {
|
|
2808
|
+
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
2727
2809
|
}
|
|
2728
2810
|
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
2729
2811
|
secret: encodeBase64(credentials.secret),
|
|
2730
2812
|
token: credentials.token
|
|
2731
2813
|
}, null, 2));
|
|
2732
2814
|
}
|
|
2815
|
+
async function clearCredentials() {
|
|
2816
|
+
if (existsSync(configuration.privateKeyFile)) {
|
|
2817
|
+
await unlink(configuration.privateKeyFile);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
async function clearMachineId() {
|
|
2821
|
+
await updateSettings((settings) => ({
|
|
2822
|
+
...settings,
|
|
2823
|
+
machineId: void 0
|
|
2824
|
+
}));
|
|
2825
|
+
}
|
|
2826
|
+
async function readDaemonState() {
|
|
2827
|
+
try {
|
|
2828
|
+
if (!existsSync(configuration.daemonStateFile)) {
|
|
2829
|
+
return null;
|
|
2830
|
+
}
|
|
2831
|
+
const content = await readFile(configuration.daemonStateFile, "utf-8");
|
|
2832
|
+
return JSON.parse(content);
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
return null;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
async function writeDaemonState(state) {
|
|
2838
|
+
if (!existsSync(configuration.happyHomeDir)) {
|
|
2839
|
+
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
2840
|
+
}
|
|
2841
|
+
await writeFile$1(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
2842
|
+
}
|
|
2843
|
+
async function clearDaemonState() {
|
|
2844
|
+
if (existsSync(configuration.daemonStateFile)) {
|
|
2845
|
+
await unlink(configuration.daemonStateFile);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2733
2848
|
|
|
2734
2849
|
class MessageQueue2 {
|
|
2735
2850
|
constructor(modeHasher) {
|
|
@@ -3092,8 +3207,13 @@ function startCaffeinate() {
|
|
|
3092
3207
|
return false;
|
|
3093
3208
|
}
|
|
3094
3209
|
}
|
|
3210
|
+
let isStopping = false;
|
|
3095
3211
|
function stopCaffeinate() {
|
|
3212
|
+
if (isStopping) {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3096
3215
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3216
|
+
isStopping = true;
|
|
3097
3217
|
logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
3098
3218
|
try {
|
|
3099
3219
|
caffeinateProcess.kill("SIGTERM");
|
|
@@ -3103,9 +3223,11 @@ function stopCaffeinate() {
|
|
|
3103
3223
|
caffeinateProcess.kill("SIGKILL");
|
|
3104
3224
|
}
|
|
3105
3225
|
caffeinateProcess = null;
|
|
3226
|
+
isStopping = false;
|
|
3106
3227
|
}, 1e3);
|
|
3107
3228
|
} catch (error) {
|
|
3108
3229
|
logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3230
|
+
isStopping = false;
|
|
3109
3231
|
}
|
|
3110
3232
|
}
|
|
3111
3233
|
}
|
|
@@ -3180,30 +3302,441 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3180
3302
|
});
|
|
3181
3303
|
}
|
|
3182
3304
|
|
|
3305
|
+
async function isDaemonRunning() {
|
|
3306
|
+
try {
|
|
3307
|
+
const state = await getDaemonState();
|
|
3308
|
+
if (!state) {
|
|
3309
|
+
return false;
|
|
3310
|
+
}
|
|
3311
|
+
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3312
|
+
if (!isRunning) {
|
|
3313
|
+
logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3314
|
+
await cleanupDaemonState();
|
|
3315
|
+
return false;
|
|
3316
|
+
}
|
|
3317
|
+
return true;
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
logger.debug("[DAEMON RUN] Error checking daemon status", error);
|
|
3320
|
+
return false;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
async function getDaemonState() {
|
|
3324
|
+
try {
|
|
3325
|
+
return await readDaemonState();
|
|
3326
|
+
} catch (error) {
|
|
3327
|
+
logger.debug("[DAEMON RUN] Error reading daemon metadata", error);
|
|
3328
|
+
return null;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
async function isDaemonProcessRunning(pid) {
|
|
3332
|
+
try {
|
|
3333
|
+
process.kill(pid, 0);
|
|
3334
|
+
return true;
|
|
3335
|
+
} catch {
|
|
3336
|
+
return false;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
async function cleanupDaemonState() {
|
|
3340
|
+
try {
|
|
3341
|
+
await clearDaemonState();
|
|
3342
|
+
logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function findAllHappyProcesses() {
|
|
3348
|
+
try {
|
|
3349
|
+
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3350
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3351
|
+
const allProcesses = [];
|
|
3352
|
+
for (const line of lines) {
|
|
3353
|
+
const parts = line.trim().split(/\s+/);
|
|
3354
|
+
if (parts.length < 11) continue;
|
|
3355
|
+
const pid = parseInt(parts[1]);
|
|
3356
|
+
const command = parts.slice(10).join(" ");
|
|
3357
|
+
let type = "unknown";
|
|
3358
|
+
if (pid === process.pid) {
|
|
3359
|
+
type = "current";
|
|
3360
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3361
|
+
type = "daemon";
|
|
3362
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3363
|
+
type = "daemon-spawned-session";
|
|
3364
|
+
} else if (command.includes("doctor")) {
|
|
3365
|
+
type = "doctor";
|
|
3366
|
+
} else {
|
|
3367
|
+
type = "user-session";
|
|
3368
|
+
}
|
|
3369
|
+
allProcesses.push({ pid, command, type });
|
|
3370
|
+
}
|
|
3371
|
+
try {
|
|
3372
|
+
const devOutput = execSync('ps aux | grep -E "(tsx.*src/index.ts|yarn.*tsx)" | grep -v grep', { encoding: "utf8" });
|
|
3373
|
+
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3374
|
+
for (const line of devLines) {
|
|
3375
|
+
const parts = line.trim().split(/\s+/);
|
|
3376
|
+
if (parts.length < 11) continue;
|
|
3377
|
+
const pid = parseInt(parts[1]);
|
|
3378
|
+
const command = parts.slice(10).join(" ");
|
|
3379
|
+
let workingDir = "";
|
|
3380
|
+
try {
|
|
3381
|
+
const pwdOutput = execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3382
|
+
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3383
|
+
} catch {
|
|
3384
|
+
}
|
|
3385
|
+
if (workingDir.includes("happy-cli")) {
|
|
3386
|
+
allProcesses.push({ pid, command, type: "dev-session" });
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
} catch {
|
|
3390
|
+
}
|
|
3391
|
+
return allProcesses;
|
|
3392
|
+
} catch (error) {
|
|
3393
|
+
return [];
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
function findRunawayHappyProcesses() {
|
|
3397
|
+
try {
|
|
3398
|
+
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3399
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3400
|
+
const processes = [];
|
|
3401
|
+
for (const line of lines) {
|
|
3402
|
+
const parts = line.trim().split(/\s+/);
|
|
3403
|
+
if (parts.length < 11) continue;
|
|
3404
|
+
const pid = parseInt(parts[1]);
|
|
3405
|
+
const command = parts.slice(10).join(" ");
|
|
3406
|
+
if (pid === process.pid) continue;
|
|
3407
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3408
|
+
processes.push({ pid, command });
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
return processes;
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
return [];
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
async function killRunawayHappyProcesses() {
|
|
3417
|
+
const runawayProcesses = findRunawayHappyProcesses();
|
|
3418
|
+
const errors = [];
|
|
3419
|
+
let killed = 0;
|
|
3420
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3421
|
+
try {
|
|
3422
|
+
process.kill(pid, "SIGTERM");
|
|
3423
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3424
|
+
try {
|
|
3425
|
+
process.kill(pid, 0);
|
|
3426
|
+
console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
|
|
3427
|
+
process.kill(pid, "SIGKILL");
|
|
3428
|
+
} catch {
|
|
3429
|
+
}
|
|
3430
|
+
killed++;
|
|
3431
|
+
console.log(`Killed runaway process PID ${pid}: ${command}`);
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
errors.push({ pid, error: error.message });
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
return { killed, errors };
|
|
3437
|
+
}
|
|
3438
|
+
async function stopDaemon() {
|
|
3439
|
+
try {
|
|
3440
|
+
stopCaffeinate();
|
|
3441
|
+
logger.debug("Stopped sleep prevention");
|
|
3442
|
+
const state = await getDaemonState();
|
|
3443
|
+
if (!state) {
|
|
3444
|
+
logger.debug("No daemon state found");
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3448
|
+
try {
|
|
3449
|
+
const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
|
|
3450
|
+
await stopDaemonHttp();
|
|
3451
|
+
await waitForProcessDeath(state.pid, 5e3);
|
|
3452
|
+
logger.debug("Daemon stopped gracefully via HTTP");
|
|
3453
|
+
return;
|
|
3454
|
+
} catch (error) {
|
|
3455
|
+
logger.debug("HTTP stop failed, will force kill", error);
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
process.kill(state.pid, "SIGKILL");
|
|
3459
|
+
logger.debug("Force killed daemon");
|
|
3460
|
+
} catch (error) {
|
|
3461
|
+
logger.debug("Daemon already dead");
|
|
3462
|
+
}
|
|
3463
|
+
} catch (error) {
|
|
3464
|
+
logger.debug("Error stopping daemon", error);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3468
|
+
const start = Date.now();
|
|
3469
|
+
while (Date.now() - start < timeout) {
|
|
3470
|
+
try {
|
|
3471
|
+
process.kill(pid, 0);
|
|
3472
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3473
|
+
} catch {
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
throw new Error("Process did not die within timeout");
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
var utils = /*#__PURE__*/Object.freeze({
|
|
3481
|
+
__proto__: null,
|
|
3482
|
+
cleanupDaemonState: cleanupDaemonState,
|
|
3483
|
+
findAllHappyProcesses: findAllHappyProcesses,
|
|
3484
|
+
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3485
|
+
getDaemonState: getDaemonState,
|
|
3486
|
+
isDaemonRunning: isDaemonRunning,
|
|
3487
|
+
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3488
|
+
stopDaemon: stopDaemon
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
function getEnvironmentInfo() {
|
|
3492
|
+
return {
|
|
3493
|
+
PWD: process.env.PWD,
|
|
3494
|
+
HAPPY_HOME_DIR: process.env.HAPPY_HOME_DIR,
|
|
3495
|
+
HAPPY_SERVER_URL: process.env.HAPPY_SERVER_URL,
|
|
3496
|
+
HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT,
|
|
3497
|
+
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING,
|
|
3498
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
3499
|
+
DEBUG: process.env.DEBUG,
|
|
3500
|
+
workingDirectory: process.cwd(),
|
|
3501
|
+
processArgv: process.argv,
|
|
3502
|
+
happyDir: configuration?.happyHomeDir,
|
|
3503
|
+
serverUrl: configuration?.serverUrl,
|
|
3504
|
+
logsDir: configuration?.logsDir
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
function getLogFiles(logDir) {
|
|
3508
|
+
if (!existsSync(logDir)) {
|
|
3509
|
+
return [];
|
|
3510
|
+
}
|
|
3511
|
+
try {
|
|
3512
|
+
return readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
|
|
3513
|
+
const path = join(logDir, file);
|
|
3514
|
+
const stats = statSync(path);
|
|
3515
|
+
return { file, path, modified: stats.mtime };
|
|
3516
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10);
|
|
3517
|
+
} catch {
|
|
3518
|
+
return [];
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
async function runDoctorCommand() {
|
|
3522
|
+
console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
|
|
3523
|
+
console.log(chalk.bold("\u{1F4CB} Basic Information"));
|
|
3524
|
+
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3525
|
+
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3526
|
+
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3527
|
+
console.log("");
|
|
3528
|
+
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3529
|
+
console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
|
|
3530
|
+
console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
|
|
3531
|
+
console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
|
|
3532
|
+
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3533
|
+
const env = getEnvironmentInfo();
|
|
3534
|
+
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3535
|
+
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3536
|
+
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3537
|
+
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3538
|
+
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3539
|
+
try {
|
|
3540
|
+
const settings = await readSettings();
|
|
3541
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3542
|
+
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3543
|
+
} catch (error) {
|
|
3544
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3545
|
+
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3546
|
+
}
|
|
3547
|
+
console.log(chalk.bold("\n\u{1F510} Authentication"));
|
|
3548
|
+
try {
|
|
3549
|
+
const credentials = await readCredentials();
|
|
3550
|
+
if (credentials) {
|
|
3551
|
+
console.log(chalk.green("\u2713 Authenticated (credentials found)"));
|
|
3552
|
+
} else {
|
|
3553
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
|
|
3554
|
+
}
|
|
3555
|
+
} catch (error) {
|
|
3556
|
+
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3557
|
+
}
|
|
3558
|
+
console.log(chalk.bold("\n\u{1F916} Daemon Status"));
|
|
3559
|
+
try {
|
|
3560
|
+
const isRunning = await isDaemonRunning();
|
|
3561
|
+
const state = await getDaemonState();
|
|
3562
|
+
if (isRunning && state) {
|
|
3563
|
+
console.log(chalk.green("\u2713 Daemon is running"));
|
|
3564
|
+
console.log(` PID: ${state.pid}`);
|
|
3565
|
+
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
3566
|
+
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
3567
|
+
if (state.httpPort) {
|
|
3568
|
+
console.log(` HTTP Port: ${state.httpPort}`);
|
|
3569
|
+
}
|
|
3570
|
+
} else if (state && !isRunning) {
|
|
3571
|
+
console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists but process not running (stale)"));
|
|
3572
|
+
} else {
|
|
3573
|
+
console.log(chalk.red("\u274C Daemon is not running"));
|
|
3574
|
+
}
|
|
3575
|
+
if (state) {
|
|
3576
|
+
console.log(chalk.bold("\n\u{1F4C4} Daemon State:"));
|
|
3577
|
+
console.log(chalk.blue(`Location: ${configuration.daemonStateFile}`));
|
|
3578
|
+
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3579
|
+
}
|
|
3580
|
+
const allProcesses = findAllHappyProcesses();
|
|
3581
|
+
if (allProcesses.length > 0) {
|
|
3582
|
+
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3583
|
+
const grouped = allProcesses.reduce((groups, process2) => {
|
|
3584
|
+
if (!groups[process2.type]) groups[process2.type] = [];
|
|
3585
|
+
groups[process2.type].push(process2);
|
|
3586
|
+
return groups;
|
|
3587
|
+
}, {});
|
|
3588
|
+
Object.entries(grouped).forEach(([type, processes]) => {
|
|
3589
|
+
const typeLabels = {
|
|
3590
|
+
"current": "\u{1F4CD} Current Process",
|
|
3591
|
+
"daemon": "\u{1F916} Daemon",
|
|
3592
|
+
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
3593
|
+
"user-session": "\u{1F464} User Sessions",
|
|
3594
|
+
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3595
|
+
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
3596
|
+
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
3597
|
+
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
3598
|
+
"doctor": "\u{1FA7A} Doctor",
|
|
3599
|
+
"unknown": "\u2753 Unknown"
|
|
3600
|
+
};
|
|
3601
|
+
console.log(chalk.blue(`
|
|
3602
|
+
${typeLabels[type] || type}:`));
|
|
3603
|
+
processes.forEach(({ pid, command }) => {
|
|
3604
|
+
const color = type === "current" ? chalk.green : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
|
|
3605
|
+
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
3606
|
+
});
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
const runawayProcesses = findRunawayHappyProcesses();
|
|
3610
|
+
if (runawayProcesses.length > 0) {
|
|
3611
|
+
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
3612
|
+
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
3613
|
+
runawayProcesses.forEach(({ pid, command }) => {
|
|
3614
|
+
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
3615
|
+
});
|
|
3616
|
+
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
3617
|
+
}
|
|
3618
|
+
if (allProcesses.length > 1) {
|
|
3619
|
+
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
3620
|
+
console.log(chalk.gray("To kill runaway processes: happy daemon kill-runaway"));
|
|
3621
|
+
}
|
|
3622
|
+
} catch (error) {
|
|
3623
|
+
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
3624
|
+
}
|
|
3625
|
+
console.log(chalk.bold("\n\u{1F4DD} Log Files"));
|
|
3626
|
+
const mainLogs = getLogFiles(configuration.logsDir);
|
|
3627
|
+
if (mainLogs.length > 0) {
|
|
3628
|
+
console.log(chalk.blue("\nMain Logs:"));
|
|
3629
|
+
mainLogs.forEach(({ file, path, modified }) => {
|
|
3630
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3631
|
+
console.log(chalk.gray(` ${path}`));
|
|
3632
|
+
});
|
|
3633
|
+
} else {
|
|
3634
|
+
console.log(chalk.yellow("No main log files found"));
|
|
3635
|
+
}
|
|
3636
|
+
const daemonLogs = mainLogs.filter(({ file }) => file.includes("daemon"));
|
|
3637
|
+
if (daemonLogs.length > 0) {
|
|
3638
|
+
console.log(chalk.blue("\nDaemon Logs:"));
|
|
3639
|
+
daemonLogs.forEach(({ file, path, modified }) => {
|
|
3640
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3641
|
+
console.log(chalk.gray(` ${path}`));
|
|
3642
|
+
});
|
|
3643
|
+
} else {
|
|
3644
|
+
console.log(chalk.yellow("No daemon log files found"));
|
|
3645
|
+
}
|
|
3646
|
+
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
3647
|
+
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
3648
|
+
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
3649
|
+
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
async function daemonPost(path, body) {
|
|
3653
|
+
const state = await getDaemonState();
|
|
3654
|
+
if (!state?.httpPort) {
|
|
3655
|
+
throw new Error("No daemon running");
|
|
3656
|
+
}
|
|
3657
|
+
try {
|
|
3658
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3659
|
+
method: "POST",
|
|
3660
|
+
headers: { "Content-Type": "application/json" },
|
|
3661
|
+
body: JSON.stringify(body || {}),
|
|
3662
|
+
signal: AbortSignal.timeout(5e3)
|
|
3663
|
+
});
|
|
3664
|
+
if (!response.ok) {
|
|
3665
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3666
|
+
}
|
|
3667
|
+
return await response.json();
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
3670
|
+
throw error;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
3674
|
+
await daemonPost("/session-started", {
|
|
3675
|
+
sessionId,
|
|
3676
|
+
metadata
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
async function listDaemonSessions() {
|
|
3680
|
+
const result = await daemonPost("/list");
|
|
3681
|
+
return result.children || [];
|
|
3682
|
+
}
|
|
3683
|
+
async function stopDaemonSession(sessionId) {
|
|
3684
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3685
|
+
return result.success || false;
|
|
3686
|
+
}
|
|
3687
|
+
async function stopDaemonHttp() {
|
|
3688
|
+
await daemonPost("/stop");
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
var controlClient = /*#__PURE__*/Object.freeze({
|
|
3692
|
+
__proto__: null,
|
|
3693
|
+
listDaemonSessions: listDaemonSessions,
|
|
3694
|
+
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
3695
|
+
stopDaemonHttp: stopDaemonHttp,
|
|
3696
|
+
stopDaemonSession: stopDaemonSession
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3183
3699
|
async function start(credentials, options = {}) {
|
|
3184
3700
|
const workingDirectory = process.cwd();
|
|
3185
3701
|
const sessionTag = randomUUID();
|
|
3186
|
-
|
|
3702
|
+
logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
|
|
3703
|
+
logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
|
|
3704
|
+
if (options.startedBy === "daemon" && options.startingMode === "local") {
|
|
3187
3705
|
logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
3188
3706
|
options.startingMode = "remote";
|
|
3189
3707
|
}
|
|
3190
3708
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
3191
3709
|
let state = {};
|
|
3192
|
-
const settings = await readSettings()
|
|
3710
|
+
const settings = await readSettings();
|
|
3711
|
+
const machineId = settings?.machineId || "unknown";
|
|
3712
|
+
logger.debug(`Using machineId: ${machineId}`);
|
|
3193
3713
|
let metadata = {
|
|
3194
3714
|
path: workingDirectory,
|
|
3195
3715
|
host: os.hostname(),
|
|
3196
3716
|
version: packageJson.version,
|
|
3197
3717
|
os: os.platform(),
|
|
3198
|
-
machineId
|
|
3199
|
-
homeDir: os.homedir()
|
|
3718
|
+
machineId,
|
|
3719
|
+
homeDir: os.homedir(),
|
|
3720
|
+
happyHomeDir: configuration.happyHomeDir,
|
|
3721
|
+
startedFromDaemon: options.startedBy === "daemon",
|
|
3722
|
+
hostPid: process.pid,
|
|
3723
|
+
startedBy: options.startedBy || "terminal"
|
|
3200
3724
|
};
|
|
3201
3725
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
3202
3726
|
logger.debug(`Session created: ${response.id}`);
|
|
3727
|
+
try {
|
|
3728
|
+
const daemonState = await getDaemonState();
|
|
3729
|
+
if (daemonState?.httpPort) {
|
|
3730
|
+
await notifyDaemonSessionStarted(response.id, metadata);
|
|
3731
|
+
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
3732
|
+
}
|
|
3733
|
+
} catch (error) {
|
|
3734
|
+
logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
3735
|
+
}
|
|
3203
3736
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
3204
3737
|
logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
3205
3738
|
try {
|
|
3206
|
-
api.
|
|
3739
|
+
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
3207
3740
|
...currentMetadata,
|
|
3208
3741
|
tools: sdkMetadata.tools,
|
|
3209
3742
|
slashCommands: sdkMetadata.slashCommands
|
|
@@ -3213,10 +3746,7 @@ async function start(credentials, options = {}) {
|
|
|
3213
3746
|
logger.debug("[start] Failed to update session metadata:", error);
|
|
3214
3747
|
}
|
|
3215
3748
|
});
|
|
3216
|
-
|
|
3217
|
-
console.log(`daemon:sessionIdCreated:${response.id}`);
|
|
3218
|
-
}
|
|
3219
|
-
const session = api.session(response);
|
|
3749
|
+
const session = api.sessionSyncClient(response);
|
|
3220
3750
|
const logPath = await logger.logFilePathPromise;
|
|
3221
3751
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
3222
3752
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -3342,6 +3872,32 @@ async function start(credentials, options = {}) {
|
|
|
3342
3872
|
messageQueue.push(message.content.text, enhancedMode);
|
|
3343
3873
|
logger.debugLargeJson("User message pushed to queue:", message);
|
|
3344
3874
|
});
|
|
3875
|
+
const cleanup = async () => {
|
|
3876
|
+
logger.debug("[START] Received termination signal, cleaning up...");
|
|
3877
|
+
try {
|
|
3878
|
+
if (session) {
|
|
3879
|
+
session.sendSessionDeath();
|
|
3880
|
+
await session.flush();
|
|
3881
|
+
await session.close();
|
|
3882
|
+
}
|
|
3883
|
+
stopCaffeinate();
|
|
3884
|
+
logger.debug("[START] Cleanup complete, exiting");
|
|
3885
|
+
process.exit(0);
|
|
3886
|
+
} catch (error) {
|
|
3887
|
+
logger.debug("[START] Error during cleanup:", error);
|
|
3888
|
+
process.exit(1);
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
process.on("SIGTERM", cleanup);
|
|
3892
|
+
process.on("SIGINT", cleanup);
|
|
3893
|
+
process.on("uncaughtException", (error) => {
|
|
3894
|
+
logger.debug("[START] Uncaught exception:", error);
|
|
3895
|
+
cleanup();
|
|
3896
|
+
});
|
|
3897
|
+
process.on("unhandledRejection", (reason) => {
|
|
3898
|
+
logger.debug("[START] Unhandled rejection:", reason);
|
|
3899
|
+
cleanup();
|
|
3900
|
+
});
|
|
3345
3901
|
await loop({
|
|
3346
3902
|
path: workingDirectory,
|
|
3347
3903
|
model: options.model,
|
|
@@ -3397,7 +3953,7 @@ async function openBrowser(url) {
|
|
|
3397
3953
|
return false;
|
|
3398
3954
|
}
|
|
3399
3955
|
logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
3400
|
-
await open(url);
|
|
3956
|
+
await open$1(url);
|
|
3401
3957
|
logger.debug("[browser] Browser opened successfully");
|
|
3402
3958
|
return true;
|
|
3403
3959
|
} catch (error) {
|
|
@@ -3451,10 +4007,14 @@ async function doAuth() {
|
|
|
3451
4007
|
const secret = new Uint8Array(randomBytes(32));
|
|
3452
4008
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
3453
4009
|
try {
|
|
4010
|
+
console.log(`[AUTH DEBUG] Sending auth request to: ${configuration.serverUrl}/v1/auth/request`);
|
|
4011
|
+
console.log(`[AUTH DEBUG] Public key: ${encodeBase64(keypair.publicKey).substring(0, 20)}...`);
|
|
3454
4012
|
await axios.post(`${configuration.serverUrl}/v1/auth/request`, {
|
|
3455
4013
|
publicKey: encodeBase64(keypair.publicKey)
|
|
3456
4014
|
});
|
|
4015
|
+
console.log(`[AUTH DEBUG] Auth request sent successfully`);
|
|
3457
4016
|
} catch (error) {
|
|
4017
|
+
console.log(`[AUTH DEBUG] Failed to send auth request:`, error);
|
|
3458
4018
|
console.log("Failed to create authentication request, please try again later.");
|
|
3459
4019
|
return null;
|
|
3460
4020
|
}
|
|
@@ -3571,550 +4131,375 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
3571
4131
|
}
|
|
3572
4132
|
return decrypted;
|
|
3573
4133
|
}
|
|
4134
|
+
async function authAndSetupMachineIfNeeded() {
|
|
4135
|
+
logger.debug("[AUTH] Starting auth and machine setup...");
|
|
4136
|
+
let credentials = await readCredentials();
|
|
4137
|
+
if (!credentials) {
|
|
4138
|
+
logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4139
|
+
const authResult = await doAuth();
|
|
4140
|
+
if (!authResult) {
|
|
4141
|
+
throw new Error("Authentication failed or was cancelled");
|
|
4142
|
+
}
|
|
4143
|
+
credentials = authResult;
|
|
4144
|
+
} else {
|
|
4145
|
+
logger.debug("[AUTH] Using existing credentials");
|
|
4146
|
+
}
|
|
4147
|
+
const settings = await updateSettings(async (s) => {
|
|
4148
|
+
if (!s.machineId) {
|
|
4149
|
+
return {
|
|
4150
|
+
...s,
|
|
4151
|
+
machineId: randomUUID()
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
return s;
|
|
4155
|
+
});
|
|
4156
|
+
logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4157
|
+
return { credentials, machineId: settings.machineId };
|
|
4158
|
+
}
|
|
3574
4159
|
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
this.secret = secret;
|
|
3587
|
-
this.machineIdentity = machineIdentity;
|
|
3588
|
-
logger.debug(`[DAEMON SESSION] Connecting to server: ${configuration.serverUrl}`);
|
|
3589
|
-
const socket = io(configuration.serverUrl, {
|
|
3590
|
-
auth: {
|
|
3591
|
-
token: this.token,
|
|
3592
|
-
clientType: "machine-scoped",
|
|
3593
|
-
machineId: this.machineIdentity.machineId
|
|
3594
|
-
},
|
|
3595
|
-
path: "/v1/updates",
|
|
3596
|
-
reconnection: true,
|
|
3597
|
-
reconnectionAttempts: Infinity,
|
|
3598
|
-
reconnectionDelay: 1e3,
|
|
3599
|
-
reconnectionDelayMax: 5e3,
|
|
3600
|
-
transports: ["websocket"],
|
|
3601
|
-
withCredentials: true,
|
|
3602
|
-
autoConnect: false
|
|
3603
|
-
});
|
|
3604
|
-
socket.on("connect", async () => {
|
|
3605
|
-
logger.debug("[DAEMON SESSION] Socket connected");
|
|
3606
|
-
logger.debug(`[DAEMON SESSION] Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
3607
|
-
if (!this.machineRegistered) {
|
|
3608
|
-
await this.registerMachine();
|
|
3609
|
-
}
|
|
3610
|
-
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
3611
|
-
socket.emit("rpc-register", { method: rpcMethod });
|
|
3612
|
-
logger.debug(`[DAEMON SESSION] Emitted RPC registration: ${rpcMethod}`);
|
|
3613
|
-
this.emit("connected");
|
|
3614
|
-
this.startKeepAlive();
|
|
4160
|
+
function startDaemonControlServer({
|
|
4161
|
+
getChildren,
|
|
4162
|
+
stopSession,
|
|
4163
|
+
spawnSession,
|
|
4164
|
+
requestShutdown,
|
|
4165
|
+
onHappySessionWebhook
|
|
4166
|
+
}) {
|
|
4167
|
+
return new Promise((resolve) => {
|
|
4168
|
+
const app = fastify({
|
|
4169
|
+
logger: false
|
|
4170
|
+
// We use our own logger
|
|
3615
4171
|
});
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
const args = [
|
|
3627
|
-
"--daemon-spawn",
|
|
3628
|
-
"--happy-starting-mode",
|
|
3629
|
-
"remote"
|
|
3630
|
-
// ALWAYS force remote mode for daemon spawns
|
|
3631
|
-
];
|
|
3632
|
-
if (configuration.installationLocation === "local") {
|
|
3633
|
-
args.push("--local");
|
|
3634
|
-
}
|
|
3635
|
-
logger.debug(`[DAEMON SESSION] Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
3636
|
-
const happyBinPath = join$1(projectPath(), "bin", "happy.mjs");
|
|
3637
|
-
logger.debug(`[DAEMON SESSION] Using happy binary at: ${happyBinPath}`);
|
|
3638
|
-
const executable = happyBinPath;
|
|
3639
|
-
const spawnArgs = args;
|
|
3640
|
-
logger.debug(`[DAEMON SESSION] Spawn: executable=${executable}, args=${JSON.stringify(spawnArgs)}, cwd=${directory}`);
|
|
3641
|
-
const happyProcess = spawn$1(executable, spawnArgs, {
|
|
3642
|
-
cwd: directory,
|
|
3643
|
-
detached: true,
|
|
3644
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3645
|
-
// We need stdout
|
|
3646
|
-
});
|
|
3647
|
-
this.spawnedProcesses.add(happyProcess);
|
|
3648
|
-
this.updateChildPidsInMetadata();
|
|
3649
|
-
let sessionId = null;
|
|
3650
|
-
let output = "";
|
|
3651
|
-
let timeoutId = null;
|
|
3652
|
-
const cleanup = () => {
|
|
3653
|
-
happyProcess.stdout.removeAllListeners("data");
|
|
3654
|
-
happyProcess.stderr.removeAllListeners("data");
|
|
3655
|
-
happyProcess.removeAllListeners("error");
|
|
3656
|
-
happyProcess.removeAllListeners("exit");
|
|
3657
|
-
if (timeoutId) {
|
|
3658
|
-
clearTimeout(timeoutId);
|
|
3659
|
-
timeoutId = null;
|
|
3660
|
-
}
|
|
3661
|
-
};
|
|
3662
|
-
happyProcess.stdout.on("data", (data2) => {
|
|
3663
|
-
output += data2.toString();
|
|
3664
|
-
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
3665
|
-
if (match && !sessionId) {
|
|
3666
|
-
sessionId = match[1];
|
|
3667
|
-
logger.debug(`[DAEMON SESSION] Session spawned successfully: ${sessionId}`);
|
|
3668
|
-
callback({ sessionId });
|
|
3669
|
-
cleanup();
|
|
3670
|
-
happyProcess.unref();
|
|
3671
|
-
}
|
|
3672
|
-
});
|
|
3673
|
-
happyProcess.stderr.on("data", (data2) => {
|
|
3674
|
-
logger.debug(`[DAEMON SESSION] Spawned process stderr: ${data2.toString()}`);
|
|
3675
|
-
});
|
|
3676
|
-
happyProcess.on("error", (error) => {
|
|
3677
|
-
logger.debug("[DAEMON SESSION] Error spawning session:", error);
|
|
3678
|
-
if (!sessionId) {
|
|
3679
|
-
callback({ error: `Failed to spawn: ${error.message}` });
|
|
3680
|
-
cleanup();
|
|
3681
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3682
|
-
}
|
|
3683
|
-
});
|
|
3684
|
-
happyProcess.on("exit", (code, signal) => {
|
|
3685
|
-
logger.debug(`[DAEMON SESSION] Spawned process exited with code ${code}, signal ${signal}`);
|
|
3686
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3687
|
-
this.updateChildPidsInMetadata();
|
|
3688
|
-
if (!sessionId) {
|
|
3689
|
-
callback({ error: `Process exited before session ID received` });
|
|
3690
|
-
cleanup();
|
|
3691
|
-
}
|
|
3692
|
-
});
|
|
3693
|
-
timeoutId = setTimeout(() => {
|
|
3694
|
-
if (!sessionId) {
|
|
3695
|
-
logger.debug("[DAEMON SESSION] Timeout waiting for session ID");
|
|
3696
|
-
callback({ error: "Timeout waiting for session" });
|
|
3697
|
-
cleanup();
|
|
3698
|
-
happyProcess.kill();
|
|
3699
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3700
|
-
this.updateChildPidsInMetadata();
|
|
3701
|
-
}
|
|
3702
|
-
}, 1e4);
|
|
3703
|
-
} catch (error) {
|
|
3704
|
-
logger.debug("[DAEMON SESSION] Error spawning session:", error);
|
|
3705
|
-
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
3706
|
-
}
|
|
3707
|
-
} else {
|
|
3708
|
-
logger.debug(`[DAEMON SESSION] Unknown RPC method: ${data.method}`);
|
|
3709
|
-
callback({ error: `Unknown method: ${data.method}` });
|
|
4172
|
+
app.setValidatorCompiler(validatorCompiler);
|
|
4173
|
+
app.setSerializerCompiler(serializerCompiler);
|
|
4174
|
+
const typed = app.withTypeProvider();
|
|
4175
|
+
typed.post("/session-started", {
|
|
4176
|
+
schema: {
|
|
4177
|
+
body: z$1.object({
|
|
4178
|
+
sessionId: z$1.string(),
|
|
4179
|
+
metadata: z$1.any()
|
|
4180
|
+
// Metadata type from API
|
|
4181
|
+
})
|
|
3710
4182
|
}
|
|
4183
|
+
}, async (request, reply) => {
|
|
4184
|
+
const { sessionId, metadata } = request.body;
|
|
4185
|
+
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4186
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
4187
|
+
return { status: "ok" };
|
|
3711
4188
|
});
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
});
|
|
3717
|
-
socket.on("reconnect", () => {
|
|
3718
|
-
logger.debug("[DAEMON SESSION] Reconnected to server");
|
|
3719
|
-
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
3720
|
-
socket.emit("rpc-register", { method: rpcMethod });
|
|
3721
|
-
logger.debug(`[DAEMON SESSION] Re-registered RPC method: ${rpcMethod}`);
|
|
4189
|
+
typed.post("/list", async (request, reply) => {
|
|
4190
|
+
const children = getChildren();
|
|
4191
|
+
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4192
|
+
return { children };
|
|
3722
4193
|
});
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
});
|
|
3729
|
-
socket.on("rpc-error", (data) => {
|
|
3730
|
-
logger.debug(`[DAEMON SESSION] RPC error: ${JSON.stringify(data)}`);
|
|
3731
|
-
});
|
|
3732
|
-
socket.onAny((event, ...args) => {
|
|
3733
|
-
if (!event.startsWith("session-alive") && event !== "ephemeral") {
|
|
3734
|
-
logger.debug(`[DAEMON SESSION] Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
4194
|
+
typed.post("/stop-session", {
|
|
4195
|
+
schema: {
|
|
4196
|
+
body: z$1.object({
|
|
4197
|
+
sessionId: z$1.string()
|
|
4198
|
+
})
|
|
3735
4199
|
}
|
|
4200
|
+
}, async (request, reply) => {
|
|
4201
|
+
const { sessionId } = request.body;
|
|
4202
|
+
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4203
|
+
const success = stopSession(sessionId);
|
|
4204
|
+
return { success };
|
|
3736
4205
|
});
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
logger.debug(`[DAEMON SESSION] Socket error: ${error}`);
|
|
3743
|
-
});
|
|
3744
|
-
socket.on("daemon-command", (data) => {
|
|
3745
|
-
switch (data.command) {
|
|
3746
|
-
case "shutdown":
|
|
3747
|
-
this.shutdown();
|
|
3748
|
-
break;
|
|
3749
|
-
case "status":
|
|
3750
|
-
this.emit("status-request");
|
|
3751
|
-
break;
|
|
3752
|
-
}
|
|
3753
|
-
});
|
|
3754
|
-
this.socket = socket;
|
|
3755
|
-
}
|
|
3756
|
-
async registerMachine() {
|
|
3757
|
-
try {
|
|
3758
|
-
const metadata = {
|
|
3759
|
-
host: this.machineIdentity.machineHost,
|
|
3760
|
-
platform: this.machineIdentity.platform,
|
|
3761
|
-
happyCliVersion: this.machineIdentity.happyCliVersion,
|
|
3762
|
-
happyHomeDirectory: this.machineIdentity.happyHomeDirectory
|
|
3763
|
-
};
|
|
3764
|
-
const encrypted = encrypt(JSON.stringify(metadata), this.secret);
|
|
3765
|
-
const encryptedMetadata = encodeBase64(encrypted);
|
|
3766
|
-
const response = await fetch(`${configuration.serverUrl}/v1/machines`, {
|
|
3767
|
-
method: "POST",
|
|
3768
|
-
headers: {
|
|
3769
|
-
"Authorization": `Bearer ${this.token}`,
|
|
3770
|
-
"Content-Type": "application/json"
|
|
3771
|
-
},
|
|
3772
|
-
body: JSON.stringify({
|
|
3773
|
-
id: this.machineIdentity.machineId,
|
|
3774
|
-
metadata: encryptedMetadata
|
|
4206
|
+
typed.post("/spawn-session", {
|
|
4207
|
+
schema: {
|
|
4208
|
+
body: z$1.object({
|
|
4209
|
+
directory: z$1.string(),
|
|
4210
|
+
sessionId: z$1.string().optional()
|
|
3775
4211
|
})
|
|
3776
|
-
});
|
|
3777
|
-
if (response.ok) {
|
|
3778
|
-
logger.debug("[DAEMON SESSION] Machine registered/updated successfully");
|
|
3779
|
-
this.machineRegistered = true;
|
|
3780
|
-
} else {
|
|
3781
|
-
logger.debug(`[DAEMON SESSION] Failed to register machine: ${response.status}`);
|
|
3782
4212
|
}
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
}, 2e4);
|
|
3797
|
-
}
|
|
3798
|
-
stopKeepAlive() {
|
|
3799
|
-
if (this.keepAliveInterval) {
|
|
3800
|
-
clearInterval(this.keepAliveInterval);
|
|
3801
|
-
this.keepAliveInterval = null;
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
updateChildPidsInMetadata() {
|
|
3805
|
-
try {
|
|
3806
|
-
if (existsSync$1(configuration.daemonMetadataFile)) {
|
|
3807
|
-
const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
|
|
3808
|
-
const metadata = JSON.parse(content);
|
|
3809
|
-
const childPids = Array.from(this.spawnedProcesses).map((proc) => proc.pid).filter((pid) => pid !== void 0);
|
|
3810
|
-
metadata.childPids = childPids;
|
|
3811
|
-
writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
|
|
4213
|
+
}, async (request, reply) => {
|
|
4214
|
+
const { directory, sessionId } = request.body;
|
|
4215
|
+
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4216
|
+
const session = await spawnSession(directory, sessionId);
|
|
4217
|
+
if (session) {
|
|
4218
|
+
return {
|
|
4219
|
+
success: true,
|
|
4220
|
+
pid: session.pid,
|
|
4221
|
+
sessionId: session.happySessionId || "pending"
|
|
4222
|
+
};
|
|
4223
|
+
} else {
|
|
4224
|
+
reply.code(500);
|
|
4225
|
+
return { error: "Failed to spawn session" };
|
|
3812
4226
|
}
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
};
|
|
3827
|
-
const encrypted = encrypt(JSON.stringify(metadata), this.secret);
|
|
3828
|
-
const encryptedMetadata = encodeBase64(encrypted);
|
|
3829
|
-
this.socket.emit("update-machine", { metadata: encryptedMetadata });
|
|
3830
|
-
}
|
|
3831
|
-
shutdown() {
|
|
3832
|
-
logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
3833
|
-
for (const process of this.spawnedProcesses) {
|
|
3834
|
-
try {
|
|
3835
|
-
logger.debug(`[DAEMON SESSION] Killing spawned process with PID: ${process.pid}`);
|
|
3836
|
-
process.kill("SIGTERM");
|
|
3837
|
-
setTimeout(() => {
|
|
3838
|
-
try {
|
|
3839
|
-
process.kill("SIGKILL");
|
|
3840
|
-
} catch (e) {
|
|
3841
|
-
}
|
|
3842
|
-
}, 1e3);
|
|
3843
|
-
} catch (error) {
|
|
3844
|
-
logger.debug(`[DAEMON SESSION] Error killing process: ${error}`);
|
|
4227
|
+
});
|
|
4228
|
+
typed.post("/stop", async (request, reply) => {
|
|
4229
|
+
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4230
|
+
setTimeout(() => {
|
|
4231
|
+
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4232
|
+
requestShutdown();
|
|
4233
|
+
}, 50);
|
|
4234
|
+
return { status: "stopping" };
|
|
4235
|
+
});
|
|
4236
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4237
|
+
if (err) {
|
|
4238
|
+
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4239
|
+
throw err;
|
|
3845
4240
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
4241
|
+
const port = parseInt(address.split(":").pop());
|
|
4242
|
+
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4243
|
+
resolve({
|
|
4244
|
+
port,
|
|
4245
|
+
stop: async () => {
|
|
4246
|
+
logger.debug("[CONTROL SERVER] Stopping server");
|
|
4247
|
+
await app.close();
|
|
4248
|
+
logger.debug("[CONTROL SERVER] Server stopped");
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
});
|
|
4252
|
+
});
|
|
3853
4253
|
}
|
|
3854
4254
|
|
|
3855
4255
|
async function startDaemon() {
|
|
3856
|
-
if (process.platform !== "darwin") {
|
|
3857
|
-
console.error("ERROR: Daemon is only supported on macOS");
|
|
3858
|
-
process.exit(1);
|
|
3859
|
-
}
|
|
3860
4256
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
3861
|
-
logger.
|
|
3862
|
-
const runningDaemon = await
|
|
4257
|
+
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4258
|
+
const runningDaemon = await getDaemonState();
|
|
3863
4259
|
if (runningDaemon) {
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3868
|
-
} else if (await isDaemonProcessRunning(runningDaemon.pid)) {
|
|
3869
|
-
logger.debug("[DAEMON RUN] Happy daemon is already running with correct version");
|
|
4260
|
+
try {
|
|
4261
|
+
process.kill(runningDaemon.pid, 0);
|
|
4262
|
+
logger.debug("[DAEMON RUN] Daemon already running");
|
|
3870
4263
|
process.exit(0);
|
|
3871
|
-
}
|
|
3872
|
-
logger.debug("[DAEMON RUN] Stale
|
|
3873
|
-
await
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
const oldMetadata = await getDaemonMetadata();
|
|
3877
|
-
if (oldMetadata && oldMetadata.childPids && oldMetadata.childPids.length > 0) {
|
|
3878
|
-
logger.debug(`[DAEMON RUN] Found ${oldMetadata.childPids.length} potential orphaned child processes from previous run`);
|
|
3879
|
-
for (const childPid of oldMetadata.childPids) {
|
|
3880
|
-
try {
|
|
3881
|
-
process.kill(childPid, 0);
|
|
3882
|
-
const isHappy = await isProcessHappyChild(childPid);
|
|
3883
|
-
if (isHappy) {
|
|
3884
|
-
logger.debug(`[DAEMON RUN] Killing orphaned happy process ${childPid}`);
|
|
3885
|
-
process.kill(childPid, "SIGTERM");
|
|
3886
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3887
|
-
try {
|
|
3888
|
-
process.kill(childPid, 0);
|
|
3889
|
-
process.kill(childPid, "SIGKILL");
|
|
3890
|
-
} catch {
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
} catch {
|
|
3894
|
-
logger.debug(`[DAEMON RUN] Process ${childPid} doesn't exist (already dead)`);
|
|
3895
|
-
}
|
|
4264
|
+
} catch {
|
|
4265
|
+
logger.debug("[DAEMON RUN] Stale state found, cleaning up");
|
|
4266
|
+
await cleanupDaemonState();
|
|
3896
4267
|
}
|
|
3897
4268
|
}
|
|
3898
|
-
writeDaemonMetadata();
|
|
3899
|
-
logger.debug("[DAEMON RUN] Daemon metadata written");
|
|
3900
4269
|
const caffeinateStarted = startCaffeinate();
|
|
3901
4270
|
if (caffeinateStarted) {
|
|
3902
|
-
logger.debug("[DAEMON RUN] Sleep prevention enabled
|
|
4271
|
+
logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
3903
4272
|
}
|
|
3904
4273
|
try {
|
|
3905
|
-
const
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
happyCliVersion: packageJson.version,
|
|
3916
|
-
happyHomeDirectory: process.cwd()
|
|
3917
|
-
};
|
|
3918
|
-
let credentials = await readCredentials();
|
|
3919
|
-
if (!credentials) {
|
|
3920
|
-
logger.debug("[DAEMON RUN] No credentials found, running auth");
|
|
3921
|
-
await doAuth();
|
|
3922
|
-
credentials = await readCredentials();
|
|
3923
|
-
if (!credentials) {
|
|
3924
|
-
throw new Error("Failed to authenticate");
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
const { token, secret } = credentials;
|
|
3928
|
-
const daemon = new ApiDaemonSession(
|
|
3929
|
-
token,
|
|
3930
|
-
secret,
|
|
3931
|
-
machineIdentity
|
|
3932
|
-
);
|
|
3933
|
-
daemon.on("connected", () => {
|
|
3934
|
-
logger.debug("[DAEMON RUN] Connected to server event received");
|
|
3935
|
-
});
|
|
3936
|
-
daemon.on("disconnected", () => {
|
|
3937
|
-
logger.debug("[DAEMON RUN] Disconnected from server event received");
|
|
3938
|
-
});
|
|
3939
|
-
daemon.on("shutdown", () => {
|
|
3940
|
-
logger.debug("[DAEMON RUN] Shutdown requested");
|
|
3941
|
-
daemon?.shutdown();
|
|
3942
|
-
cleanupDaemonMetadata();
|
|
3943
|
-
process.exit(0);
|
|
3944
|
-
});
|
|
3945
|
-
daemon.connect();
|
|
3946
|
-
logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
3947
|
-
process.on("SIGINT", async () => {
|
|
3948
|
-
logger.debug("[DAEMON RUN] Received SIGINT, shutting down...");
|
|
3949
|
-
if (daemon) {
|
|
3950
|
-
daemon.shutdown();
|
|
4274
|
+
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4275
|
+
logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4276
|
+
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4277
|
+
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4278
|
+
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4279
|
+
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4280
|
+
const pid = sessionMetadata.hostPid;
|
|
4281
|
+
if (!pid) {
|
|
4282
|
+
logger.debug(`[DAEMON RUN] Session webhook missing hostPid for session ${sessionId}`);
|
|
4283
|
+
return;
|
|
3951
4284
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4285
|
+
logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
|
|
4286
|
+
const existingSession = pidToTrackedSession.get(pid);
|
|
4287
|
+
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4288
|
+
existingSession.happySessionId = sessionId;
|
|
4289
|
+
existingSession.happySessionMetadataFromLocalWebhook = sessionMetadata;
|
|
4290
|
+
logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
4291
|
+
const awaiter = pidToAwaiter.get(pid);
|
|
4292
|
+
if (awaiter) {
|
|
4293
|
+
pidToAwaiter.delete(pid);
|
|
4294
|
+
awaiter(existingSession);
|
|
4295
|
+
logger.debug(`[DAEMON RUN] Resolved session awaiter for PID ${pid}`);
|
|
4296
|
+
}
|
|
4297
|
+
} else if (!existingSession) {
|
|
4298
|
+
const trackedSession = {
|
|
4299
|
+
startedBy: "happy directly - likely by user from terminal",
|
|
4300
|
+
happySessionId: sessionId,
|
|
4301
|
+
happySessionMetadataFromLocalWebhook: sessionMetadata,
|
|
4302
|
+
pid
|
|
4303
|
+
};
|
|
4304
|
+
pidToTrackedSession.set(pid, trackedSession);
|
|
4305
|
+
logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
3959
4306
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
});
|
|
3963
|
-
} catch (error) {
|
|
3964
|
-
logger.debug("[DAEMON RUN] Failed to start daemon", error);
|
|
3965
|
-
stopDaemon();
|
|
3966
|
-
process.exit(1);
|
|
3967
|
-
}
|
|
3968
|
-
while (true) {
|
|
3969
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
async function isDaemonRunning() {
|
|
3973
|
-
try {
|
|
3974
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] Checking if daemon is running...");
|
|
3975
|
-
const metadata = await getDaemonMetadata();
|
|
3976
|
-
if (!metadata) {
|
|
3977
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] No daemon metadata found");
|
|
3978
|
-
return false;
|
|
3979
|
-
}
|
|
3980
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] Daemon metadata exists");
|
|
3981
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] PID from metadata:", metadata.pid);
|
|
3982
|
-
const isRunning = await isDaemonProcessRunning(metadata.pid);
|
|
3983
|
-
if (!isRunning) {
|
|
3984
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] Process not running, cleaning up stale metadata");
|
|
3985
|
-
await cleanupDaemonMetadata();
|
|
3986
|
-
return false;
|
|
3987
|
-
}
|
|
3988
|
-
return true;
|
|
3989
|
-
} catch (error) {
|
|
3990
|
-
logger.debug("[DAEMON RUN] [isDaemonRunning] Error:", error);
|
|
3991
|
-
logger.debug("Error checking daemon status", error);
|
|
3992
|
-
return false;
|
|
3993
|
-
}
|
|
3994
|
-
}
|
|
3995
|
-
async function isDaemonProcessRunning(pid) {
|
|
3996
|
-
try {
|
|
3997
|
-
process.kill(pid, 0);
|
|
3998
|
-
logger.debug("[DAEMON RUN] Process exists, checking if it's a happy daemon...");
|
|
3999
|
-
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
4000
|
-
logger.debug("[DAEMON RUN] isHappyDaemon:", isHappyDaemon);
|
|
4001
|
-
return isHappyDaemon;
|
|
4002
|
-
} catch (error) {
|
|
4003
|
-
return false;
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
function writeDaemonMetadata(childPids) {
|
|
4007
|
-
const happyDir = join$1(homedir$1(), ".happy");
|
|
4008
|
-
if (!existsSync$1(happyDir)) {
|
|
4009
|
-
mkdirSync$1(happyDir, { recursive: true });
|
|
4010
|
-
}
|
|
4011
|
-
const metadata = {
|
|
4012
|
-
pid: process.pid,
|
|
4013
|
-
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4014
|
-
version: packageJson.version,
|
|
4015
|
-
...childPids
|
|
4016
|
-
};
|
|
4017
|
-
writeFileSync(configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
|
|
4018
|
-
}
|
|
4019
|
-
async function getDaemonMetadata() {
|
|
4020
|
-
try {
|
|
4021
|
-
if (!existsSync$1(configuration.daemonMetadataFile)) {
|
|
4022
|
-
return null;
|
|
4023
|
-
}
|
|
4024
|
-
const content = readFileSync$1(configuration.daemonMetadataFile, "utf-8");
|
|
4025
|
-
return JSON.parse(content);
|
|
4026
|
-
} catch (error) {
|
|
4027
|
-
logger.debug("Error reading daemon metadata", error);
|
|
4028
|
-
return null;
|
|
4029
|
-
}
|
|
4030
|
-
}
|
|
4031
|
-
async function cleanupDaemonMetadata() {
|
|
4032
|
-
try {
|
|
4033
|
-
if (existsSync$1(configuration.daemonMetadataFile)) {
|
|
4034
|
-
unlinkSync(configuration.daemonMetadataFile);
|
|
4035
|
-
}
|
|
4036
|
-
} catch (error) {
|
|
4037
|
-
logger.debug("Error cleaning up daemon metadata", error);
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
async function stopDaemon() {
|
|
4041
|
-
try {
|
|
4042
|
-
stopCaffeinate();
|
|
4043
|
-
logger.debug("Stopped sleep prevention");
|
|
4044
|
-
const metadata = await getDaemonMetadata();
|
|
4045
|
-
if (metadata) {
|
|
4046
|
-
logger.debug(`Stopping daemon with PID ${metadata.pid}`);
|
|
4307
|
+
};
|
|
4308
|
+
const spawnSession = async (directory, sessionId) => {
|
|
4047
4309
|
try {
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4310
|
+
const happyBinPath = join$1(projectPath(), "bin", "happy.mjs");
|
|
4311
|
+
const args = [
|
|
4312
|
+
"--happy-starting-mode",
|
|
4313
|
+
"remote",
|
|
4314
|
+
"--started-by",
|
|
4315
|
+
"daemon"
|
|
4316
|
+
];
|
|
4317
|
+
const fullCommand = `${happyBinPath} ${args.join(" ")}`;
|
|
4318
|
+
logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
|
|
4319
|
+
const happyProcess = spawn$1(happyBinPath, args, {
|
|
4320
|
+
cwd: directory,
|
|
4321
|
+
detached: true,
|
|
4322
|
+
// Sessions stay alive when daemon stops
|
|
4323
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4324
|
+
// Capture stdout/stderr for debugging
|
|
4325
|
+
// env is inherited automatically from parent process
|
|
4326
|
+
});
|
|
4327
|
+
happyProcess.stdout?.on("data", (data) => {
|
|
4328
|
+
logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
|
|
4329
|
+
});
|
|
4330
|
+
happyProcess.stderr?.on("data", (data) => {
|
|
4331
|
+
logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
|
|
4332
|
+
});
|
|
4333
|
+
if (!happyProcess.pid) {
|
|
4334
|
+
logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4335
|
+
return null;
|
|
4056
4336
|
}
|
|
4337
|
+
logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4338
|
+
const trackedSession = {
|
|
4339
|
+
startedBy: "daemon",
|
|
4340
|
+
pid: happyProcess.pid,
|
|
4341
|
+
childProcess: happyProcess
|
|
4342
|
+
};
|
|
4343
|
+
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4344
|
+
happyProcess.on("exit", (code, signal) => {
|
|
4345
|
+
logger.debug(`[DAEMON RUN] Child PID ${happyProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
4346
|
+
if (happyProcess.pid) {
|
|
4347
|
+
onChildExited(happyProcess.pid);
|
|
4348
|
+
}
|
|
4349
|
+
});
|
|
4350
|
+
happyProcess.on("error", (error) => {
|
|
4351
|
+
logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
4352
|
+
if (happyProcess.pid) {
|
|
4353
|
+
onChildExited(happyProcess.pid);
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4357
|
+
return new Promise((resolve, reject) => {
|
|
4358
|
+
const timeout = setTimeout(() => {
|
|
4359
|
+
pidToAwaiter.delete(happyProcess.pid);
|
|
4360
|
+
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4361
|
+
resolve(trackedSession);
|
|
4362
|
+
}, 1e4);
|
|
4363
|
+
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4364
|
+
clearTimeout(timeout);
|
|
4365
|
+
logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4366
|
+
resolve(completedSession);
|
|
4367
|
+
});
|
|
4368
|
+
});
|
|
4057
4369
|
} catch (error) {
|
|
4058
|
-
logger.debug("
|
|
4370
|
+
logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4371
|
+
return null;
|
|
4059
4372
|
}
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
logger.debug(`
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
}
|
|
4373
|
+
};
|
|
4374
|
+
const stopSession = (sessionId) => {
|
|
4375
|
+
logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
4376
|
+
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
4377
|
+
if (session.happySessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
4378
|
+
if (session.startedBy === "daemon" && session.childProcess) {
|
|
4379
|
+
try {
|
|
4380
|
+
session.childProcess.kill("SIGTERM");
|
|
4381
|
+
logger.debug(`[DAEMON RUN] Sent SIGTERM to daemon-spawned session ${sessionId}`);
|
|
4382
|
+
} catch (error) {
|
|
4383
|
+
logger.debug(`[DAEMON RUN] Failed to kill session ${sessionId}:`, error);
|
|
4384
|
+
}
|
|
4385
|
+
} else {
|
|
4386
|
+
try {
|
|
4387
|
+
process.kill(pid, "SIGTERM");
|
|
4388
|
+
logger.debug(`[DAEMON RUN] Sent SIGTERM to external session PID ${pid}`);
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
4076
4391
|
}
|
|
4077
|
-
} catch {
|
|
4078
4392
|
}
|
|
4393
|
+
pidToTrackedSession.delete(pid);
|
|
4394
|
+
logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
4395
|
+
return true;
|
|
4079
4396
|
}
|
|
4080
4397
|
}
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
ps.stdout.on("data", (data) => {
|
|
4092
|
-
output += data.toString();
|
|
4398
|
+
logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
4399
|
+
return false;
|
|
4400
|
+
};
|
|
4401
|
+
const onChildExited = (pid) => {
|
|
4402
|
+
logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4403
|
+
pidToTrackedSession.delete(pid);
|
|
4404
|
+
};
|
|
4405
|
+
let requestShutdown;
|
|
4406
|
+
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4407
|
+
requestShutdown = resolve;
|
|
4093
4408
|
});
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4409
|
+
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4410
|
+
getChildren: getCurrentChildren,
|
|
4411
|
+
stopSession,
|
|
4412
|
+
spawnSession,
|
|
4413
|
+
requestShutdown: () => requestShutdown("happy-cli"),
|
|
4414
|
+
onHappySessionWebhook
|
|
4097
4415
|
});
|
|
4098
|
-
|
|
4099
|
-
|
|
4416
|
+
const fileState = {
|
|
4417
|
+
pid: process.pid,
|
|
4418
|
+
httpPort: controlPort,
|
|
4419
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4420
|
+
startedWithCliVersion: packageJson.version
|
|
4421
|
+
};
|
|
4422
|
+
await writeDaemonState(fileState);
|
|
4423
|
+
logger.debug("[DAEMON RUN] Daemon state written");
|
|
4424
|
+
const initialMetadata = {
|
|
4425
|
+
host: os$1.hostname(),
|
|
4426
|
+
platform: os$1.platform(),
|
|
4427
|
+
happyCliVersion: packageJson.version,
|
|
4428
|
+
homeDir: os$1.homedir(),
|
|
4429
|
+
happyHomeDir: configuration.happyHomeDir
|
|
4430
|
+
};
|
|
4431
|
+
const initialDaemonState = {
|
|
4432
|
+
status: "offline",
|
|
4433
|
+
pid: process.pid,
|
|
4434
|
+
httpPort: controlPort,
|
|
4435
|
+
startedAt: Date.now()
|
|
4436
|
+
};
|
|
4437
|
+
const api = new ApiClient(credentials.token, credentials.secret);
|
|
4438
|
+
const machine = await api.createOrReturnExistingAsIs({
|
|
4439
|
+
machineId,
|
|
4440
|
+
metadata: initialMetadata,
|
|
4441
|
+
daemonState: initialDaemonState
|
|
4100
4442
|
});
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4443
|
+
logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
4444
|
+
const apiMachine = api.machineSyncClient(machine);
|
|
4445
|
+
apiMachine.setRPCHandlers({
|
|
4446
|
+
spawnSession,
|
|
4447
|
+
stopSession,
|
|
4448
|
+
requestShutdown: () => requestShutdown("happy-app")
|
|
4449
|
+
});
|
|
4450
|
+
apiMachine.connect();
|
|
4451
|
+
const cleanupAndShutdown = async (source) => {
|
|
4452
|
+
logger.debug(`[DAEMON RUN] Starting cleanup (source: ${source})...`);
|
|
4453
|
+
if (apiMachine) {
|
|
4454
|
+
await apiMachine.updateDaemonState((state) => ({
|
|
4455
|
+
...state,
|
|
4456
|
+
status: "shutting-down",
|
|
4457
|
+
shutdownRequestedAt: Date.now(),
|
|
4458
|
+
shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
|
|
4459
|
+
}));
|
|
4460
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4461
|
+
}
|
|
4462
|
+
if (apiMachine) {
|
|
4463
|
+
apiMachine.shutdown();
|
|
4464
|
+
}
|
|
4465
|
+
logger.debug("[DAEMON RUN] Machine session shutdown");
|
|
4466
|
+
await stopControlServer();
|
|
4467
|
+
logger.debug("[DAEMON RUN] Control server stopped");
|
|
4468
|
+
await cleanupDaemonState();
|
|
4469
|
+
logger.debug("[DAEMON RUN] State cleaned up");
|
|
4470
|
+
stopCaffeinate();
|
|
4471
|
+
logger.debug("[DAEMON RUN] Caffeinate stopped");
|
|
4472
|
+
process.exit(0);
|
|
4473
|
+
};
|
|
4474
|
+
process.on("SIGINT", () => {
|
|
4475
|
+
logger.debug("[DAEMON RUN] Received SIGINT");
|
|
4476
|
+
cleanupAndShutdown("os-signal");
|
|
4109
4477
|
});
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4478
|
+
process.on("SIGTERM", () => {
|
|
4479
|
+
logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4480
|
+
cleanupAndShutdown("os-signal");
|
|
4113
4481
|
});
|
|
4114
|
-
|
|
4115
|
-
|
|
4482
|
+
process.on("uncaughtException", (error) => {
|
|
4483
|
+
logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4484
|
+
cleanupAndShutdown("unknown");
|
|
4116
4485
|
});
|
|
4117
|
-
|
|
4486
|
+
process.on("unhandledRejection", (reason) => {
|
|
4487
|
+
logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4488
|
+
cleanupAndShutdown("unknown");
|
|
4489
|
+
});
|
|
4490
|
+
process.on("exit", () => {
|
|
4491
|
+
logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
4492
|
+
});
|
|
4493
|
+
logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4494
|
+
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4495
|
+
logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4496
|
+
await cleanupAndShutdown(shutdownSource);
|
|
4497
|
+
} catch (error) {
|
|
4498
|
+
logger.debug("[DAEMON RUN] Failed to start daemon", error);
|
|
4499
|
+
await cleanupDaemonState();
|
|
4500
|
+
stopCaffeinate();
|
|
4501
|
+
process.exit(1);
|
|
4502
|
+
}
|
|
4118
4503
|
}
|
|
4119
4504
|
|
|
4120
4505
|
function trimIdent(text) {
|
|
@@ -4138,7 +4523,6 @@ function trimIdent(text) {
|
|
|
4138
4523
|
|
|
4139
4524
|
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
4140
4525
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
4141
|
-
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
4142
4526
|
async function install$1() {
|
|
4143
4527
|
try {
|
|
4144
4528
|
if (existsSync$1(PLIST_FILE$1)) {
|
|
@@ -4175,10 +4559,10 @@ async function install$1() {
|
|
|
4175
4559
|
<true/>
|
|
4176
4560
|
|
|
4177
4561
|
<key>StandardErrorPath</key>
|
|
4178
|
-
<string>${
|
|
4562
|
+
<string>${os$1.homedir()}/.happy/daemon.err</string>
|
|
4179
4563
|
|
|
4180
4564
|
<key>StandardOutPath</key>
|
|
4181
|
-
<string>${
|
|
4565
|
+
<string>${os$1.homedir()}/.happy/daemon.log</string>
|
|
4182
4566
|
|
|
4183
4567
|
<key>WorkingDirectory</key>
|
|
4184
4568
|
<string>/tmp</string>
|
|
@@ -4242,16 +4626,249 @@ async function uninstall() {
|
|
|
4242
4626
|
await uninstall$1();
|
|
4243
4627
|
}
|
|
4244
4628
|
|
|
4629
|
+
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4630
|
+
function bytesToBase32(bytes) {
|
|
4631
|
+
let result = "";
|
|
4632
|
+
let buffer = 0;
|
|
4633
|
+
let bufferLength = 0;
|
|
4634
|
+
for (const byte of bytes) {
|
|
4635
|
+
buffer = buffer << 8 | byte;
|
|
4636
|
+
bufferLength += 8;
|
|
4637
|
+
while (bufferLength >= 5) {
|
|
4638
|
+
bufferLength -= 5;
|
|
4639
|
+
result += BASE32_ALPHABET[buffer >> bufferLength & 31];
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
if (bufferLength > 0) {
|
|
4643
|
+
result += BASE32_ALPHABET[buffer << 5 - bufferLength & 31];
|
|
4644
|
+
}
|
|
4645
|
+
return result;
|
|
4646
|
+
}
|
|
4647
|
+
function formatSecretKeyForBackup(secretBytes) {
|
|
4648
|
+
const base32 = bytesToBase32(secretBytes);
|
|
4649
|
+
const groups = [];
|
|
4650
|
+
for (let i = 0; i < base32.length; i += 5) {
|
|
4651
|
+
groups.push(base32.slice(i, i + 5));
|
|
4652
|
+
}
|
|
4653
|
+
return groups.join("-");
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
async function handleAuthCommand(args) {
|
|
4657
|
+
const subcommand = args[0];
|
|
4658
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
4659
|
+
showAuthHelp();
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
switch (subcommand) {
|
|
4663
|
+
case "login":
|
|
4664
|
+
await handleAuthLogin(args.slice(1));
|
|
4665
|
+
break;
|
|
4666
|
+
case "logout":
|
|
4667
|
+
await handleAuthLogout();
|
|
4668
|
+
break;
|
|
4669
|
+
case "show-backup":
|
|
4670
|
+
await handleAuthShowBackup();
|
|
4671
|
+
break;
|
|
4672
|
+
case "status":
|
|
4673
|
+
await handleAuthStatus();
|
|
4674
|
+
break;
|
|
4675
|
+
default:
|
|
4676
|
+
console.error(chalk.red(`Unknown auth subcommand: ${subcommand}`));
|
|
4677
|
+
showAuthHelp();
|
|
4678
|
+
process.exit(1);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
function showAuthHelp() {
|
|
4682
|
+
console.log(`
|
|
4683
|
+
${chalk.bold("happy auth")} - Authentication management
|
|
4684
|
+
|
|
4685
|
+
${chalk.bold("Usage:")}
|
|
4686
|
+
happy auth login [--force] Authenticate with Happy
|
|
4687
|
+
happy auth logout Remove authentication and machine data
|
|
4688
|
+
happy auth status Show authentication status
|
|
4689
|
+
happy auth show-backup Display backup key for mobile/web clients
|
|
4690
|
+
happy auth help Show this help message
|
|
4691
|
+
|
|
4692
|
+
${chalk.bold("Options:")}
|
|
4693
|
+
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
4694
|
+
|
|
4695
|
+
${chalk.bold("Examples:")}
|
|
4696
|
+
happy auth login Authenticate if not already logged in
|
|
4697
|
+
happy auth login --force Force re-authentication (complete reset)
|
|
4698
|
+
happy auth status Check authentication and machine status
|
|
4699
|
+
happy auth show-backup Get backup key to link other devices
|
|
4700
|
+
happy auth logout Remove all authentication data
|
|
4701
|
+
|
|
4702
|
+
${chalk.bold("Notes:")}
|
|
4703
|
+
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
4704
|
+
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
4705
|
+
\u2022 The backup key allows linking multiple devices to the same account
|
|
4706
|
+
`);
|
|
4707
|
+
}
|
|
4708
|
+
async function handleAuthLogin(args) {
|
|
4709
|
+
const forceAuth = args.includes("--force") || args.includes("-f");
|
|
4710
|
+
if (forceAuth) {
|
|
4711
|
+
console.log(chalk.yellow("Force authentication requested."));
|
|
4712
|
+
console.log(chalk.gray("This will:"));
|
|
4713
|
+
console.log(chalk.gray(" \u2022 Clear existing credentials"));
|
|
4714
|
+
console.log(chalk.gray(" \u2022 Clear machine ID"));
|
|
4715
|
+
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
4716
|
+
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
4717
|
+
try {
|
|
4718
|
+
logger.debug("Stopping daemon for force auth...");
|
|
4719
|
+
await stopDaemon();
|
|
4720
|
+
console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
4721
|
+
} catch (error) {
|
|
4722
|
+
logger.debug("Daemon was not running or failed to stop:", error);
|
|
4723
|
+
}
|
|
4724
|
+
await clearCredentials();
|
|
4725
|
+
console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
4726
|
+
await clearMachineId();
|
|
4727
|
+
console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
4728
|
+
console.log("");
|
|
4729
|
+
}
|
|
4730
|
+
if (!forceAuth) {
|
|
4731
|
+
const existingCreds = await readCredentials();
|
|
4732
|
+
const settings = await readSettings();
|
|
4733
|
+
if (existingCreds && settings?.machineId) {
|
|
4734
|
+
console.log(chalk.green("\u2713 Already authenticated"));
|
|
4735
|
+
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4736
|
+
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4737
|
+
console.log(chalk.gray(` Use 'happy auth login --force' to re-authenticate`));
|
|
4738
|
+
return;
|
|
4739
|
+
} else if (existingCreds && !settings?.machineId) {
|
|
4740
|
+
console.log(chalk.yellow("\u26A0\uFE0F Credentials exist but machine ID is missing"));
|
|
4741
|
+
console.log(chalk.gray(" This can happen if --auth flag was used previously"));
|
|
4742
|
+
console.log(chalk.gray(" Fixing by setting up machine...\n"));
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
try {
|
|
4746
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
4747
|
+
console.log(chalk.green("\n\u2713 Authentication successful"));
|
|
4748
|
+
console.log(chalk.gray(` Machine ID: ${result.machineId}`));
|
|
4749
|
+
} catch (error) {
|
|
4750
|
+
console.error(chalk.red("Authentication failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
4751
|
+
process.exit(1);
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
async function handleAuthLogout() {
|
|
4755
|
+
const happyDir = configuration.happyHomeDir;
|
|
4756
|
+
const credentials = await readCredentials();
|
|
4757
|
+
if (!credentials) {
|
|
4758
|
+
console.log(chalk.yellow("Not currently authenticated"));
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
console.log(chalk.blue("This will log you out of Happy"));
|
|
4762
|
+
console.log(chalk.yellow("\u26A0\uFE0F You will need to re-authenticate to use Happy again"));
|
|
4763
|
+
const rl = createInterface({
|
|
4764
|
+
input: process.stdin,
|
|
4765
|
+
output: process.stdout
|
|
4766
|
+
});
|
|
4767
|
+
const answer = await new Promise((resolve) => {
|
|
4768
|
+
rl.question(chalk.yellow("Are you sure you want to log out? (y/N): "), resolve);
|
|
4769
|
+
});
|
|
4770
|
+
rl.close();
|
|
4771
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
4772
|
+
try {
|
|
4773
|
+
try {
|
|
4774
|
+
await stopDaemon();
|
|
4775
|
+
console.log(chalk.gray("Stopped daemon"));
|
|
4776
|
+
} catch {
|
|
4777
|
+
}
|
|
4778
|
+
if (existsSync(happyDir)) {
|
|
4779
|
+
rmSync(happyDir, { recursive: true, force: true });
|
|
4780
|
+
}
|
|
4781
|
+
console.log(chalk.green("\u2713 Successfully logged out"));
|
|
4782
|
+
console.log(chalk.gray(' Run "happy auth login" to authenticate again'));
|
|
4783
|
+
} catch (error) {
|
|
4784
|
+
throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4785
|
+
}
|
|
4786
|
+
} else {
|
|
4787
|
+
console.log(chalk.blue("Logout cancelled"));
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
async function handleAuthShowBackup() {
|
|
4791
|
+
const credentials = await readCredentials();
|
|
4792
|
+
const settings = await readSettings();
|
|
4793
|
+
if (!credentials) {
|
|
4794
|
+
console.log(chalk.yellow("Not authenticated"));
|
|
4795
|
+
console.log(chalk.gray('Run "happy auth login" to authenticate first'));
|
|
4796
|
+
return;
|
|
4797
|
+
}
|
|
4798
|
+
const formattedBackupKey = formatSecretKeyForBackup(credentials.secret);
|
|
4799
|
+
console.log(chalk.bold("\n\u{1F4F1} Backup Key\n"));
|
|
4800
|
+
console.log(chalk.cyan("Your backup key:"));
|
|
4801
|
+
console.log(chalk.bold(formattedBackupKey));
|
|
4802
|
+
console.log("");
|
|
4803
|
+
console.log(chalk.cyan("Machine Information:"));
|
|
4804
|
+
console.log(` Machine ID: ${settings?.machineId || "not set"}`);
|
|
4805
|
+
console.log(` Host: ${os.hostname()}`);
|
|
4806
|
+
console.log("");
|
|
4807
|
+
console.log(chalk.bold("How to use this backup key:"));
|
|
4808
|
+
console.log(chalk.gray("\u2022 In Happy mobile app: Go to restore/link device and enter this key"));
|
|
4809
|
+
console.log(chalk.gray("\u2022 This key format matches what the mobile app expects"));
|
|
4810
|
+
console.log(chalk.gray("\u2022 You can type it with or without dashes - the app will normalize it"));
|
|
4811
|
+
console.log(chalk.gray("\u2022 Common typos (0\u2192O, 1\u2192I) are automatically corrected"));
|
|
4812
|
+
console.log("");
|
|
4813
|
+
console.log(chalk.yellow("\u26A0\uFE0F Keep this key secure - it provides full access to your account"));
|
|
4814
|
+
}
|
|
4815
|
+
async function handleAuthStatus() {
|
|
4816
|
+
const credentials = await readCredentials();
|
|
4817
|
+
const settings = await readSettings();
|
|
4818
|
+
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
4819
|
+
if (!credentials) {
|
|
4820
|
+
console.log(chalk.red("\u2717 Not authenticated"));
|
|
4821
|
+
console.log(chalk.gray(' Run "happy auth login" to authenticate'));
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
console.log(chalk.green("\u2713 Authenticated"));
|
|
4825
|
+
const tokenPreview = credentials.token.substring(0, 30) + "...";
|
|
4826
|
+
console.log(chalk.gray(` Token: ${tokenPreview}`));
|
|
4827
|
+
if (settings?.machineId) {
|
|
4828
|
+
console.log(chalk.green("\u2713 Machine registered"));
|
|
4829
|
+
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4830
|
+
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4831
|
+
} else {
|
|
4832
|
+
console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
|
|
4833
|
+
console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
|
|
4834
|
+
}
|
|
4835
|
+
console.log(chalk.gray(`
|
|
4836
|
+
Data directory: ${configuration.happyHomeDir}`));
|
|
4837
|
+
try {
|
|
4838
|
+
const { isDaemonRunning } = await Promise.resolve().then(function () { return utils; });
|
|
4839
|
+
const running = await isDaemonRunning();
|
|
4840
|
+
if (running) {
|
|
4841
|
+
console.log(chalk.green("\u2713 Daemon running"));
|
|
4842
|
+
} else {
|
|
4843
|
+
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
4844
|
+
}
|
|
4845
|
+
} catch {
|
|
4846
|
+
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4245
4850
|
(async () => {
|
|
4246
4851
|
const args = process.argv.slice(2);
|
|
4247
|
-
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
4248
|
-
initializeConfiguration(installationLocation);
|
|
4249
|
-
initLoggerWithGlobalConfiguration();
|
|
4250
4852
|
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
4251
4853
|
const subcommand = args[0];
|
|
4252
|
-
if (subcommand === "
|
|
4854
|
+
if (subcommand === "doctor") {
|
|
4855
|
+
await runDoctorCommand();
|
|
4856
|
+
return;
|
|
4857
|
+
} else if (subcommand === "auth") {
|
|
4858
|
+
try {
|
|
4859
|
+
await handleAuthCommand(args.slice(1));
|
|
4860
|
+
} catch (error) {
|
|
4861
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
4862
|
+
if (process.env.DEBUG) {
|
|
4863
|
+
console.error(error);
|
|
4864
|
+
}
|
|
4865
|
+
process.exit(1);
|
|
4866
|
+
}
|
|
4867
|
+
return;
|
|
4868
|
+
} else if (subcommand === "logout") {
|
|
4869
|
+
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
4253
4870
|
try {
|
|
4254
|
-
await
|
|
4871
|
+
await handleAuthCommand(["logout"]);
|
|
4255
4872
|
} catch (error) {
|
|
4256
4873
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
4257
4874
|
if (process.env.DEBUG) {
|
|
@@ -4273,12 +4890,92 @@ async function uninstall() {
|
|
|
4273
4890
|
return;
|
|
4274
4891
|
} else if (subcommand === "daemon") {
|
|
4275
4892
|
const daemonSubcommand = args[1];
|
|
4276
|
-
if (daemonSubcommand === "
|
|
4893
|
+
if (daemonSubcommand === "list") {
|
|
4894
|
+
try {
|
|
4895
|
+
const sessions = await listDaemonSessions();
|
|
4896
|
+
if (sessions.length === 0) {
|
|
4897
|
+
console.log("No active sessions");
|
|
4898
|
+
} else {
|
|
4899
|
+
console.log("Active sessions:");
|
|
4900
|
+
const cleanSessions = sessions.map((s) => ({
|
|
4901
|
+
pid: s.pid,
|
|
4902
|
+
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
4903
|
+
startedBy: s.startedBy,
|
|
4904
|
+
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
4905
|
+
}));
|
|
4906
|
+
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
4907
|
+
}
|
|
4908
|
+
} catch (error) {
|
|
4909
|
+
console.log("No daemon running");
|
|
4910
|
+
}
|
|
4911
|
+
return;
|
|
4912
|
+
} else if (daemonSubcommand === "stop-session") {
|
|
4913
|
+
const sessionId = args[2];
|
|
4914
|
+
if (!sessionId) {
|
|
4915
|
+
console.error("Session ID required");
|
|
4916
|
+
process.exit(1);
|
|
4917
|
+
}
|
|
4918
|
+
try {
|
|
4919
|
+
const success = await stopDaemonSession(sessionId);
|
|
4920
|
+
console.log(success ? "Session stopped" : "Failed to stop session");
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
console.log("No daemon running");
|
|
4923
|
+
}
|
|
4924
|
+
return;
|
|
4925
|
+
} else if (daemonSubcommand === "start") {
|
|
4926
|
+
const happyBinPath = join(projectPath(), "bin", "happy.mjs");
|
|
4927
|
+
const child = spawn$1(happyBinPath, ["daemon", "start-sync"], {
|
|
4928
|
+
detached: true,
|
|
4929
|
+
stdio: "ignore",
|
|
4930
|
+
env: process.env
|
|
4931
|
+
});
|
|
4932
|
+
child.unref();
|
|
4933
|
+
let started = false;
|
|
4934
|
+
for (let i = 0; i < 50; i++) {
|
|
4935
|
+
if (await isDaemonRunning()) {
|
|
4936
|
+
started = true;
|
|
4937
|
+
break;
|
|
4938
|
+
}
|
|
4939
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4940
|
+
}
|
|
4941
|
+
if (started) {
|
|
4942
|
+
console.log("Daemon started successfully");
|
|
4943
|
+
} else {
|
|
4944
|
+
console.error("Failed to start daemon");
|
|
4945
|
+
process.exit(1);
|
|
4946
|
+
}
|
|
4947
|
+
process.exit(0);
|
|
4948
|
+
} else if (daemonSubcommand === "start-sync") {
|
|
4277
4949
|
await startDaemon();
|
|
4278
4950
|
process.exit(0);
|
|
4279
4951
|
} else if (daemonSubcommand === "stop") {
|
|
4280
4952
|
await stopDaemon();
|
|
4281
4953
|
process.exit(0);
|
|
4954
|
+
} else if (daemonSubcommand === "status") {
|
|
4955
|
+
const state = await getDaemonState();
|
|
4956
|
+
if (!state) {
|
|
4957
|
+
console.log("Daemon is not running");
|
|
4958
|
+
} else {
|
|
4959
|
+
const isRunning = await isDaemonRunning();
|
|
4960
|
+
if (isRunning) {
|
|
4961
|
+
console.log("Daemon is running");
|
|
4962
|
+
console.log(` PID: ${state.pid}`);
|
|
4963
|
+
console.log(` Port: ${state.httpPort}`);
|
|
4964
|
+
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
4965
|
+
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
4966
|
+
} else {
|
|
4967
|
+
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
process.exit(0);
|
|
4971
|
+
} else if (daemonSubcommand === "kill-runaway") {
|
|
4972
|
+
const { killRunawayHappyProcesses } = await Promise.resolve().then(function () { return utils; });
|
|
4973
|
+
const result = await killRunawayHappyProcesses();
|
|
4974
|
+
console.log(`Killed ${result.killed} runaway processes`);
|
|
4975
|
+
if (result.errors.length > 0) {
|
|
4976
|
+
console.log("Errors:", result.errors);
|
|
4977
|
+
}
|
|
4978
|
+
process.exit(0);
|
|
4282
4979
|
} else if (daemonSubcommand === "install") {
|
|
4283
4980
|
try {
|
|
4284
4981
|
await install();
|
|
@@ -4298,13 +4995,16 @@ async function uninstall() {
|
|
|
4298
4995
|
${chalk.bold("happy daemon")} - Daemon management
|
|
4299
4996
|
|
|
4300
4997
|
${chalk.bold("Usage:")}
|
|
4301
|
-
happy daemon start
|
|
4302
|
-
happy daemon stop
|
|
4303
|
-
|
|
4304
|
-
|
|
4998
|
+
happy daemon start Start the daemon (detached)
|
|
4999
|
+
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5000
|
+
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5001
|
+
happy daemon status Show daemon status
|
|
5002
|
+
happy daemon list List active sessions
|
|
5003
|
+
happy daemon stop-session <id> Stop a specific session
|
|
5004
|
+
happy daemon kill-runaway Kill all runaway Happy processes
|
|
4305
5005
|
|
|
4306
|
-
${chalk.bold("Note:")} The daemon runs in the background and
|
|
4307
|
-
|
|
5006
|
+
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5007
|
+
Sessions spawned by the daemon will continue running after daemon stops unless --kill-managed is used.
|
|
4308
5008
|
`);
|
|
4309
5009
|
}
|
|
4310
5010
|
return;
|
|
@@ -4313,104 +5013,105 @@ Currently only supported on macOS.
|
|
|
4313
5013
|
let showHelp = false;
|
|
4314
5014
|
let showVersion = false;
|
|
4315
5015
|
let forceAuth = false;
|
|
5016
|
+
let forceAuthNew = false;
|
|
5017
|
+
const unknownArgs = [];
|
|
4316
5018
|
for (let i = 0; i < args.length; i++) {
|
|
4317
5019
|
const arg = args[i];
|
|
4318
|
-
if (arg === "
|
|
5020
|
+
if (arg === "--help") {
|
|
4319
5021
|
showHelp = true;
|
|
4320
|
-
} else if (arg === "
|
|
5022
|
+
} else if (arg === "--version") {
|
|
4321
5023
|
showVersion = true;
|
|
4322
5024
|
} else if (arg === "--auth" || arg === "--login") {
|
|
4323
5025
|
forceAuth = true;
|
|
4324
|
-
} else if (arg === "-
|
|
4325
|
-
|
|
4326
|
-
} else if (arg === "
|
|
4327
|
-
options.permissionMode = z$1.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
|
|
4328
|
-
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
5026
|
+
} else if (arg === "--force-auth") {
|
|
5027
|
+
forceAuthNew = true;
|
|
5028
|
+
} else if (arg === "--happy-starting-mode") {
|
|
4329
5029
|
options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
|
|
4330
|
-
} else if (arg === "--
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
|
|
4335
|
-
process.exit(1);
|
|
4336
|
-
}
|
|
4337
|
-
options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
|
|
4338
|
-
} else if (arg === "--claude-arg") {
|
|
4339
|
-
const claudeArg = args[++i];
|
|
4340
|
-
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
4341
|
-
} else if (arg === "--daemon-spawn") {
|
|
4342
|
-
options.daemonSpawn = true;
|
|
5030
|
+
} else if (arg === "--yolo") {
|
|
5031
|
+
unknownArgs.push("--dangerously-skip-permissions");
|
|
5032
|
+
} else if (arg === "--started-by") {
|
|
5033
|
+
options.startedBy = args[++i];
|
|
4343
5034
|
} else {
|
|
4344
|
-
|
|
4345
|
-
|
|
5035
|
+
unknownArgs.push(arg);
|
|
5036
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
5037
|
+
unknownArgs.push(args[++i]);
|
|
5038
|
+
}
|
|
4346
5039
|
}
|
|
4347
5040
|
}
|
|
5041
|
+
if (unknownArgs.length > 0) {
|
|
5042
|
+
options.claudeArgs = [...options.claudeArgs || [], ...unknownArgs];
|
|
5043
|
+
}
|
|
4348
5044
|
if (showHelp) {
|
|
4349
5045
|
console.log(`
|
|
4350
5046
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
4351
5047
|
|
|
4352
5048
|
${chalk.bold("Usage:")}
|
|
4353
|
-
happy [options]
|
|
4354
|
-
happy
|
|
4355
|
-
happy
|
|
4356
|
-
happy daemon
|
|
5049
|
+
happy [options] Start Claude with mobile control
|
|
5050
|
+
happy auth Manage authentication
|
|
5051
|
+
happy notify Send push notification
|
|
5052
|
+
happy daemon Manage background service
|
|
4357
5053
|
|
|
4358
|
-
${chalk.bold("Options:")}
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
-
|
|
4362
|
-
-p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
|
|
4363
|
-
--auth, --login Force re-authentication
|
|
4364
|
-
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
4365
|
-
--claude-arg ARG Pass additional argument to Claude CLI
|
|
4366
|
-
|
|
4367
|
-
[Daemon Management]
|
|
4368
|
-
--happy-daemon-start Start the daemon in background
|
|
4369
|
-
--happy-daemon-stop Stop the daemon
|
|
4370
|
-
--happy-daemon-install Install daemon to run on startup
|
|
4371
|
-
--happy-daemon-uninstall Uninstall daemon from startup
|
|
5054
|
+
${chalk.bold("Happy Options:")}
|
|
5055
|
+
--help Show this help message
|
|
5056
|
+
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5057
|
+
--force-auth Force re-authentication
|
|
4372
5058
|
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
4376
|
-
You will require re-login each time you run this in a new directory.
|
|
4377
|
-
--happy-starting-mode <interactive|remote>
|
|
4378
|
-
Set the starting mode for new sessions (default: remote)
|
|
4379
|
-
--happy-server-url <url>
|
|
4380
|
-
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
5059
|
+
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5060
|
+
Use any claude flag exactly as you normally would.
|
|
4381
5061
|
|
|
4382
5062
|
${chalk.bold("Examples:")}
|
|
4383
|
-
happy Start
|
|
4384
|
-
happy
|
|
4385
|
-
happy
|
|
4386
|
-
happy
|
|
4387
|
-
happy
|
|
4388
|
-
happy
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
happy logout Logs out of your account and removes data directory
|
|
5063
|
+
happy Start session
|
|
5064
|
+
happy --yolo Start without permissions
|
|
5065
|
+
happy --verbose Enable verbose mode
|
|
5066
|
+
happy -c Continue last conversation
|
|
5067
|
+
happy auth login Authenticate
|
|
5068
|
+
happy notify -p "Done!" Send notification
|
|
5069
|
+
|
|
5070
|
+
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5071
|
+
${chalk.bold('Use "happy daemon" for background service management.')}
|
|
4393
5072
|
|
|
4394
|
-
|
|
4395
|
-
${chalk.bold("
|
|
4396
|
-
TODO: exec cluade --help and show inline here
|
|
5073
|
+
${chalk.gray("\u2500".repeat(60))}
|
|
5074
|
+
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
4397
5075
|
`);
|
|
5076
|
+
const { execSync } = await import('child_process');
|
|
5077
|
+
try {
|
|
5078
|
+
const claudeHelp = execSync("claude --help", { encoding: "utf8" });
|
|
5079
|
+
console.log(claudeHelp);
|
|
5080
|
+
} catch (e) {
|
|
5081
|
+
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
5082
|
+
}
|
|
4398
5083
|
process.exit(0);
|
|
4399
5084
|
}
|
|
4400
5085
|
if (showVersion) {
|
|
4401
5086
|
console.log(packageJson.version);
|
|
4402
5087
|
process.exit(0);
|
|
4403
5088
|
}
|
|
4404
|
-
let credentials
|
|
4405
|
-
if (
|
|
4406
|
-
|
|
5089
|
+
let credentials;
|
|
5090
|
+
if (forceAuthNew) {
|
|
5091
|
+
console.log(chalk.yellow("Force authentication requested..."));
|
|
5092
|
+
try {
|
|
5093
|
+
await stopDaemon();
|
|
5094
|
+
} catch {
|
|
5095
|
+
}
|
|
5096
|
+
await clearCredentials();
|
|
5097
|
+
await clearMachineId();
|
|
5098
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5099
|
+
credentials = result.credentials;
|
|
5100
|
+
} else if (forceAuth) {
|
|
5101
|
+
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5102
|
+
const res = await doAuth();
|
|
4407
5103
|
if (!res) {
|
|
4408
5104
|
process.exit(1);
|
|
4409
5105
|
}
|
|
4410
|
-
|
|
5106
|
+
await writeCredentials(res);
|
|
5107
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5108
|
+
credentials = result.credentials;
|
|
5109
|
+
} else {
|
|
5110
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5111
|
+
credentials = result.credentials;
|
|
4411
5112
|
}
|
|
4412
|
-
|
|
4413
|
-
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5113
|
+
let settings = await readSettings();
|
|
5114
|
+
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
4414
5115
|
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
4415
5116
|
const rl = createInterface({
|
|
4416
5117
|
input: process.stdin,
|
|
@@ -4425,39 +5126,28 @@ TODO: exec cluade --help and show inline here
|
|
|
4425
5126
|
});
|
|
4426
5127
|
rl.close();
|
|
4427
5128
|
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
4428
|
-
settings
|
|
5129
|
+
settings = await updateSettings((settings2) => ({
|
|
5130
|
+
...settings2,
|
|
5131
|
+
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5132
|
+
}));
|
|
4429
5133
|
if (shouldAutoStart) {
|
|
4430
5134
|
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
4431
5135
|
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
4432
5136
|
} else {
|
|
4433
5137
|
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
4434
5138
|
}
|
|
4435
|
-
await writeSettings(settings);
|
|
4436
5139
|
}
|
|
4437
|
-
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
5140
|
+
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
4438
5141
|
logger.debug("Starting Happy background service...");
|
|
4439
5142
|
if (!await isDaemonRunning()) {
|
|
4440
|
-
const
|
|
4441
|
-
const
|
|
4442
|
-
const daemonArgs = ["daemon", "start"];
|
|
4443
|
-
if (installationLocation === "local") {
|
|
4444
|
-
daemonArgs.push("--local");
|
|
4445
|
-
}
|
|
4446
|
-
let executable, args2;
|
|
4447
|
-
if (runningFromBuiltBinary) {
|
|
4448
|
-
executable = happyPath;
|
|
4449
|
-
args2 = daemonArgs;
|
|
4450
|
-
} else {
|
|
4451
|
-
executable = "npx";
|
|
4452
|
-
args2 = ["tsx", happyPath, ...daemonArgs];
|
|
4453
|
-
}
|
|
4454
|
-
const daemonProcess = spawn$1(executable, args2, {
|
|
5143
|
+
const happyBinPath = join(projectPath(), "bin", "happy.mjs");
|
|
5144
|
+
const daemonProcess = spawn$1(happyBinPath, ["daemon", "start-sync"], {
|
|
4455
5145
|
detached: true,
|
|
4456
|
-
stdio:
|
|
4457
|
-
|
|
5146
|
+
stdio: "ignore",
|
|
5147
|
+
env: process.env
|
|
4458
5148
|
});
|
|
4459
5149
|
daemonProcess.unref();
|
|
4460
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5150
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4461
5151
|
}
|
|
4462
5152
|
}
|
|
4463
5153
|
try {
|
|
@@ -4471,34 +5161,6 @@ TODO: exec cluade --help and show inline here
|
|
|
4471
5161
|
}
|
|
4472
5162
|
}
|
|
4473
5163
|
})();
|
|
4474
|
-
async function cleanKey() {
|
|
4475
|
-
const happyDir = configuration.happyDir;
|
|
4476
|
-
if (!existsSync(happyDir)) {
|
|
4477
|
-
console.log(chalk.yellow("No happy data directory found at:"), happyDir);
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
console.log(chalk.blue("Found happy data directory at:"), happyDir);
|
|
4481
|
-
console.log(chalk.yellow("\u26A0\uFE0F This will remove all authentication data and require reconnecting your phone."));
|
|
4482
|
-
const rl = createInterface({
|
|
4483
|
-
input: process.stdin,
|
|
4484
|
-
output: process.stdout
|
|
4485
|
-
});
|
|
4486
|
-
const answer = await new Promise((resolve) => {
|
|
4487
|
-
rl.question(chalk.yellow("Are you sure you want to remove the happy data directory? (y/N): "), resolve);
|
|
4488
|
-
});
|
|
4489
|
-
rl.close();
|
|
4490
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
4491
|
-
try {
|
|
4492
|
-
rmSync(happyDir, { recursive: true, force: true });
|
|
4493
|
-
console.log(chalk.green("\u2713 Happy data directory removed successfully"));
|
|
4494
|
-
console.log(chalk.blue("\u2139\uFE0F You will need to reconnect your phone on the next session"));
|
|
4495
|
-
} catch (error) {
|
|
4496
|
-
throw new Error(`Failed to remove data directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4497
|
-
}
|
|
4498
|
-
} else {
|
|
4499
|
-
console.log(chalk.blue("Operation cancelled"));
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
5164
|
async function handleNotifyCommand(args) {
|
|
4503
5165
|
let message = "";
|
|
4504
5166
|
let title = "";
|