happy-coder 0.9.0-6 → 0.9.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 +1044 -1089
- package/dist/index.mjs +797 -824
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +48 -3
- package/dist/lib.d.mts +48 -3
- package/dist/lib.mjs +4 -4
- package/dist/{types-DJOX-XG-.mjs → types-BS8Pr3Im.mjs} +588 -204
- package/dist/{types-a-nJyP-e.cjs → types-DNUk09Np.cjs} +486 -71
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import os$1, { homedir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
+
import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, e as AsyncLock, r as readDaemonState, c as configuration, f as clearDaemonState, p as packageJson, g as readSettings, h as readCredentials, i as encodeBase64, u as updateSettings, j as encodeBase64Url, k as decodeBase64, w as writeCredentials, m as acquireDaemonLock, n as writeDaemonState, A as ApiClient, o as releaseDaemonLock, q as clearCredentials, s as clearMachineId, t as getLatestDaemonLog } from './types-BS8Pr3Im.mjs';
|
|
4
5
|
import { spawn, execSync } from 'node:child_process';
|
|
5
6
|
import { resolve, join, dirname as dirname$1 } from 'node:path';
|
|
6
7
|
import { createInterface } from 'node:readline';
|
|
7
8
|
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
8
|
-
import { existsSync, readFileSync, mkdirSync, watch,
|
|
9
|
-
import os$1, { homedir } from 'node:os';
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync, watch, readdirSync, statSync, rmSync } from 'node:fs';
|
|
10
10
|
import { dirname, resolve as resolve$1, join as join$1 } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { readFile
|
|
13
|
-
import { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import fs, { watch as watch$1, access, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
14
14
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
15
15
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
16
16
|
import axios from 'axios';
|
|
@@ -21,17 +21,16 @@ import 'expo-server-sdk';
|
|
|
21
21
|
import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
|
|
22
22
|
import { promisify } from 'util';
|
|
23
23
|
import { createHash } from 'crypto';
|
|
24
|
-
import * as z from 'zod';
|
|
25
|
-
import { z as z$1 } from 'zod';
|
|
26
|
-
import fastify from 'fastify';
|
|
27
|
-
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
28
24
|
import os from 'os';
|
|
29
25
|
import qrcode from 'qrcode-terminal';
|
|
30
|
-
import open
|
|
26
|
+
import open from 'open';
|
|
27
|
+
import fastify from 'fastify';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
|
|
30
|
+
import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
31
31
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
32
32
|
import { createServer } from 'node:http';
|
|
33
33
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
34
|
-
import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
35
34
|
|
|
36
35
|
class Session {
|
|
37
36
|
path;
|
|
@@ -41,6 +40,7 @@ class Session {
|
|
|
41
40
|
queue;
|
|
42
41
|
claudeEnvVars;
|
|
43
42
|
claudeArgs;
|
|
43
|
+
// Made mutable to allow filtering
|
|
44
44
|
mcpServers;
|
|
45
45
|
allowedTools;
|
|
46
46
|
_onModeChange;
|
|
@@ -75,6 +75,11 @@ class Session {
|
|
|
75
75
|
};
|
|
76
76
|
onSessionFound = (sessionId) => {
|
|
77
77
|
this.sessionId = sessionId;
|
|
78
|
+
this.client.updateMetadata((metadata) => ({
|
|
79
|
+
...metadata,
|
|
80
|
+
claudeSessionId: sessionId
|
|
81
|
+
}));
|
|
82
|
+
logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
78
83
|
};
|
|
79
84
|
/**
|
|
80
85
|
* Clear the current session ID (used by /clear command)
|
|
@@ -83,6 +88,33 @@ class Session {
|
|
|
83
88
|
this.sessionId = null;
|
|
84
89
|
logger.debug("[Session] Session ID cleared");
|
|
85
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
93
|
+
* Currently handles: --resume (with or without session ID)
|
|
94
|
+
*/
|
|
95
|
+
consumeOneTimeFlags = () => {
|
|
96
|
+
if (!this.claudeArgs) return;
|
|
97
|
+
const filteredArgs = [];
|
|
98
|
+
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
99
|
+
if (this.claudeArgs[i] === "--resume") {
|
|
100
|
+
if (i + 1 < this.claudeArgs.length) {
|
|
101
|
+
const nextArg = this.claudeArgs[i + 1];
|
|
102
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
103
|
+
i++;
|
|
104
|
+
logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
|
|
105
|
+
} else {
|
|
106
|
+
logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
filteredArgs.push(this.claudeArgs[i]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
116
|
+
logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
117
|
+
};
|
|
86
118
|
}
|
|
87
119
|
|
|
88
120
|
function getProjectPath(workingDirectory) {
|
|
@@ -137,6 +169,16 @@ function trimIdent(text) {
|
|
|
137
169
|
|
|
138
170
|
const systemPrompt = trimIdent(`
|
|
139
171
|
ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
|
|
172
|
+
|
|
173
|
+
When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
|
|
174
|
+
|
|
175
|
+
<main commit message>
|
|
176
|
+
|
|
177
|
+
Generated with [Claude Code](https://claude.ai/code)
|
|
178
|
+
via [Happy](https://happy.engineering)
|
|
179
|
+
|
|
180
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
181
|
+
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
140
182
|
`);
|
|
141
183
|
|
|
142
184
|
dirname$1(fileURLToPath$1(import.meta.url));
|
|
@@ -596,6 +638,7 @@ async function claudeLocalLauncher(session) {
|
|
|
596
638
|
mcpServers: session.mcpServers,
|
|
597
639
|
allowedTools: session.allowedTools
|
|
598
640
|
});
|
|
641
|
+
session.consumeOneTimeFlags();
|
|
599
642
|
if (!exitReason) {
|
|
600
643
|
exitReason = "exit";
|
|
601
644
|
break;
|
|
@@ -1399,6 +1442,26 @@ async function claudeRemote(opts) {
|
|
|
1399
1442
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
1400
1443
|
startFrom = null;
|
|
1401
1444
|
}
|
|
1445
|
+
if (!startFrom && opts.claudeArgs) {
|
|
1446
|
+
for (let i = 0; i < opts.claudeArgs.length; i++) {
|
|
1447
|
+
if (opts.claudeArgs[i] === "--resume") {
|
|
1448
|
+
if (i + 1 < opts.claudeArgs.length) {
|
|
1449
|
+
const nextArg = opts.claudeArgs[i + 1];
|
|
1450
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
1451
|
+
startFrom = nextArg;
|
|
1452
|
+
logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
|
|
1453
|
+
break;
|
|
1454
|
+
} else {
|
|
1455
|
+
logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1402
1465
|
if (opts.claudeEnvVars) {
|
|
1403
1466
|
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
1404
1467
|
process.env[key] = value;
|
|
@@ -2587,7 +2650,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2587
2650
|
let modeHash = null;
|
|
2588
2651
|
let mode = null;
|
|
2589
2652
|
try {
|
|
2590
|
-
await claudeRemote({
|
|
2653
|
+
const remoteResult = await claudeRemote({
|
|
2591
2654
|
sessionId: session.sessionId,
|
|
2592
2655
|
path: session.path,
|
|
2593
2656
|
allowedTools: session.allowedTools ?? [],
|
|
@@ -2648,6 +2711,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2648
2711
|
},
|
|
2649
2712
|
signal: abortController.signal
|
|
2650
2713
|
});
|
|
2714
|
+
session.consumeOneTimeFlags();
|
|
2651
2715
|
if (!exitReason && abortController.signal.aborted) {
|
|
2652
2716
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2653
2717
|
}
|
|
@@ -2698,7 +2762,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2698
2762
|
}
|
|
2699
2763
|
|
|
2700
2764
|
async function loop(opts) {
|
|
2701
|
-
const logPath =
|
|
2765
|
+
const logPath = logger.logFilePath;
|
|
2702
2766
|
let session = new Session({
|
|
2703
2767
|
api: opts.api,
|
|
2704
2768
|
client: opts.session,
|
|
@@ -2743,133 +2807,6 @@ async function loop(opts) {
|
|
|
2743
2807
|
}
|
|
2744
2808
|
}
|
|
2745
2809
|
|
|
2746
|
-
var name = "happy-coder";
|
|
2747
|
-
var version = "0.9.0-6";
|
|
2748
|
-
var description = "Claude Code session sharing CLI";
|
|
2749
|
-
var author = "Kirill Dubovitskiy";
|
|
2750
|
-
var license = "MIT";
|
|
2751
|
-
var type = "module";
|
|
2752
|
-
var homepage = "https://github.com/slopus/happy-cli";
|
|
2753
|
-
var bugs = "https://github.com/slopus/happy-cli/issues";
|
|
2754
|
-
var repository = "slopus/happy-cli";
|
|
2755
|
-
var bin = {
|
|
2756
|
-
happy: "./bin/happy.mjs"
|
|
2757
|
-
};
|
|
2758
|
-
var main = "./dist/index.cjs";
|
|
2759
|
-
var module = "./dist/index.mjs";
|
|
2760
|
-
var types = "./dist/index.d.cts";
|
|
2761
|
-
var exports = {
|
|
2762
|
-
".": {
|
|
2763
|
-
require: {
|
|
2764
|
-
types: "./dist/index.d.cts",
|
|
2765
|
-
"default": "./dist/index.cjs"
|
|
2766
|
-
},
|
|
2767
|
-
"import": {
|
|
2768
|
-
types: "./dist/index.d.mts",
|
|
2769
|
-
"default": "./dist/index.mjs"
|
|
2770
|
-
}
|
|
2771
|
-
},
|
|
2772
|
-
"./lib": {
|
|
2773
|
-
require: {
|
|
2774
|
-
types: "./dist/lib.d.cts",
|
|
2775
|
-
"default": "./dist/lib.cjs"
|
|
2776
|
-
},
|
|
2777
|
-
"import": {
|
|
2778
|
-
types: "./dist/lib.d.mts",
|
|
2779
|
-
"default": "./dist/lib.mjs"
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
};
|
|
2783
|
-
var files = [
|
|
2784
|
-
"dist",
|
|
2785
|
-
"bin",
|
|
2786
|
-
"scripts",
|
|
2787
|
-
"ripgrep",
|
|
2788
|
-
"package.json"
|
|
2789
|
-
];
|
|
2790
|
-
var scripts = {
|
|
2791
|
-
"why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
|
|
2792
|
-
typecheck: "tsc --noEmit",
|
|
2793
|
-
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2794
|
-
test: "yarn build && vitest run",
|
|
2795
|
-
"test:watch": "vitest",
|
|
2796
|
-
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2797
|
-
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2798
|
-
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2799
|
-
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2800
|
-
prepublishOnly: "yarn build && yarn test",
|
|
2801
|
-
release: "release-it"
|
|
2802
|
-
};
|
|
2803
|
-
var dependencies = {
|
|
2804
|
-
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2805
|
-
"@anthropic-ai/sdk": "^0.56.0",
|
|
2806
|
-
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2807
|
-
"@stablelib/base64": "^2.0.1",
|
|
2808
|
-
"@types/http-proxy": "^1.17.16",
|
|
2809
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
2810
|
-
"@types/react": "^19.1.9",
|
|
2811
|
-
axios: "^1.10.0",
|
|
2812
|
-
chalk: "^5.4.1",
|
|
2813
|
-
"expo-server-sdk": "^3.15.0",
|
|
2814
|
-
fastify: "^5.5.0",
|
|
2815
|
-
"fastify-type-provider-zod": "4.0.2",
|
|
2816
|
-
"http-proxy": "^1.18.1",
|
|
2817
|
-
"http-proxy-middleware": "^3.0.5",
|
|
2818
|
-
ink: "^6.1.0",
|
|
2819
|
-
open: "^10.2.0",
|
|
2820
|
-
"qrcode-terminal": "^0.12.0",
|
|
2821
|
-
react: "^19.1.1",
|
|
2822
|
-
"socket.io-client": "^4.8.1",
|
|
2823
|
-
tweetnacl: "^1.0.3",
|
|
2824
|
-
zod: "^3.23.8"
|
|
2825
|
-
};
|
|
2826
|
-
var devDependencies = {
|
|
2827
|
-
"@eslint/compat": "^1",
|
|
2828
|
-
"@types/node": ">=20",
|
|
2829
|
-
"cross-env": "^10.0.0",
|
|
2830
|
-
eslint: "^9",
|
|
2831
|
-
"eslint-config-prettier": "^10",
|
|
2832
|
-
pkgroll: "^2.14.2",
|
|
2833
|
-
"release-it": "^19.0.4",
|
|
2834
|
-
shx: "^0.3.3",
|
|
2835
|
-
"ts-node": "^10",
|
|
2836
|
-
tsx: "^4.20.3",
|
|
2837
|
-
typescript: "^5",
|
|
2838
|
-
vitest: "^3.2.4"
|
|
2839
|
-
};
|
|
2840
|
-
var resolutions = {
|
|
2841
|
-
"whatwg-url": "14.2.0",
|
|
2842
|
-
"parse-path": "7.0.3",
|
|
2843
|
-
"@types/parse-path": "7.0.3"
|
|
2844
|
-
};
|
|
2845
|
-
var publishConfig = {
|
|
2846
|
-
registry: "https://registry.npmjs.org"
|
|
2847
|
-
};
|
|
2848
|
-
var packageManager = "yarn@1.22.22";
|
|
2849
|
-
var packageJson = {
|
|
2850
|
-
name: name,
|
|
2851
|
-
version: version,
|
|
2852
|
-
description: description,
|
|
2853
|
-
author: author,
|
|
2854
|
-
license: license,
|
|
2855
|
-
type: type,
|
|
2856
|
-
homepage: homepage,
|
|
2857
|
-
bugs: bugs,
|
|
2858
|
-
repository: repository,
|
|
2859
|
-
bin: bin,
|
|
2860
|
-
main: main,
|
|
2861
|
-
module: module,
|
|
2862
|
-
types: types,
|
|
2863
|
-
exports: exports,
|
|
2864
|
-
files: files,
|
|
2865
|
-
scripts: scripts,
|
|
2866
|
-
dependencies: dependencies,
|
|
2867
|
-
devDependencies: devDependencies,
|
|
2868
|
-
resolutions: resolutions,
|
|
2869
|
-
publishConfig: publishConfig,
|
|
2870
|
-
packageManager: packageManager
|
|
2871
|
-
};
|
|
2872
|
-
|
|
2873
2810
|
function run(args, options) {
|
|
2874
2811
|
const RUNNER_PATH = resolve$1(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
2875
2812
|
return new Promise((resolve2, reject) => {
|
|
@@ -3129,129 +3066,6 @@ function registerHandlers(session) {
|
|
|
3129
3066
|
});
|
|
3130
3067
|
}
|
|
3131
3068
|
|
|
3132
|
-
const defaultSettings = {
|
|
3133
|
-
onboardingCompleted: false
|
|
3134
|
-
};
|
|
3135
|
-
async function readSettings() {
|
|
3136
|
-
if (!existsSync(configuration.settingsFile)) {
|
|
3137
|
-
return { ...defaultSettings };
|
|
3138
|
-
}
|
|
3139
|
-
try {
|
|
3140
|
-
const content = await readFile(configuration.settingsFile, "utf8");
|
|
3141
|
-
return JSON.parse(content);
|
|
3142
|
-
} catch {
|
|
3143
|
-
return { ...defaultSettings };
|
|
3144
|
-
}
|
|
3145
|
-
}
|
|
3146
|
-
async function updateSettings(updater) {
|
|
3147
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
3148
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
3149
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
3150
|
-
const lockFile = configuration.settingsFile + ".lock";
|
|
3151
|
-
const tmpFile = configuration.settingsFile + ".tmp";
|
|
3152
|
-
let fileHandle;
|
|
3153
|
-
let attempts = 0;
|
|
3154
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
3155
|
-
try {
|
|
3156
|
-
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
3157
|
-
break;
|
|
3158
|
-
} catch (err) {
|
|
3159
|
-
if (err.code === "EEXIST") {
|
|
3160
|
-
attempts++;
|
|
3161
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
3162
|
-
try {
|
|
3163
|
-
const stats = await stat$1(lockFile);
|
|
3164
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
3165
|
-
await unlink(lockFile).catch(() => {
|
|
3166
|
-
});
|
|
3167
|
-
}
|
|
3168
|
-
} catch {
|
|
3169
|
-
}
|
|
3170
|
-
} else {
|
|
3171
|
-
throw err;
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
if (!fileHandle) {
|
|
3176
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
3177
|
-
}
|
|
3178
|
-
try {
|
|
3179
|
-
const current = await readSettings() || { ...defaultSettings };
|
|
3180
|
-
const updated = await updater(current);
|
|
3181
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3182
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3183
|
-
}
|
|
3184
|
-
await writeFile$1(tmpFile, JSON.stringify(updated, null, 2));
|
|
3185
|
-
await rename(tmpFile, configuration.settingsFile);
|
|
3186
|
-
return updated;
|
|
3187
|
-
} finally {
|
|
3188
|
-
await fileHandle.close();
|
|
3189
|
-
await unlink(lockFile).catch(() => {
|
|
3190
|
-
});
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
const credentialsSchema = z.object({
|
|
3194
|
-
secret: z.string().base64(),
|
|
3195
|
-
token: z.string()
|
|
3196
|
-
});
|
|
3197
|
-
async function readCredentials() {
|
|
3198
|
-
if (!existsSync(configuration.privateKeyFile)) {
|
|
3199
|
-
return null;
|
|
3200
|
-
}
|
|
3201
|
-
try {
|
|
3202
|
-
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
3203
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
3204
|
-
return {
|
|
3205
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
3206
|
-
token: credentials.token
|
|
3207
|
-
};
|
|
3208
|
-
} catch {
|
|
3209
|
-
return null;
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
async function writeCredentials(credentials) {
|
|
3213
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3214
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3215
|
-
}
|
|
3216
|
-
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
3217
|
-
secret: encodeBase64(credentials.secret),
|
|
3218
|
-
token: credentials.token
|
|
3219
|
-
}, null, 2));
|
|
3220
|
-
}
|
|
3221
|
-
async function clearCredentials() {
|
|
3222
|
-
if (existsSync(configuration.privateKeyFile)) {
|
|
3223
|
-
await unlink(configuration.privateKeyFile);
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
async function clearMachineId() {
|
|
3227
|
-
await updateSettings((settings) => ({
|
|
3228
|
-
...settings,
|
|
3229
|
-
machineId: void 0
|
|
3230
|
-
}));
|
|
3231
|
-
}
|
|
3232
|
-
async function readDaemonState() {
|
|
3233
|
-
try {
|
|
3234
|
-
if (!existsSync(configuration.daemonStateFile)) {
|
|
3235
|
-
return null;
|
|
3236
|
-
}
|
|
3237
|
-
const content = await readFile(configuration.daemonStateFile, "utf-8");
|
|
3238
|
-
return JSON.parse(content);
|
|
3239
|
-
} catch (error) {
|
|
3240
|
-
return null;
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
async function writeDaemonState(state) {
|
|
3244
|
-
if (!existsSync(configuration.happyHomeDir)) {
|
|
3245
|
-
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
3246
|
-
}
|
|
3247
|
-
await writeFile$1(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
3248
|
-
}
|
|
3249
|
-
async function clearDaemonState() {
|
|
3250
|
-
if (existsSync(configuration.daemonStateFile)) {
|
|
3251
|
-
await unlink(configuration.daemonStateFile);
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
3069
|
class MessageQueue2 {
|
|
3256
3070
|
queue = [];
|
|
3257
3071
|
// Made public for testing
|
|
@@ -3619,8 +3433,9 @@ function startCaffeinate() {
|
|
|
3619
3433
|
}
|
|
3620
3434
|
}
|
|
3621
3435
|
let isStopping = false;
|
|
3622
|
-
function stopCaffeinate() {
|
|
3436
|
+
async function stopCaffeinate() {
|
|
3623
3437
|
if (isStopping) {
|
|
3438
|
+
logger.debug("[caffeinate] Already stopping, skipping");
|
|
3624
3439
|
return;
|
|
3625
3440
|
}
|
|
3626
3441
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
@@ -3628,14 +3443,13 @@ function stopCaffeinate() {
|
|
|
3628
3443
|
logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
3629
3444
|
try {
|
|
3630
3445
|
caffeinateProcess.kill("SIGTERM");
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
}, 1e3);
|
|
3446
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3447
|
+
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3448
|
+
logger.debug("[caffeinate] Force killing caffeinate process");
|
|
3449
|
+
caffeinateProcess.kill("SIGKILL");
|
|
3450
|
+
}
|
|
3451
|
+
caffeinateProcess = null;
|
|
3452
|
+
isStopping = false;
|
|
3639
3453
|
} catch (error) {
|
|
3640
3454
|
logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3641
3455
|
isStopping = false;
|
|
@@ -3659,12 +3473,10 @@ function setupCleanupHandlers() {
|
|
|
3659
3473
|
process.on("uncaughtException", (error) => {
|
|
3660
3474
|
logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
|
|
3661
3475
|
cleanup();
|
|
3662
|
-
process.exit(1);
|
|
3663
3476
|
});
|
|
3664
3477
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3665
3478
|
logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
|
|
3666
3479
|
cleanup();
|
|
3667
|
-
process.exit(1);
|
|
3668
3480
|
});
|
|
3669
3481
|
}
|
|
3670
3482
|
|
|
@@ -3713,37 +3525,97 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3713
3525
|
});
|
|
3714
3526
|
}
|
|
3715
3527
|
|
|
3716
|
-
async function
|
|
3528
|
+
async function daemonPost(path, body) {
|
|
3529
|
+
const state = await readDaemonState();
|
|
3530
|
+
if (!state?.httpPort) {
|
|
3531
|
+
const errorMessage = "No daemon running, no state file found";
|
|
3532
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3533
|
+
return {
|
|
3534
|
+
error: errorMessage
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3717
3537
|
try {
|
|
3718
|
-
|
|
3719
|
-
if (!state) {
|
|
3720
|
-
return false;
|
|
3721
|
-
}
|
|
3722
|
-
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3723
|
-
if (!isRunning) {
|
|
3724
|
-
logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3725
|
-
await cleanupDaemonState();
|
|
3726
|
-
return false;
|
|
3727
|
-
}
|
|
3728
|
-
return true;
|
|
3538
|
+
process.kill(state.pid, 0);
|
|
3729
3539
|
} catch (error) {
|
|
3730
|
-
|
|
3731
|
-
|
|
3540
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
3541
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3542
|
+
return {
|
|
3543
|
+
error: errorMessage
|
|
3544
|
+
};
|
|
3732
3545
|
}
|
|
3733
|
-
}
|
|
3734
|
-
async function getDaemonState() {
|
|
3735
3546
|
try {
|
|
3736
|
-
|
|
3547
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
3548
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3549
|
+
method: "POST",
|
|
3550
|
+
headers: { "Content-Type": "application/json" },
|
|
3551
|
+
body: JSON.stringify(body || {}),
|
|
3552
|
+
// Mostly increased for stress test
|
|
3553
|
+
signal: AbortSignal.timeout(timeout)
|
|
3554
|
+
});
|
|
3555
|
+
if (!response.ok) {
|
|
3556
|
+
const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
|
|
3557
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3558
|
+
return {
|
|
3559
|
+
error: errorMessage
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
return await response.json();
|
|
3737
3563
|
} catch (error) {
|
|
3738
|
-
|
|
3739
|
-
|
|
3564
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
3565
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3566
|
+
return {
|
|
3567
|
+
error: errorMessage
|
|
3568
|
+
};
|
|
3740
3569
|
}
|
|
3741
3570
|
}
|
|
3742
|
-
async function
|
|
3571
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
3572
|
+
return await daemonPost("/session-started", {
|
|
3573
|
+
sessionId,
|
|
3574
|
+
metadata
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
async function listDaemonSessions() {
|
|
3578
|
+
const result = await daemonPost("/list");
|
|
3579
|
+
return result.children || [];
|
|
3580
|
+
}
|
|
3581
|
+
async function stopDaemonSession(sessionId) {
|
|
3582
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3583
|
+
return result.success || false;
|
|
3584
|
+
}
|
|
3585
|
+
async function stopDaemonHttp() {
|
|
3586
|
+
await daemonPost("/stop");
|
|
3587
|
+
}
|
|
3588
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
3589
|
+
const state = await readDaemonState();
|
|
3590
|
+
if (!state) {
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3743
3593
|
try {
|
|
3744
|
-
process.kill(pid, 0);
|
|
3594
|
+
process.kill(state.pid, 0);
|
|
3745
3595
|
return true;
|
|
3746
3596
|
} catch {
|
|
3597
|
+
logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3598
|
+
await cleanupDaemonState();
|
|
3599
|
+
return false;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
async function isDaemonRunningSameVersion() {
|
|
3603
|
+
logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3604
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3605
|
+
if (!runningDaemon) {
|
|
3606
|
+
logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
3607
|
+
return false;
|
|
3608
|
+
}
|
|
3609
|
+
const state = await readDaemonState();
|
|
3610
|
+
if (!state) {
|
|
3611
|
+
logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
try {
|
|
3615
|
+
logger.debug(`[DAEMON CONTROL] Current CLI version: ${configuration.currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3616
|
+
return configuration.currentCliVersion === state.startedWithCliVersion;
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3747
3619
|
return false;
|
|
3748
3620
|
}
|
|
3749
3621
|
}
|
|
@@ -3755,47 +3627,102 @@ async function cleanupDaemonState() {
|
|
|
3755
3627
|
logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3756
3628
|
}
|
|
3757
3629
|
}
|
|
3630
|
+
async function stopDaemon() {
|
|
3631
|
+
try {
|
|
3632
|
+
const state = await readDaemonState();
|
|
3633
|
+
if (!state) {
|
|
3634
|
+
logger.debug("No daemon state found");
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3638
|
+
try {
|
|
3639
|
+
await stopDaemonHttp();
|
|
3640
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
3641
|
+
logger.debug("Daemon stopped gracefully via HTTP");
|
|
3642
|
+
return;
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
logger.debug("HTTP stop failed, will force kill", error);
|
|
3645
|
+
}
|
|
3646
|
+
try {
|
|
3647
|
+
process.kill(state.pid, "SIGKILL");
|
|
3648
|
+
logger.debug("Force killed daemon");
|
|
3649
|
+
} catch (error) {
|
|
3650
|
+
logger.debug("Daemon already dead");
|
|
3651
|
+
}
|
|
3652
|
+
} catch (error) {
|
|
3653
|
+
logger.debug("Error stopping daemon", error);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3657
|
+
const start = Date.now();
|
|
3658
|
+
while (Date.now() - start < timeout) {
|
|
3659
|
+
try {
|
|
3660
|
+
process.kill(pid, 0);
|
|
3661
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3662
|
+
} catch {
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
throw new Error("Process did not die within timeout");
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3758
3669
|
function findAllHappyProcesses() {
|
|
3759
3670
|
try {
|
|
3760
|
-
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3761
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3762
3671
|
const allProcesses = [];
|
|
3763
|
-
|
|
3764
|
-
const
|
|
3765
|
-
|
|
3766
|
-
const
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3672
|
+
try {
|
|
3673
|
+
const happyOutput = execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3674
|
+
const happyLines = happyOutput.trim().split("\n").filter((line) => line.trim());
|
|
3675
|
+
for (const line of happyLines) {
|
|
3676
|
+
const parts = line.trim().split(/\s+/);
|
|
3677
|
+
if (parts.length < 11) continue;
|
|
3678
|
+
const pid = parseInt(parts[1]);
|
|
3679
|
+
const command = parts.slice(10).join(" ");
|
|
3680
|
+
let type = "unknown";
|
|
3681
|
+
if (pid === process.pid) {
|
|
3682
|
+
type = "current";
|
|
3683
|
+
} else if (command.includes("--version")) {
|
|
3684
|
+
type = "daemon-version-check";
|
|
3685
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3686
|
+
type = "daemon";
|
|
3687
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3688
|
+
type = "daemon-spawned-session";
|
|
3689
|
+
} else if (command.includes("doctor")) {
|
|
3690
|
+
type = "doctor";
|
|
3691
|
+
} else {
|
|
3692
|
+
type = "user-session";
|
|
3693
|
+
}
|
|
3694
|
+
allProcesses.push({ pid, command, type });
|
|
3779
3695
|
}
|
|
3780
|
-
|
|
3696
|
+
} catch {
|
|
3781
3697
|
}
|
|
3782
3698
|
try {
|
|
3783
|
-
const devOutput = execSync('ps aux | grep -E "
|
|
3699
|
+
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3784
3700
|
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3785
3701
|
for (const line of devLines) {
|
|
3786
3702
|
const parts = line.trim().split(/\s+/);
|
|
3787
3703
|
if (parts.length < 11) continue;
|
|
3788
3704
|
const pid = parseInt(parts[1]);
|
|
3789
3705
|
const command = parts.slice(10).join(" ");
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
const pwdOutput = execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3793
|
-
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3794
|
-
} catch {
|
|
3706
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3707
|
+
continue;
|
|
3795
3708
|
}
|
|
3796
|
-
|
|
3797
|
-
|
|
3709
|
+
let type = "unknown";
|
|
3710
|
+
if (pid === process.pid) {
|
|
3711
|
+
type = "current";
|
|
3712
|
+
} else if (command.includes("--version")) {
|
|
3713
|
+
type = "dev-daemon-version-check";
|
|
3714
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3715
|
+
type = "dev-daemon";
|
|
3716
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3717
|
+
type = "dev-daemon-spawned";
|
|
3718
|
+
} else if (command.includes("doctor")) {
|
|
3719
|
+
type = "dev-doctor";
|
|
3720
|
+
} else if (command.includes("--yolo")) {
|
|
3721
|
+
type = "dev-session";
|
|
3722
|
+
} else {
|
|
3723
|
+
type = "dev-related";
|
|
3798
3724
|
}
|
|
3725
|
+
allProcesses.push({ pid, command, type });
|
|
3799
3726
|
}
|
|
3800
3727
|
} catch {
|
|
3801
3728
|
}
|
|
@@ -3806,18 +3733,39 @@ function findAllHappyProcesses() {
|
|
|
3806
3733
|
}
|
|
3807
3734
|
function findRunawayHappyProcesses() {
|
|
3808
3735
|
try {
|
|
3809
|
-
const output = execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3810
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3811
3736
|
const processes = [];
|
|
3812
|
-
|
|
3813
|
-
const
|
|
3814
|
-
|
|
3815
|
-
const
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3737
|
+
try {
|
|
3738
|
+
const output = execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3739
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3740
|
+
for (const line of lines) {
|
|
3741
|
+
const parts = line.trim().split(/\s+/);
|
|
3742
|
+
if (parts.length < 11) continue;
|
|
3743
|
+
const pid = parseInt(parts[1]);
|
|
3744
|
+
const command = parts.slice(10).join(" ");
|
|
3745
|
+
if (pid === process.pid) continue;
|
|
3746
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3747
|
+
processes.push({ pid, command });
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3752
|
+
try {
|
|
3753
|
+
const devOutput = execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3754
|
+
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3755
|
+
for (const line of devLines) {
|
|
3756
|
+
const parts = line.trim().split(/\s+/);
|
|
3757
|
+
if (parts.length < 11) continue;
|
|
3758
|
+
const pid = parseInt(parts[1]);
|
|
3759
|
+
const command = parts.slice(10).join(" ");
|
|
3760
|
+
if (pid === process.pid) continue;
|
|
3761
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3765
|
+
processes.push({ pid, command });
|
|
3766
|
+
}
|
|
3820
3767
|
}
|
|
3768
|
+
} catch {
|
|
3821
3769
|
}
|
|
3822
3770
|
return processes;
|
|
3823
3771
|
} catch (error) {
|
|
@@ -3827,10 +3775,10 @@ function findRunawayHappyProcesses() {
|
|
|
3827
3775
|
async function killRunawayHappyProcesses() {
|
|
3828
3776
|
const runawayProcesses = findRunawayHappyProcesses();
|
|
3829
3777
|
const errors = [];
|
|
3830
|
-
|
|
3831
|
-
for (const { pid, command } of runawayProcesses) {
|
|
3778
|
+
const killPromises = runawayProcesses.map(async ({ pid, command }) => {
|
|
3832
3779
|
try {
|
|
3833
3780
|
process.kill(pid, "SIGTERM");
|
|
3781
|
+
console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
|
|
3834
3782
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3835
3783
|
try {
|
|
3836
3784
|
process.kill(pid, 0);
|
|
@@ -3838,148 +3786,114 @@ async function killRunawayHappyProcesses() {
|
|
|
3838
3786
|
process.kill(pid, "SIGKILL");
|
|
3839
3787
|
} catch {
|
|
3840
3788
|
}
|
|
3841
|
-
killed
|
|
3842
|
-
|
|
3789
|
+
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3790
|
+
return { success: true, pid, command };
|
|
3843
3791
|
} catch (error) {
|
|
3844
|
-
|
|
3792
|
+
const errorMessage = error.message;
|
|
3793
|
+
errors.push({ pid, error: errorMessage });
|
|
3794
|
+
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3795
|
+
return { success: false, pid, command };
|
|
3845
3796
|
}
|
|
3846
|
-
}
|
|
3797
|
+
});
|
|
3798
|
+
const results = await Promise.all(killPromises);
|
|
3799
|
+
const killed = results.filter((r) => r.success).length;
|
|
3847
3800
|
return { killed, errors };
|
|
3848
3801
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
}
|
|
3878
|
-
async function waitForProcessDeath(pid, timeout) {
|
|
3879
|
-
const start = Date.now();
|
|
3880
|
-
while (Date.now() - start < timeout) {
|
|
3881
|
-
try {
|
|
3882
|
-
process.kill(pid, 0);
|
|
3883
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3884
|
-
} catch {
|
|
3885
|
-
return;
|
|
3886
|
-
}
|
|
3887
|
-
}
|
|
3888
|
-
throw new Error("Process did not die within timeout");
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
3892
|
-
__proto__: null,
|
|
3893
|
-
cleanupDaemonState: cleanupDaemonState,
|
|
3894
|
-
findAllHappyProcesses: findAllHappyProcesses,
|
|
3895
|
-
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3896
|
-
getDaemonState: getDaemonState,
|
|
3897
|
-
isDaemonRunning: isDaemonRunning,
|
|
3898
|
-
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3899
|
-
stopDaemon: stopDaemon
|
|
3900
|
-
});
|
|
3901
|
-
|
|
3902
|
-
function getEnvironmentInfo() {
|
|
3903
|
-
return {
|
|
3904
|
-
PWD: process.env.PWD,
|
|
3905
|
-
HAPPY_HOME_DIR: process.env.HAPPY_HOME_DIR,
|
|
3906
|
-
HAPPY_SERVER_URL: process.env.HAPPY_SERVER_URL,
|
|
3907
|
-
HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT,
|
|
3908
|
-
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING,
|
|
3909
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
3910
|
-
DEBUG: process.env.DEBUG,
|
|
3911
|
-
workingDirectory: process.cwd(),
|
|
3912
|
-
processArgv: process.argv,
|
|
3913
|
-
happyDir: configuration?.happyHomeDir,
|
|
3914
|
-
serverUrl: configuration?.serverUrl,
|
|
3915
|
-
logsDir: configuration?.logsDir
|
|
3916
|
-
};
|
|
3917
|
-
}
|
|
3918
|
-
function getLogFiles(logDir) {
|
|
3919
|
-
if (!existsSync(logDir)) {
|
|
3920
|
-
return [];
|
|
3921
|
-
}
|
|
3802
|
+
|
|
3803
|
+
function getEnvironmentInfo() {
|
|
3804
|
+
return {
|
|
3805
|
+
PWD: process.env.PWD,
|
|
3806
|
+
HAPPY_HOME_DIR: process.env.HAPPY_HOME_DIR,
|
|
3807
|
+
HAPPY_SERVER_URL: process.env.HAPPY_SERVER_URL,
|
|
3808
|
+
HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT,
|
|
3809
|
+
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING,
|
|
3810
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
3811
|
+
DEBUG: process.env.DEBUG,
|
|
3812
|
+
workingDirectory: process.cwd(),
|
|
3813
|
+
processArgv: process.argv,
|
|
3814
|
+
happyDir: configuration?.happyHomeDir,
|
|
3815
|
+
serverUrl: configuration?.serverUrl,
|
|
3816
|
+
logsDir: configuration?.logsDir,
|
|
3817
|
+
processPid: process.pid,
|
|
3818
|
+
nodeVersion: process.version,
|
|
3819
|
+
platform: process.platform,
|
|
3820
|
+
arch: process.arch,
|
|
3821
|
+
user: process.env.USER,
|
|
3822
|
+
home: process.env.HOME,
|
|
3823
|
+
shell: process.env.SHELL,
|
|
3824
|
+
terminal: process.env.TERM
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
function getLogFiles(logDir) {
|
|
3828
|
+
if (!existsSync(logDir)) {
|
|
3829
|
+
return [];
|
|
3830
|
+
}
|
|
3922
3831
|
try {
|
|
3923
3832
|
return readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
|
|
3924
3833
|
const path = join(logDir, file);
|
|
3925
3834
|
const stats = statSync(path);
|
|
3926
3835
|
return { file, path, modified: stats.mtime };
|
|
3927
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
3836
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
3928
3837
|
} catch {
|
|
3929
3838
|
return [];
|
|
3930
3839
|
}
|
|
3931
3840
|
}
|
|
3932
|
-
async function runDoctorCommand() {
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3936
|
-
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3937
|
-
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3938
|
-
console.log("");
|
|
3939
|
-
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3940
|
-
const projectRoot = projectPath();
|
|
3941
|
-
const wrapperPath = join(projectRoot, "bin", "happy.mjs");
|
|
3942
|
-
const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
|
|
3943
|
-
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3944
|
-
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3945
|
-
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3946
|
-
console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3947
|
-
console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3948
|
-
console.log("");
|
|
3949
|
-
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3950
|
-
console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
|
|
3951
|
-
console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
|
|
3952
|
-
console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
|
|
3953
|
-
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3954
|
-
const env = getEnvironmentInfo();
|
|
3955
|
-
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3956
|
-
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3957
|
-
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3958
|
-
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3959
|
-
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3960
|
-
try {
|
|
3961
|
-
const settings = await readSettings();
|
|
3962
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3963
|
-
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3964
|
-
} catch (error) {
|
|
3965
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3966
|
-
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3841
|
+
async function runDoctorCommand(filter) {
|
|
3842
|
+
if (!filter) {
|
|
3843
|
+
filter = "all";
|
|
3967
3844
|
}
|
|
3968
|
-
console.log(chalk.bold("\n\u{
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3845
|
+
console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
|
|
3846
|
+
if (filter === "all") {
|
|
3847
|
+
console.log(chalk.bold("\u{1F4CB} Basic Information"));
|
|
3848
|
+
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3849
|
+
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3850
|
+
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3851
|
+
console.log("");
|
|
3852
|
+
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3853
|
+
const projectRoot = projectPath();
|
|
3854
|
+
const wrapperPath = join(projectRoot, "bin", "happy.mjs");
|
|
3855
|
+
const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
|
|
3856
|
+
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3857
|
+
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3858
|
+
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3859
|
+
console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3860
|
+
console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3861
|
+
console.log("");
|
|
3862
|
+
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3863
|
+
console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
|
|
3864
|
+
console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
|
|
3865
|
+
console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
|
|
3866
|
+
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3867
|
+
const env = getEnvironmentInfo();
|
|
3868
|
+
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3869
|
+
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3870
|
+
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3871
|
+
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3872
|
+
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3873
|
+
try {
|
|
3874
|
+
const settings = await readSettings();
|
|
3875
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3876
|
+
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3877
|
+
} catch (error) {
|
|
3878
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3879
|
+
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3880
|
+
}
|
|
3881
|
+
console.log(chalk.bold("\n\u{1F510} Authentication"));
|
|
3882
|
+
try {
|
|
3883
|
+
const credentials = await readCredentials();
|
|
3884
|
+
if (credentials) {
|
|
3885
|
+
console.log(chalk.green("\u2713 Authenticated (credentials found)"));
|
|
3886
|
+
} else {
|
|
3887
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
|
|
3888
|
+
}
|
|
3889
|
+
} catch (error) {
|
|
3890
|
+
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3975
3891
|
}
|
|
3976
|
-
} catch (error) {
|
|
3977
|
-
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3978
3892
|
}
|
|
3979
3893
|
console.log(chalk.bold("\n\u{1F916} Daemon Status"));
|
|
3980
3894
|
try {
|
|
3981
|
-
const isRunning = await
|
|
3982
|
-
const state = await
|
|
3895
|
+
const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3896
|
+
const state = await readDaemonState();
|
|
3983
3897
|
if (isRunning && state) {
|
|
3984
3898
|
console.log(chalk.green("\u2713 Daemon is running"));
|
|
3985
3899
|
console.log(` PID: ${state.pid}`);
|
|
@@ -4010,9 +3924,11 @@ async function runDoctorCommand() {
|
|
|
4010
3924
|
const typeLabels = {
|
|
4011
3925
|
"current": "\u{1F4CD} Current Process",
|
|
4012
3926
|
"daemon": "\u{1F916} Daemon",
|
|
3927
|
+
"daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
|
|
4013
3928
|
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
4014
3929
|
"user-session": "\u{1F464} User Sessions",
|
|
4015
3930
|
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3931
|
+
"dev-daemon-version-check": "\u{1F6E0}\uFE0F Dev Daemon Version Check (stuck)",
|
|
4016
3932
|
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
4017
3933
|
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
4018
3934
|
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
@@ -4026,190 +3942,54 @@ ${typeLabels[type] || type}:`));
|
|
|
4026
3942
|
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4027
3943
|
});
|
|
4028
3944
|
});
|
|
3945
|
+
} else {
|
|
3946
|
+
console.log(chalk.red("\u274C No happy processes found"));
|
|
4029
3947
|
}
|
|
4030
|
-
|
|
4031
|
-
if (runawayProcesses.length > 0) {
|
|
4032
|
-
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
4033
|
-
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
4034
|
-
runawayProcesses.forEach(({ pid, command }) => {
|
|
4035
|
-
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4036
|
-
});
|
|
4037
|
-
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
4038
|
-
}
|
|
4039
|
-
if (allProcesses.length > 1) {
|
|
3948
|
+
if (filter === "all" && allProcesses.length > 1) {
|
|
4040
3949
|
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
4041
|
-
console.log(chalk.gray("To
|
|
3950
|
+
console.log(chalk.gray("To clean up runaway processes: happy doctor clean"));
|
|
4042
3951
|
}
|
|
4043
3952
|
} catch (error) {
|
|
4044
3953
|
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
4045
3954
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
}
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
console.log(chalk.gray(` ${path}`));
|
|
4063
|
-
});
|
|
4064
|
-
} else {
|
|
4065
|
-
console.log(chalk.yellow("No daemon log files found"));
|
|
4066
|
-
}
|
|
4067
|
-
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
4068
|
-
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
4069
|
-
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
4070
|
-
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4071
|
-
}
|
|
4072
|
-
|
|
4073
|
-
async function daemonPost(path, body) {
|
|
4074
|
-
const state = await getDaemonState();
|
|
4075
|
-
if (!state?.httpPort) {
|
|
4076
|
-
throw new Error("No daemon running");
|
|
4077
|
-
}
|
|
4078
|
-
try {
|
|
4079
|
-
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4080
|
-
method: "POST",
|
|
4081
|
-
headers: { "Content-Type": "application/json" },
|
|
4082
|
-
body: JSON.stringify(body || {}),
|
|
4083
|
-
signal: AbortSignal.timeout(5e3)
|
|
4084
|
-
});
|
|
4085
|
-
if (!response.ok) {
|
|
4086
|
-
throw new Error(`HTTP ${response.status}`);
|
|
4087
|
-
}
|
|
4088
|
-
return await response.json();
|
|
4089
|
-
} catch (error) {
|
|
4090
|
-
logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
4091
|
-
throw error;
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
4095
|
-
await daemonPost("/session-started", {
|
|
4096
|
-
sessionId,
|
|
4097
|
-
metadata
|
|
4098
|
-
});
|
|
4099
|
-
}
|
|
4100
|
-
async function listDaemonSessions() {
|
|
4101
|
-
const result = await daemonPost("/list");
|
|
4102
|
-
return result.children || [];
|
|
4103
|
-
}
|
|
4104
|
-
async function stopDaemonSession(sessionId) {
|
|
4105
|
-
const result = await daemonPost("/stop-session", { sessionId });
|
|
4106
|
-
return result.success || false;
|
|
4107
|
-
}
|
|
4108
|
-
async function stopDaemonHttp() {
|
|
4109
|
-
await daemonPost("/stop");
|
|
4110
|
-
}
|
|
4111
|
-
|
|
4112
|
-
var controlClient = /*#__PURE__*/Object.freeze({
|
|
4113
|
-
__proto__: null,
|
|
4114
|
-
listDaemonSessions: listDaemonSessions,
|
|
4115
|
-
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
4116
|
-
stopDaemonHttp: stopDaemonHttp,
|
|
4117
|
-
stopDaemonSession: stopDaemonSession
|
|
4118
|
-
});
|
|
4119
|
-
|
|
4120
|
-
function startDaemonControlServer({
|
|
4121
|
-
getChildren,
|
|
4122
|
-
stopSession,
|
|
4123
|
-
spawnSession,
|
|
4124
|
-
requestShutdown,
|
|
4125
|
-
onHappySessionWebhook
|
|
4126
|
-
}) {
|
|
4127
|
-
return new Promise((resolve) => {
|
|
4128
|
-
const app = fastify({
|
|
4129
|
-
logger: false
|
|
4130
|
-
// We use our own logger
|
|
4131
|
-
});
|
|
4132
|
-
app.setValidatorCompiler(validatorCompiler);
|
|
4133
|
-
app.setSerializerCompiler(serializerCompiler);
|
|
4134
|
-
const typed = app.withTypeProvider();
|
|
4135
|
-
typed.post("/session-started", {
|
|
4136
|
-
schema: {
|
|
4137
|
-
body: z$1.object({
|
|
4138
|
-
sessionId: z$1.string(),
|
|
4139
|
-
metadata: z$1.any()
|
|
4140
|
-
// Metadata type from API
|
|
4141
|
-
})
|
|
4142
|
-
}
|
|
4143
|
-
}, async (request, reply) => {
|
|
4144
|
-
const { sessionId, metadata } = request.body;
|
|
4145
|
-
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4146
|
-
onHappySessionWebhook(sessionId, metadata);
|
|
4147
|
-
return { status: "ok" };
|
|
4148
|
-
});
|
|
4149
|
-
typed.post("/list", async (request, reply) => {
|
|
4150
|
-
const children = getChildren();
|
|
4151
|
-
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4152
|
-
return { children };
|
|
4153
|
-
});
|
|
4154
|
-
typed.post("/stop-session", {
|
|
4155
|
-
schema: {
|
|
4156
|
-
body: z$1.object({
|
|
4157
|
-
sessionId: z$1.string()
|
|
4158
|
-
})
|
|
4159
|
-
}
|
|
4160
|
-
}, async (request, reply) => {
|
|
4161
|
-
const { sessionId } = request.body;
|
|
4162
|
-
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4163
|
-
const success = stopSession(sessionId);
|
|
4164
|
-
return { success };
|
|
4165
|
-
});
|
|
4166
|
-
typed.post("/spawn-session", {
|
|
4167
|
-
schema: {
|
|
4168
|
-
body: z$1.object({
|
|
4169
|
-
directory: z$1.string(),
|
|
4170
|
-
sessionId: z$1.string().optional()
|
|
4171
|
-
})
|
|
3955
|
+
if (filter === "all") {
|
|
3956
|
+
console.log(chalk.bold("\n\u{1F4DD} Log Files"));
|
|
3957
|
+
const allLogs = getLogFiles(configuration.logsDir);
|
|
3958
|
+
if (allLogs.length > 0) {
|
|
3959
|
+
const daemonLogs = allLogs.filter(({ file }) => file.includes("daemon"));
|
|
3960
|
+
const regularLogs = allLogs.filter(({ file }) => !file.includes("daemon"));
|
|
3961
|
+
if (regularLogs.length > 0) {
|
|
3962
|
+
console.log(chalk.blue("\nRecent Logs:"));
|
|
3963
|
+
const logsToShow = regularLogs.slice(0, 10);
|
|
3964
|
+
logsToShow.forEach(({ file, path, modified }) => {
|
|
3965
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3966
|
+
console.log(chalk.gray(` ${path}`));
|
|
3967
|
+
});
|
|
3968
|
+
if (regularLogs.length > 10) {
|
|
3969
|
+
console.log(chalk.gray(` ... and ${regularLogs.length - 10} more log files`));
|
|
3970
|
+
}
|
|
4172
3971
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
}
|
|
3972
|
+
if (daemonLogs.length > 0) {
|
|
3973
|
+
console.log(chalk.blue("\nDaemon Logs:"));
|
|
3974
|
+
const daemonLogsToShow = daemonLogs.slice(0, 5);
|
|
3975
|
+
daemonLogsToShow.forEach(({ file, path, modified }) => {
|
|
3976
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3977
|
+
console.log(chalk.gray(` ${path}`));
|
|
3978
|
+
});
|
|
3979
|
+
if (daemonLogs.length > 5) {
|
|
3980
|
+
console.log(chalk.gray(` ... and ${daemonLogs.length - 5} more daemon log files`));
|
|
3981
|
+
}
|
|
4183
3982
|
} else {
|
|
4184
|
-
|
|
4185
|
-
return { error: "Failed to spawn session" };
|
|
4186
|
-
}
|
|
4187
|
-
});
|
|
4188
|
-
typed.post("/stop", async (request, reply) => {
|
|
4189
|
-
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4190
|
-
setTimeout(() => {
|
|
4191
|
-
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4192
|
-
requestShutdown();
|
|
4193
|
-
}, 50);
|
|
4194
|
-
return { status: "stopping" };
|
|
4195
|
-
});
|
|
4196
|
-
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4197
|
-
if (err) {
|
|
4198
|
-
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4199
|
-
throw err;
|
|
3983
|
+
console.log(chalk.yellow("\nNo daemon log files found"));
|
|
4200
3984
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
}
|
|
4210
|
-
});
|
|
4211
|
-
});
|
|
4212
|
-
});
|
|
3985
|
+
} else {
|
|
3986
|
+
console.log(chalk.yellow("No log files found"));
|
|
3987
|
+
}
|
|
3988
|
+
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
3989
|
+
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
3990
|
+
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
3991
|
+
}
|
|
3992
|
+
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4213
3993
|
}
|
|
4214
3994
|
|
|
4215
3995
|
function displayQRCode(url) {
|
|
@@ -4236,7 +4016,7 @@ async function openBrowser(url) {
|
|
|
4236
4016
|
return false;
|
|
4237
4017
|
}
|
|
4238
4018
|
logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
4239
|
-
await open
|
|
4019
|
+
await open(url);
|
|
4240
4020
|
logger.debug("[browser] Browser opened successfully");
|
|
4241
4021
|
return true;
|
|
4242
4022
|
} catch (error) {
|
|
@@ -4452,16 +4232,140 @@ function spawnHappyCLI(args, options = {}) {
|
|
|
4452
4232
|
directory = process.cwd();
|
|
4453
4233
|
}
|
|
4454
4234
|
const fullCommand = `happy ${args.join(" ")}`;
|
|
4455
|
-
logger.debug(`[
|
|
4235
|
+
logger.debug(`[SPAWN HAPPY CLI] Spawning: ${fullCommand} in ${directory}`);
|
|
4456
4236
|
const nodeArgs = [
|
|
4457
4237
|
"--no-warnings",
|
|
4458
4238
|
"--no-deprecation",
|
|
4459
4239
|
entrypoint,
|
|
4460
4240
|
...args
|
|
4461
4241
|
];
|
|
4242
|
+
if (!existsSync(entrypoint)) {
|
|
4243
|
+
const errorMessage = `Entrypoint ${entrypoint} does not exist`;
|
|
4244
|
+
logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
|
|
4245
|
+
throw new Error(errorMessage);
|
|
4246
|
+
}
|
|
4462
4247
|
return spawn$1("node", nodeArgs, options);
|
|
4463
4248
|
}
|
|
4464
4249
|
|
|
4250
|
+
function startDaemonControlServer({
|
|
4251
|
+
getChildren,
|
|
4252
|
+
stopSession,
|
|
4253
|
+
spawnSession,
|
|
4254
|
+
requestShutdown,
|
|
4255
|
+
onHappySessionWebhook
|
|
4256
|
+
}) {
|
|
4257
|
+
return new Promise((resolve) => {
|
|
4258
|
+
const app = fastify({
|
|
4259
|
+
logger: false
|
|
4260
|
+
// We use our own logger
|
|
4261
|
+
});
|
|
4262
|
+
app.setValidatorCompiler(validatorCompiler);
|
|
4263
|
+
app.setSerializerCompiler(serializerCompiler);
|
|
4264
|
+
const typed = app.withTypeProvider();
|
|
4265
|
+
typed.post("/session-started", {
|
|
4266
|
+
schema: {
|
|
4267
|
+
body: z.object({
|
|
4268
|
+
sessionId: z.string(),
|
|
4269
|
+
metadata: z.any()
|
|
4270
|
+
// Metadata type from API
|
|
4271
|
+
})
|
|
4272
|
+
}
|
|
4273
|
+
}, async (request, reply) => {
|
|
4274
|
+
const { sessionId, metadata } = request.body;
|
|
4275
|
+
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4276
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
4277
|
+
return { status: "ok" };
|
|
4278
|
+
});
|
|
4279
|
+
typed.post("/list", async (request, reply) => {
|
|
4280
|
+
const children = getChildren();
|
|
4281
|
+
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4282
|
+
return {
|
|
4283
|
+
children: children.map((child) => {
|
|
4284
|
+
delete child.childProcess;
|
|
4285
|
+
return child;
|
|
4286
|
+
})
|
|
4287
|
+
};
|
|
4288
|
+
});
|
|
4289
|
+
typed.post("/stop-session", {
|
|
4290
|
+
schema: {
|
|
4291
|
+
body: z.object({
|
|
4292
|
+
sessionId: z.string()
|
|
4293
|
+
})
|
|
4294
|
+
}
|
|
4295
|
+
}, async (request, reply) => {
|
|
4296
|
+
const { sessionId } = request.body;
|
|
4297
|
+
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4298
|
+
const success = stopSession(sessionId);
|
|
4299
|
+
return { success };
|
|
4300
|
+
});
|
|
4301
|
+
typed.post("/spawn-session", {
|
|
4302
|
+
schema: {
|
|
4303
|
+
body: z.object({
|
|
4304
|
+
directory: z.string(),
|
|
4305
|
+
sessionId: z.string().optional()
|
|
4306
|
+
})
|
|
4307
|
+
}
|
|
4308
|
+
}, async (request, reply) => {
|
|
4309
|
+
const { directory, sessionId } = request.body;
|
|
4310
|
+
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4311
|
+
const session = await spawnSession(directory, sessionId);
|
|
4312
|
+
if (session) {
|
|
4313
|
+
return {
|
|
4314
|
+
success: true,
|
|
4315
|
+
pid: session.pid,
|
|
4316
|
+
sessionId: session.happySessionId || "pending",
|
|
4317
|
+
message: session.message
|
|
4318
|
+
};
|
|
4319
|
+
} else {
|
|
4320
|
+
reply.code(500);
|
|
4321
|
+
return {
|
|
4322
|
+
success: false,
|
|
4323
|
+
error: "Failed to spawn session. Check the directory path and permissions."
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
});
|
|
4327
|
+
typed.post("/stop", async (request, reply) => {
|
|
4328
|
+
logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4329
|
+
setTimeout(() => {
|
|
4330
|
+
logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4331
|
+
requestShutdown();
|
|
4332
|
+
}, 50);
|
|
4333
|
+
return { status: "stopping" };
|
|
4334
|
+
});
|
|
4335
|
+
typed.post("/dev-simulate-error", {
|
|
4336
|
+
schema: {
|
|
4337
|
+
body: z.object({
|
|
4338
|
+
error: z.string()
|
|
4339
|
+
})
|
|
4340
|
+
}
|
|
4341
|
+
}, async (request, reply) => {
|
|
4342
|
+
const { error } = request.body;
|
|
4343
|
+
logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4344
|
+
setTimeout(() => {
|
|
4345
|
+
logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4346
|
+
throw new Error(error);
|
|
4347
|
+
}, 100);
|
|
4348
|
+
return { status: "error will be thrown" };
|
|
4349
|
+
});
|
|
4350
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4351
|
+
if (err) {
|
|
4352
|
+
logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4353
|
+
throw err;
|
|
4354
|
+
}
|
|
4355
|
+
const port = parseInt(address.split(":").pop());
|
|
4356
|
+
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4357
|
+
resolve({
|
|
4358
|
+
port,
|
|
4359
|
+
stop: async () => {
|
|
4360
|
+
logger.debug("[CONTROL SERVER] Stopping server");
|
|
4361
|
+
await app.close();
|
|
4362
|
+
logger.debug("[CONTROL SERVER] Server stopped");
|
|
4363
|
+
}
|
|
4364
|
+
});
|
|
4365
|
+
});
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4465
4369
|
const initialMachineMetadata = {
|
|
4466
4370
|
host: os.hostname(),
|
|
4467
4371
|
platform: os.platform(),
|
|
@@ -4470,37 +4374,79 @@ const initialMachineMetadata = {
|
|
|
4470
4374
|
happyHomeDir: configuration.happyHomeDir
|
|
4471
4375
|
};
|
|
4472
4376
|
async function startDaemon() {
|
|
4377
|
+
let requestShutdown;
|
|
4378
|
+
let resolvesWhenShutdownRequested = new Promise((resolve2) => {
|
|
4379
|
+
requestShutdown = (source, errorMessage) => {
|
|
4380
|
+
logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
4381
|
+
setTimeout(async () => {
|
|
4382
|
+
logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
|
|
4383
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4384
|
+
process.exit(1);
|
|
4385
|
+
}, 1e3);
|
|
4386
|
+
resolve2({ source, errorMessage });
|
|
4387
|
+
};
|
|
4388
|
+
});
|
|
4389
|
+
process.on("SIGINT", () => {
|
|
4390
|
+
logger.debug("[DAEMON RUN] Received SIGINT");
|
|
4391
|
+
requestShutdown("os-signal");
|
|
4392
|
+
});
|
|
4393
|
+
process.on("SIGTERM", () => {
|
|
4394
|
+
logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4395
|
+
requestShutdown("os-signal");
|
|
4396
|
+
});
|
|
4397
|
+
process.on("uncaughtException", (error) => {
|
|
4398
|
+
logger.debug("[DAEMON RUN] FATAL: Uncaught exception", error);
|
|
4399
|
+
logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4400
|
+
requestShutdown("exception", error.message);
|
|
4401
|
+
});
|
|
4402
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
4403
|
+
logger.debug("[DAEMON RUN] FATAL: Unhandled promise rejection", reason);
|
|
4404
|
+
logger.debug(`[DAEMON RUN] Rejected promise:`, promise);
|
|
4405
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${reason}`);
|
|
4406
|
+
logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4407
|
+
requestShutdown("exception", error.message);
|
|
4408
|
+
});
|
|
4409
|
+
process.on("exit", (code) => {
|
|
4410
|
+
logger.debug(`[DAEMON RUN] Process exiting with code: ${code}`);
|
|
4411
|
+
});
|
|
4412
|
+
process.on("beforeExit", (code) => {
|
|
4413
|
+
logger.debug(`[DAEMON RUN] Process about to exit with code: ${code}`);
|
|
4414
|
+
});
|
|
4473
4415
|
logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4474
4416
|
logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4475
|
-
const
|
|
4476
|
-
if (
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
logger.debug("[DAEMON RUN] Stale state found, cleaning up");
|
|
4484
|
-
await cleanupDaemonState();
|
|
4485
|
-
}
|
|
4417
|
+
const runningDaemonVersionMatches = await isDaemonRunningSameVersion();
|
|
4418
|
+
if (!runningDaemonVersionMatches) {
|
|
4419
|
+
logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4420
|
+
await stopDaemon();
|
|
4421
|
+
} else {
|
|
4422
|
+
logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
|
|
4423
|
+
console.log("Daemon already running with matching version");
|
|
4424
|
+
process.exit(0);
|
|
4486
4425
|
}
|
|
4487
|
-
const
|
|
4488
|
-
if (
|
|
4489
|
-
logger.debug("[DAEMON RUN]
|
|
4426
|
+
const daemonLockHandle = await acquireDaemonLock(5, 200);
|
|
4427
|
+
if (!daemonLockHandle) {
|
|
4428
|
+
logger.debug("[DAEMON RUN] Daemon lock file already held, another daemon is running");
|
|
4429
|
+
process.exit(0);
|
|
4490
4430
|
}
|
|
4491
4431
|
try {
|
|
4432
|
+
const caffeinateStarted = startCaffeinate();
|
|
4433
|
+
if (caffeinateStarted) {
|
|
4434
|
+
logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
4435
|
+
}
|
|
4492
4436
|
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4493
4437
|
logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4494
4438
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4495
4439
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4496
4440
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4497
4441
|
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4442
|
+
logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
4498
4443
|
const pid = sessionMetadata.hostPid;
|
|
4499
4444
|
if (!pid) {
|
|
4500
|
-
logger.debug(`[DAEMON RUN] Session webhook missing hostPid for
|
|
4445
|
+
logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
|
|
4501
4446
|
return;
|
|
4502
4447
|
}
|
|
4503
4448
|
logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
|
|
4449
|
+
logger.debug(`[DAEMON RUN] Current tracked sessions before webhook: ${Array.from(pidToTrackedSession.keys()).join(", ")}`);
|
|
4504
4450
|
const existingSession = pidToTrackedSession.get(pid);
|
|
4505
4451
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4506
4452
|
existingSession.happySessionId = sessionId;
|
|
@@ -4524,8 +4470,37 @@ async function startDaemon() {
|
|
|
4524
4470
|
}
|
|
4525
4471
|
};
|
|
4526
4472
|
const spawnSession = async (directory, sessionId) => {
|
|
4473
|
+
let directoryCreated = false;
|
|
4474
|
+
if (directory.startsWith("~")) {
|
|
4475
|
+
directory = resolve$1(os.homedir(), directory.replace("~", ""));
|
|
4476
|
+
}
|
|
4477
|
+
try {
|
|
4478
|
+
await fs.access(directory);
|
|
4479
|
+
logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4480
|
+
} catch (error) {
|
|
4481
|
+
logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4482
|
+
try {
|
|
4483
|
+
await fs.mkdir(directory, { recursive: true });
|
|
4484
|
+
logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
4485
|
+
directoryCreated = true;
|
|
4486
|
+
} catch (mkdirError) {
|
|
4487
|
+
let errorMessage = `Unable to create directory at '${directory}'. `;
|
|
4488
|
+
if (mkdirError.code === "EACCES") {
|
|
4489
|
+
errorMessage += `Permission denied. You don't have write access to create a folder at this location. Try using a different path or check your permissions.`;
|
|
4490
|
+
} else if (mkdirError.code === "ENOTDIR") {
|
|
4491
|
+
errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
|
|
4492
|
+
} else if (mkdirError.code === "ENOSPC") {
|
|
4493
|
+
errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
|
|
4494
|
+
} else if (mkdirError.code === "EROFS") {
|
|
4495
|
+
errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
|
|
4496
|
+
} else {
|
|
4497
|
+
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4498
|
+
}
|
|
4499
|
+
logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4500
|
+
return null;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4527
4503
|
try {
|
|
4528
|
-
const happyBinPath = join$1(projectPath(), "bin", "happy.mjs");
|
|
4529
4504
|
const args = [
|
|
4530
4505
|
"--happy-starting-mode",
|
|
4531
4506
|
"remote",
|
|
@@ -4540,12 +4515,14 @@ async function startDaemon() {
|
|
|
4540
4515
|
// Capture stdout/stderr for debugging
|
|
4541
4516
|
// env is inherited automatically from parent process
|
|
4542
4517
|
});
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4518
|
+
if (process.env.DEBUG) {
|
|
4519
|
+
happyProcess.stdout?.on("data", (data) => {
|
|
4520
|
+
logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
|
|
4521
|
+
});
|
|
4522
|
+
happyProcess.stderr?.on("data", (data) => {
|
|
4523
|
+
logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4549
4526
|
if (!happyProcess.pid) {
|
|
4550
4527
|
logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4551
4528
|
return null;
|
|
@@ -4554,7 +4531,9 @@ async function startDaemon() {
|
|
|
4554
4531
|
const trackedSession = {
|
|
4555
4532
|
startedBy: "daemon",
|
|
4556
4533
|
pid: happyProcess.pid,
|
|
4557
|
-
childProcess: happyProcess
|
|
4534
|
+
childProcess: happyProcess,
|
|
4535
|
+
directoryCreated,
|
|
4536
|
+
message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
|
|
4558
4537
|
};
|
|
4559
4538
|
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4560
4539
|
happyProcess.on("exit", (code, signal) => {
|
|
@@ -4570,16 +4549,16 @@ async function startDaemon() {
|
|
|
4570
4549
|
}
|
|
4571
4550
|
});
|
|
4572
4551
|
logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4573
|
-
return new Promise((
|
|
4552
|
+
return new Promise((resolve2, reject) => {
|
|
4574
4553
|
const timeout = setTimeout(() => {
|
|
4575
4554
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4576
4555
|
logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4577
|
-
|
|
4556
|
+
resolve2(trackedSession);
|
|
4578
4557
|
}, 1e4);
|
|
4579
4558
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4580
4559
|
clearTimeout(timeout);
|
|
4581
4560
|
logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4582
|
-
|
|
4561
|
+
resolve2(completedSession);
|
|
4583
4562
|
});
|
|
4584
4563
|
});
|
|
4585
4564
|
} catch (error) {
|
|
@@ -4618,10 +4597,6 @@ async function startDaemon() {
|
|
|
4618
4597
|
logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4619
4598
|
pidToTrackedSession.delete(pid);
|
|
4620
4599
|
};
|
|
4621
|
-
let requestShutdown;
|
|
4622
|
-
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4623
|
-
requestShutdown = resolve;
|
|
4624
|
-
});
|
|
4625
4600
|
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4626
4601
|
getChildren: getCurrentChildren,
|
|
4627
4602
|
stopSession,
|
|
@@ -4632,10 +4607,11 @@ async function startDaemon() {
|
|
|
4632
4607
|
const fileState = {
|
|
4633
4608
|
pid: process.pid,
|
|
4634
4609
|
httpPort: controlPort,
|
|
4635
|
-
startTime: (/* @__PURE__ */ new Date()).
|
|
4636
|
-
startedWithCliVersion: packageJson.version
|
|
4610
|
+
startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4611
|
+
startedWithCliVersion: packageJson.version,
|
|
4612
|
+
daemonLogPath: logger.logFilePath
|
|
4637
4613
|
};
|
|
4638
|
-
|
|
4614
|
+
writeDaemonState(fileState);
|
|
4639
4615
|
logger.debug("[DAEMON RUN] Daemon state written");
|
|
4640
4616
|
const initialDaemonState = {
|
|
4641
4617
|
status: "offline",
|
|
@@ -4657,56 +4633,89 @@ async function startDaemon() {
|
|
|
4657
4633
|
requestShutdown: () => requestShutdown("happy-app")
|
|
4658
4634
|
});
|
|
4659
4635
|
apiMachine.connect();
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
status: "shutting-down",
|
|
4666
|
-
shutdownRequestedAt: Date.now(),
|
|
4667
|
-
shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
|
|
4668
|
-
}));
|
|
4669
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4636
|
+
const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
|
|
4637
|
+
let heartbeatRunning = false;
|
|
4638
|
+
const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
|
|
4639
|
+
if (heartbeatRunning) {
|
|
4640
|
+
return;
|
|
4670
4641
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4642
|
+
heartbeatRunning = true;
|
|
4643
|
+
if (process.env.DEBUG) {
|
|
4644
|
+
logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
4673
4645
|
}
|
|
4674
|
-
|
|
4646
|
+
for (const [pid, _] of pidToTrackedSession.entries()) {
|
|
4647
|
+
try {
|
|
4648
|
+
process.kill(pid, 0);
|
|
4649
|
+
} catch (error) {
|
|
4650
|
+
logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
|
|
4651
|
+
pidToTrackedSession.delete(pid);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
const projectVersion = JSON.parse(readFileSync$1(join$1(projectPath(), "package.json"), "utf-8")).version;
|
|
4655
|
+
if (projectVersion !== configuration.currentCliVersion) {
|
|
4656
|
+
logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
4657
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4658
|
+
try {
|
|
4659
|
+
spawnHappyCLI(["daemon", "start"], {
|
|
4660
|
+
detached: true,
|
|
4661
|
+
stdio: "ignore"
|
|
4662
|
+
});
|
|
4663
|
+
} catch (error) {
|
|
4664
|
+
logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
|
|
4665
|
+
}
|
|
4666
|
+
logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
|
|
4667
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e4));
|
|
4668
|
+
process.exit(0);
|
|
4669
|
+
}
|
|
4670
|
+
const daemonState = await readDaemonState();
|
|
4671
|
+
if (daemonState && daemonState.pid !== process.pid) {
|
|
4672
|
+
logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
4673
|
+
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
4674
|
+
}
|
|
4675
|
+
try {
|
|
4676
|
+
const updatedState = {
|
|
4677
|
+
pid: process.pid,
|
|
4678
|
+
httpPort: controlPort,
|
|
4679
|
+
startTime: fileState.startTime,
|
|
4680
|
+
startedWithCliVersion: packageJson.version,
|
|
4681
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4682
|
+
daemonLogPath: fileState.daemonLogPath
|
|
4683
|
+
};
|
|
4684
|
+
writeDaemonState(updatedState);
|
|
4685
|
+
if (process.env.DEBUG) {
|
|
4686
|
+
logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
4687
|
+
}
|
|
4688
|
+
} catch (error) {
|
|
4689
|
+
logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
|
|
4690
|
+
}
|
|
4691
|
+
heartbeatRunning = false;
|
|
4692
|
+
}, heartbeatIntervalMs);
|
|
4693
|
+
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
4694
|
+
logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
4695
|
+
if (restartOnStaleVersionAndHeartbeat) {
|
|
4696
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4697
|
+
logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
4698
|
+
}
|
|
4699
|
+
await apiMachine.updateDaemonState((state) => ({
|
|
4700
|
+
...state,
|
|
4701
|
+
status: "shutting-down",
|
|
4702
|
+
shutdownRequestedAt: Date.now(),
|
|
4703
|
+
shutdownSource: source
|
|
4704
|
+
}));
|
|
4705
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4706
|
+
apiMachine.shutdown();
|
|
4675
4707
|
await stopControlServer();
|
|
4676
|
-
logger.debug("[DAEMON RUN] Control server stopped");
|
|
4677
4708
|
await cleanupDaemonState();
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
logger.debug("[DAEMON RUN]
|
|
4709
|
+
await stopCaffeinate();
|
|
4710
|
+
await releaseDaemonLock(daemonLockHandle);
|
|
4711
|
+
logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
|
|
4681
4712
|
process.exit(0);
|
|
4682
4713
|
};
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
});
|
|
4687
|
-
process.on("SIGTERM", () => {
|
|
4688
|
-
logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4689
|
-
cleanupAndShutdown("os-signal");
|
|
4690
|
-
});
|
|
4691
|
-
process.on("uncaughtException", (error) => {
|
|
4692
|
-
logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4693
|
-
cleanupAndShutdown("unknown");
|
|
4694
|
-
});
|
|
4695
|
-
process.on("unhandledRejection", (reason) => {
|
|
4696
|
-
logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4697
|
-
cleanupAndShutdown("unknown");
|
|
4698
|
-
});
|
|
4699
|
-
process.on("exit", () => {
|
|
4700
|
-
logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
4701
|
-
});
|
|
4702
|
-
logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4703
|
-
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4704
|
-
logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4705
|
-
await cleanupAndShutdown(shutdownSource);
|
|
4714
|
+
logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
|
|
4715
|
+
const shutdownRequest = await resolvesWhenShutdownRequested;
|
|
4716
|
+
await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
|
|
4706
4717
|
} catch (error) {
|
|
4707
|
-
logger.debug("[DAEMON RUN] Failed
|
|
4708
|
-
await cleanupDaemonState();
|
|
4709
|
-
stopCaffeinate();
|
|
4718
|
+
logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
4710
4719
|
process.exit(1);
|
|
4711
4720
|
}
|
|
4712
4721
|
}
|
|
@@ -4734,7 +4743,7 @@ async function startHappyServer(client) {
|
|
|
4734
4743
|
description: "Change the title of the current chat session",
|
|
4735
4744
|
title: "Change Chat Title",
|
|
4736
4745
|
inputSchema: {
|
|
4737
|
-
title: z
|
|
4746
|
+
title: z.string().describe("The new title for the chat session")
|
|
4738
4747
|
}
|
|
4739
4748
|
}, async (args) => {
|
|
4740
4749
|
const response = await handler(args.title);
|
|
@@ -4807,10 +4816,14 @@ async function start(credentials, options = {}) {
|
|
|
4807
4816
|
const settings = await readSettings();
|
|
4808
4817
|
let machineId = settings?.machineId;
|
|
4809
4818
|
if (!machineId) {
|
|
4810
|
-
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it
|
|
4811
|
-
|
|
4819
|
+
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
|
|
4820
|
+
process.exit(1);
|
|
4812
4821
|
}
|
|
4813
4822
|
logger.debug(`Using machineId: ${machineId}`);
|
|
4823
|
+
await api.createMachineOrGetExistingAsIs({
|
|
4824
|
+
machineId,
|
|
4825
|
+
metadata: initialMachineMetadata
|
|
4826
|
+
});
|
|
4814
4827
|
let metadata = {
|
|
4815
4828
|
path: workingDirectory,
|
|
4816
4829
|
host: os$1.hostname(),
|
|
@@ -4821,18 +4834,19 @@ async function start(credentials, options = {}) {
|
|
|
4821
4834
|
happyHomeDir: configuration.happyHomeDir,
|
|
4822
4835
|
startedFromDaemon: options.startedBy === "daemon",
|
|
4823
4836
|
hostPid: process.pid,
|
|
4824
|
-
startedBy: options.startedBy || "terminal"
|
|
4837
|
+
startedBy: options.startedBy || "terminal",
|
|
4838
|
+
// Initialize lifecycle state
|
|
4839
|
+
lifecycleState: "running",
|
|
4840
|
+
lifecycleStateSince: Date.now()
|
|
4825
4841
|
};
|
|
4826
4842
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4827
4843
|
logger.debug(`Session created: ${response.id}`);
|
|
4828
|
-
await api.createMachineOrGetExistingAsIs({
|
|
4829
|
-
machineId,
|
|
4830
|
-
metadata: initialMachineMetadata
|
|
4831
|
-
});
|
|
4832
4844
|
try {
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4845
|
+
logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
4846
|
+
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
|
4847
|
+
if (result.error) {
|
|
4848
|
+
logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
|
4849
|
+
} else {
|
|
4836
4850
|
logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
4837
4851
|
}
|
|
4838
4852
|
} catch (error) {
|
|
@@ -4854,7 +4868,7 @@ async function start(credentials, options = {}) {
|
|
|
4854
4868
|
const session = api.sessionSyncClient(response);
|
|
4855
4869
|
const happyServer = await startHappyServer(session);
|
|
4856
4870
|
logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4857
|
-
const logPath =
|
|
4871
|
+
const logPath = logger.logFilePath;
|
|
4858
4872
|
logger.infoDeveloper(`Session: ${response.id}`);
|
|
4859
4873
|
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
4860
4874
|
session.updateAgentState((currentState) => ({
|
|
@@ -4991,6 +5005,13 @@ async function start(credentials, options = {}) {
|
|
|
4991
5005
|
logger.debug("[START] Received termination signal, cleaning up...");
|
|
4992
5006
|
try {
|
|
4993
5007
|
if (session) {
|
|
5008
|
+
session.updateMetadata((currentMetadata) => ({
|
|
5009
|
+
...currentMetadata,
|
|
5010
|
+
lifecycleState: "archived",
|
|
5011
|
+
lifecycleStateSince: Date.now(),
|
|
5012
|
+
archivedBy: "cli",
|
|
5013
|
+
archiveReason: "User terminated"
|
|
5014
|
+
}));
|
|
4994
5015
|
session.sendSessionDeath();
|
|
4995
5016
|
await session.flush();
|
|
4996
5017
|
await session.close();
|
|
@@ -5218,19 +5239,12 @@ ${chalk.bold("Usage:")}
|
|
|
5218
5239
|
happy auth login [--force] Authenticate with Happy
|
|
5219
5240
|
happy auth logout Remove authentication and machine data
|
|
5220
5241
|
happy auth status Show authentication status
|
|
5221
|
-
happy auth
|
|
5242
|
+
happy auth backup Display backup key for mobile/web clients
|
|
5222
5243
|
happy auth help Show this help message
|
|
5223
5244
|
|
|
5224
5245
|
${chalk.bold("Options:")}
|
|
5225
5246
|
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
5226
5247
|
|
|
5227
|
-
${chalk.bold("Examples:")}
|
|
5228
|
-
happy auth login Authenticate if not already logged in
|
|
5229
|
-
happy auth login --force Force re-authentication (complete reset)
|
|
5230
|
-
happy auth status Check authentication and machine status
|
|
5231
|
-
happy auth show-backup Get backup key to link other devices
|
|
5232
|
-
happy auth logout Remove all authentication data
|
|
5233
|
-
|
|
5234
5248
|
${chalk.bold("Notes:")}
|
|
5235
5249
|
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
5236
5250
|
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
@@ -5367,8 +5381,7 @@ async function handleAuthStatus() {
|
|
|
5367
5381
|
console.log(chalk.gray(`
|
|
5368
5382
|
Data directory: ${configuration.happyHomeDir}`));
|
|
5369
5383
|
try {
|
|
5370
|
-
const
|
|
5371
|
-
const running = await isDaemonRunning();
|
|
5384
|
+
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
5372
5385
|
if (running) {
|
|
5373
5386
|
console.log(chalk.green("\u2713 Daemon running"));
|
|
5374
5387
|
} else {
|
|
@@ -5409,9 +5422,19 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5409
5422
|
|
|
5410
5423
|
(async () => {
|
|
5411
5424
|
const args = process.argv.slice(2);
|
|
5412
|
-
|
|
5425
|
+
if (!args.includes("--version")) {
|
|
5426
|
+
logger.debug("Starting happy CLI with args: ", process.argv);
|
|
5427
|
+
}
|
|
5413
5428
|
const subcommand = args[0];
|
|
5414
5429
|
if (subcommand === "doctor") {
|
|
5430
|
+
if (args[1] === "clean") {
|
|
5431
|
+
const result = await killRunawayHappyProcesses();
|
|
5432
|
+
console.log(`Cleaned up ${result.killed} runaway processes`);
|
|
5433
|
+
if (result.errors.length > 0) {
|
|
5434
|
+
console.log("Errors:", result.errors);
|
|
5435
|
+
}
|
|
5436
|
+
process.exit(0);
|
|
5437
|
+
}
|
|
5415
5438
|
await runDoctorCommand();
|
|
5416
5439
|
return;
|
|
5417
5440
|
} else if (subcommand === "auth") {
|
|
@@ -5454,16 +5477,10 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5454
5477
|
try {
|
|
5455
5478
|
const sessions = await listDaemonSessions();
|
|
5456
5479
|
if (sessions.length === 0) {
|
|
5457
|
-
console.log("No active sessions");
|
|
5480
|
+
console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
|
|
5458
5481
|
} else {
|
|
5459
5482
|
console.log("Active sessions:");
|
|
5460
|
-
|
|
5461
|
-
pid: s.pid,
|
|
5462
|
-
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
5463
|
-
startedBy: s.startedBy,
|
|
5464
|
-
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
5465
|
-
}));
|
|
5466
|
-
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
5483
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
5467
5484
|
}
|
|
5468
5485
|
} catch (error) {
|
|
5469
5486
|
console.log("No daemon running");
|
|
@@ -5491,7 +5508,7 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5491
5508
|
child.unref();
|
|
5492
5509
|
let started = false;
|
|
5493
5510
|
for (let i = 0; i < 50; i++) {
|
|
5494
|
-
if (await
|
|
5511
|
+
if (await checkIfDaemonRunningAndCleanupStaleState()) {
|
|
5495
5512
|
started = true;
|
|
5496
5513
|
break;
|
|
5497
5514
|
}
|
|
@@ -5511,28 +5528,14 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5511
5528
|
await stopDaemon();
|
|
5512
5529
|
process.exit(0);
|
|
5513
5530
|
} else if (daemonSubcommand === "status") {
|
|
5514
|
-
|
|
5515
|
-
if (!state) {
|
|
5516
|
-
console.log("Daemon is not running");
|
|
5517
|
-
} else {
|
|
5518
|
-
const isRunning = await isDaemonRunning();
|
|
5519
|
-
if (isRunning) {
|
|
5520
|
-
console.log("Daemon is running");
|
|
5521
|
-
console.log(` PID: ${state.pid}`);
|
|
5522
|
-
console.log(` Port: ${state.httpPort}`);
|
|
5523
|
-
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
5524
|
-
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
5525
|
-
} else {
|
|
5526
|
-
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
5527
|
-
}
|
|
5528
|
-
}
|
|
5531
|
+
await runDoctorCommand("daemon");
|
|
5529
5532
|
process.exit(0);
|
|
5530
|
-
} else if (daemonSubcommand === "
|
|
5531
|
-
const
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
console.log(
|
|
5533
|
+
} else if (daemonSubcommand === "logs") {
|
|
5534
|
+
const latest = await getLatestDaemonLog();
|
|
5535
|
+
if (!latest) {
|
|
5536
|
+
console.log("No daemon logs found");
|
|
5537
|
+
} else {
|
|
5538
|
+
console.log(latest.path);
|
|
5536
5539
|
}
|
|
5537
5540
|
process.exit(0);
|
|
5538
5541
|
} else if (daemonSubcommand === "install") {
|
|
@@ -5556,14 +5559,15 @@ ${chalk.bold("happy daemon")} - Daemon management
|
|
|
5556
5559
|
${chalk.bold("Usage:")}
|
|
5557
5560
|
happy daemon start Start the daemon (detached)
|
|
5558
5561
|
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5559
|
-
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5560
5562
|
happy daemon status Show daemon status
|
|
5561
5563
|
happy daemon list List active sessions
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
+
|
|
5565
|
+
If you want to kill all happy related processes run
|
|
5566
|
+
${chalk.cyan("happy doctor clean")}
|
|
5564
5567
|
|
|
5565
5568
|
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5566
|
-
|
|
5569
|
+
|
|
5570
|
+
${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor clean")}
|
|
5567
5571
|
`);
|
|
5568
5572
|
}
|
|
5569
5573
|
return;
|
|
@@ -5571,8 +5575,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5571
5575
|
const options = {};
|
|
5572
5576
|
let showHelp = false;
|
|
5573
5577
|
let showVersion = false;
|
|
5574
|
-
let forceAuth = false;
|
|
5575
|
-
let forceAuthNew = false;
|
|
5576
5578
|
const unknownArgs = [];
|
|
5577
5579
|
for (let i = 0; i < args.length; i++) {
|
|
5578
5580
|
const arg = args[i];
|
|
@@ -5582,12 +5584,8 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5582
5584
|
} else if (arg === "-v" || arg === "--version") {
|
|
5583
5585
|
showVersion = true;
|
|
5584
5586
|
unknownArgs.push(arg);
|
|
5585
|
-
} else if (arg === "--auth" || arg === "--login") {
|
|
5586
|
-
forceAuth = true;
|
|
5587
|
-
} else if (arg === "--force-auth") {
|
|
5588
|
-
forceAuthNew = true;
|
|
5589
5587
|
} else if (arg === "--happy-starting-mode") {
|
|
5590
|
-
options.startingMode = z
|
|
5588
|
+
options.startingMode = z.enum(["local", "remote"]).parse(args[++i]);
|
|
5591
5589
|
} else if (arg === "--yolo") {
|
|
5592
5590
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
5593
5591
|
} else if (arg === "--started-by") {
|
|
@@ -5607,29 +5605,23 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5607
5605
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
5608
5606
|
|
|
5609
5607
|
${chalk.bold("Usage:")}
|
|
5610
|
-
happy [options]
|
|
5611
|
-
happy auth Manage authentication
|
|
5612
|
-
happy
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
${chalk.bold("Happy Options:")}
|
|
5616
|
-
--help Show this help message
|
|
5617
|
-
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5618
|
-
--force-auth Force re-authentication
|
|
5619
|
-
|
|
5620
|
-
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5621
|
-
Use any claude flag exactly as you normally would.
|
|
5608
|
+
happy [options] Start Claude with mobile control
|
|
5609
|
+
happy auth Manage authentication
|
|
5610
|
+
happy daemon Manage background service that allows
|
|
5611
|
+
to spawn new sessions away from your computer
|
|
5612
|
+
happy doctor System diagnostics & troubleshooting
|
|
5622
5613
|
|
|
5623
5614
|
${chalk.bold("Examples:")}
|
|
5624
|
-
happy
|
|
5625
|
-
happy --yolo
|
|
5626
|
-
|
|
5627
|
-
happy
|
|
5628
|
-
happy
|
|
5629
|
-
happy notify -p "Done!" Send notification
|
|
5615
|
+
happy Start session
|
|
5616
|
+
happy --yolo Start with bypassing permissions
|
|
5617
|
+
happy sugar for --dangerously-skip-permissions
|
|
5618
|
+
happy auth login --force Authenticate
|
|
5619
|
+
happy doctor Run diagnostics
|
|
5630
5620
|
|
|
5631
|
-
${chalk.bold("Happy
|
|
5632
|
-
|
|
5621
|
+
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5622
|
+
Use any claude flag with happy as you would with claude. Our favorite:
|
|
5623
|
+
|
|
5624
|
+
happy --resume
|
|
5633
5625
|
|
|
5634
5626
|
${chalk.gray("\u2500".repeat(60))}
|
|
5635
5627
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
@@ -5644,33 +5636,11 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5644
5636
|
process.exit(0);
|
|
5645
5637
|
}
|
|
5646
5638
|
if (showVersion) {
|
|
5647
|
-
console.log(packageJson.version);
|
|
5648
|
-
process.exit(0);
|
|
5649
|
-
}
|
|
5650
|
-
let credentials;
|
|
5651
|
-
if (forceAuthNew) {
|
|
5652
|
-
console.log(chalk.yellow("Force authentication requested..."));
|
|
5653
|
-
try {
|
|
5654
|
-
await stopDaemon();
|
|
5655
|
-
} catch {
|
|
5656
|
-
}
|
|
5657
|
-
await clearCredentials();
|
|
5658
|
-
await clearMachineId();
|
|
5659
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5660
|
-
credentials = result.credentials;
|
|
5661
|
-
} else if (forceAuth) {
|
|
5662
|
-
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5663
|
-
const res = await doAuth();
|
|
5664
|
-
if (!res) {
|
|
5665
|
-
process.exit(1);
|
|
5666
|
-
}
|
|
5667
|
-
await writeCredentials(res);
|
|
5668
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5669
|
-
credentials = result.credentials;
|
|
5670
|
-
} else {
|
|
5671
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5672
|
-
credentials = result.credentials;
|
|
5639
|
+
console.log(`happy version: ${packageJson.version}`);
|
|
5673
5640
|
}
|
|
5641
|
+
const {
|
|
5642
|
+
credentials
|
|
5643
|
+
} = await authAndSetupMachineIfNeeded();
|
|
5674
5644
|
let settings = await readSettings();
|
|
5675
5645
|
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5676
5646
|
const shouldAutoStart = await new Promise((resolve) => {
|
|
@@ -5699,15 +5669,18 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5699
5669
|
}
|
|
5700
5670
|
}
|
|
5701
5671
|
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5702
|
-
logger.debug("
|
|
5703
|
-
if (!await
|
|
5672
|
+
logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5673
|
+
if (!await isDaemonRunningSameVersion()) {
|
|
5674
|
+
logger.debug("Starting Happy background service...");
|
|
5704
5675
|
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5705
5676
|
detached: true,
|
|
5706
5677
|
stdio: "ignore",
|
|
5707
5678
|
env: process.env
|
|
5708
5679
|
});
|
|
5709
5680
|
daemonProcess.unref();
|
|
5710
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5681
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5682
|
+
} else {
|
|
5683
|
+
logger.debug("Happy background service is running & matches our version");
|
|
5711
5684
|
}
|
|
5712
5685
|
}
|
|
5713
5686
|
try {
|