happy-coder 0.1.12 → 0.1.14
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/bin/happy +2 -3
- package/bin/happy.cmd +1 -1
- package/dist/index.cjs +440 -297
- package/dist/index.mjs +442 -299
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +7 -2
- package/dist/lib.d.mts +7 -2
- package/dist/lib.mjs +1 -1
- package/dist/types-Cg4664gs.cjs +879 -0
- package/dist/types-DD9P_5rj.mjs +868 -0
- package/package.json +3 -3
- package/scripts/claudeInteractiveLaunch.cjs +72 -13
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var types = require('./types-
|
|
4
|
+
var types = require('./types-Cg4664gs.cjs');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
6
|
var claudeCode = require('@anthropic-ai/claude-code');
|
|
7
7
|
var node_fs = require('node:fs');
|
|
@@ -21,7 +21,6 @@ var util = require('util');
|
|
|
21
21
|
var crypto = require('crypto');
|
|
22
22
|
var path = require('path');
|
|
23
23
|
var url = require('url');
|
|
24
|
-
var httpProxy = require('http-proxy');
|
|
25
24
|
var tweetnacl = require('tweetnacl');
|
|
26
25
|
var axios = require('axios');
|
|
27
26
|
var qrcode = require('qrcode-terminal');
|
|
@@ -261,18 +260,32 @@ async function claudeRemote(opts) {
|
|
|
261
260
|
});
|
|
262
261
|
}
|
|
263
262
|
printDivider();
|
|
263
|
+
let thinking = false;
|
|
264
|
+
const updateThinking = (newThinking) => {
|
|
265
|
+
if (thinking !== newThinking) {
|
|
266
|
+
thinking = newThinking;
|
|
267
|
+
types.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
|
|
268
|
+
if (opts.onThinkingChange) {
|
|
269
|
+
opts.onThinkingChange(thinking);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
264
273
|
try {
|
|
265
274
|
types.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
266
275
|
for await (const message of response) {
|
|
267
276
|
types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
268
277
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
269
278
|
if (message.type === "system" && message.subtype === "init") {
|
|
279
|
+
updateThinking(true);
|
|
270
280
|
types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
|
|
271
281
|
const projectDir = getProjectPath(opts.path);
|
|
272
282
|
const found = await awaitFileExist(node_path.join(projectDir, `${message.session_id}.jsonl`));
|
|
273
283
|
types.logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
|
|
274
284
|
opts.onSessionFound(message.session_id);
|
|
275
285
|
}
|
|
286
|
+
if (message.type === "result") {
|
|
287
|
+
updateThinking(false);
|
|
288
|
+
}
|
|
276
289
|
}
|
|
277
290
|
types.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
278
291
|
} catch (e) {
|
|
@@ -285,6 +298,7 @@ async function claudeRemote(opts) {
|
|
|
285
298
|
throw e;
|
|
286
299
|
}
|
|
287
300
|
} finally {
|
|
301
|
+
updateThinking(false);
|
|
288
302
|
if (opts.interruptController) {
|
|
289
303
|
opts.interruptController.unregister();
|
|
290
304
|
}
|
|
@@ -348,22 +362,70 @@ async function claudeLocal(opts) {
|
|
|
348
362
|
input: child.stdio[3],
|
|
349
363
|
crlfDelay: Infinity
|
|
350
364
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
365
|
+
const activeFetches = /* @__PURE__ */ new Map();
|
|
366
|
+
let thinking = false;
|
|
367
|
+
let stopThinkingTimeout = null;
|
|
368
|
+
const updateThinking = (newThinking) => {
|
|
369
|
+
if (thinking !== newThinking) {
|
|
370
|
+
thinking = newThinking;
|
|
371
|
+
types.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
|
|
372
|
+
if (opts.onThinkingChange) {
|
|
373
|
+
opts.onThinkingChange(thinking);
|
|
357
374
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
rl.on("line", (line) => {
|
|
378
|
+
try {
|
|
379
|
+
const message = JSON.parse(line);
|
|
380
|
+
switch (message.type) {
|
|
381
|
+
case "uuid":
|
|
382
|
+
detectedIdsRandomUUID.add(message.value);
|
|
383
|
+
if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
|
|
384
|
+
resolvedSessionId = message.value;
|
|
385
|
+
opts.onSessionFound(message.value);
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "fetch-start":
|
|
389
|
+
types.logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
|
|
390
|
+
activeFetches.set(message.id, {
|
|
391
|
+
hostname: message.hostname,
|
|
392
|
+
path: message.path,
|
|
393
|
+
startTime: message.timestamp
|
|
394
|
+
});
|
|
395
|
+
if (stopThinkingTimeout) {
|
|
396
|
+
clearTimeout(stopThinkingTimeout);
|
|
397
|
+
stopThinkingTimeout = null;
|
|
398
|
+
}
|
|
399
|
+
updateThinking(true);
|
|
400
|
+
break;
|
|
401
|
+
case "fetch-end":
|
|
402
|
+
types.logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
|
|
403
|
+
activeFetches.delete(message.id);
|
|
404
|
+
if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
|
|
405
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
406
|
+
if (activeFetches.size === 0) {
|
|
407
|
+
updateThinking(false);
|
|
408
|
+
}
|
|
409
|
+
stopThinkingTimeout = null;
|
|
410
|
+
}, 500);
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
default:
|
|
414
|
+
types.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
|
|
361
415
|
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
types.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
362
418
|
}
|
|
363
419
|
});
|
|
364
420
|
rl.on("error", (err) => {
|
|
365
421
|
console.error("Error reading from fd 3:", err);
|
|
366
422
|
});
|
|
423
|
+
child.on("exit", () => {
|
|
424
|
+
if (stopThinkingTimeout) {
|
|
425
|
+
clearTimeout(stopThinkingTimeout);
|
|
426
|
+
}
|
|
427
|
+
updateThinking(false);
|
|
428
|
+
});
|
|
367
429
|
}
|
|
368
430
|
child.on("error", (error) => {
|
|
369
431
|
});
|
|
@@ -819,6 +881,7 @@ async function loop(opts) {
|
|
|
819
881
|
path: opts.path,
|
|
820
882
|
sessionId,
|
|
821
883
|
onSessionFound,
|
|
884
|
+
onThinkingChange: opts.onThinkingChange,
|
|
822
885
|
abort: interactiveAbortController.signal,
|
|
823
886
|
claudeEnvVars: opts.claudeEnvVars,
|
|
824
887
|
claudeArgs: opts.claudeArgs
|
|
@@ -881,6 +944,7 @@ async function loop(opts) {
|
|
|
881
944
|
mcpServers: opts.mcpServers,
|
|
882
945
|
permissionPromptToolName: opts.permissionPromptToolName,
|
|
883
946
|
onSessionFound,
|
|
947
|
+
onThinkingChange: opts.onThinkingChange,
|
|
884
948
|
messages: currentMessageQueue,
|
|
885
949
|
onAssistantResult: opts.onAssistantResult,
|
|
886
950
|
interruptController: opts.interruptController,
|
|
@@ -1009,7 +1073,7 @@ class InterruptController {
|
|
|
1009
1073
|
}
|
|
1010
1074
|
}
|
|
1011
1075
|
|
|
1012
|
-
var version = "0.1.
|
|
1076
|
+
var version = "0.1.14";
|
|
1013
1077
|
var packageJson = {
|
|
1014
1078
|
version: version};
|
|
1015
1079
|
|
|
@@ -1294,148 +1358,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
|
|
|
1294
1358
|
});
|
|
1295
1359
|
}
|
|
1296
1360
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1311
|
-
if (options.onRequest) {
|
|
1312
|
-
options.onRequest(req, proxyReq);
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1316
|
-
if (options.onResponse) {
|
|
1317
|
-
options.onResponse(req, proxyRes);
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
const server = node_http.createServer((req, res) => {
|
|
1321
|
-
proxy.web(req, res);
|
|
1322
|
-
});
|
|
1323
|
-
const url = await new Promise((resolve, reject) => {
|
|
1324
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1325
|
-
const addr = server.address();
|
|
1326
|
-
if (addr && typeof addr === "object") {
|
|
1327
|
-
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1328
|
-
types.logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1329
|
-
resolve(proxyUrl);
|
|
1330
|
-
} else {
|
|
1331
|
-
reject(new Error("Failed to get server address"));
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
return url;
|
|
1361
|
+
const defaultSettings = {
|
|
1362
|
+
onboardingCompleted: false
|
|
1363
|
+
};
|
|
1364
|
+
async function readSettings() {
|
|
1365
|
+
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1366
|
+
return { ...defaultSettings };
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1370
|
+
return JSON.parse(content);
|
|
1371
|
+
} catch {
|
|
1372
|
+
return { ...defaultSettings };
|
|
1373
|
+
}
|
|
1336
1374
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
activeRequests.set(requestId, timeout);
|
|
1371
|
-
if (!isThinking) {
|
|
1372
|
-
isThinking = true;
|
|
1373
|
-
onThinking(true);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
},
|
|
1377
|
-
onResponse: (req, proxyRes) => {
|
|
1378
|
-
proxyRes.headers["connection"] = "close";
|
|
1379
|
-
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1380
|
-
const requestId = req._requestId;
|
|
1381
|
-
const timeout = activeRequests.get(requestId);
|
|
1382
|
-
if (timeout) {
|
|
1383
|
-
clearTimeout(timeout);
|
|
1384
|
-
}
|
|
1385
|
-
let cleaned = false;
|
|
1386
|
-
const cleanupRequest = () => {
|
|
1387
|
-
if (!cleaned) {
|
|
1388
|
-
cleaned = true;
|
|
1389
|
-
activeRequests.delete(requestId);
|
|
1390
|
-
checkAndStopThinking();
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
proxyRes.on("end", () => {
|
|
1394
|
-
cleanupRequest();
|
|
1395
|
-
});
|
|
1396
|
-
proxyRes.on("error", (err) => {
|
|
1397
|
-
cleanupRequest();
|
|
1398
|
-
});
|
|
1399
|
-
proxyRes.on("aborted", () => {
|
|
1400
|
-
cleanupRequest();
|
|
1401
|
-
});
|
|
1402
|
-
proxyRes.on("close", () => {
|
|
1403
|
-
cleanupRequest();
|
|
1404
|
-
});
|
|
1405
|
-
req.on("close", () => {
|
|
1406
|
-
cleanupRequest();
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
});
|
|
1411
|
-
const reset = () => {
|
|
1412
|
-
for (const [requestId, timeout] of activeRequests) {
|
|
1413
|
-
clearTimeout(timeout);
|
|
1414
|
-
}
|
|
1415
|
-
activeRequests.clear();
|
|
1416
|
-
if (stopThinkingTimeout) {
|
|
1417
|
-
clearTimeout(stopThinkingTimeout);
|
|
1418
|
-
stopThinkingTimeout = null;
|
|
1419
|
-
}
|
|
1420
|
-
if (isThinking) {
|
|
1421
|
-
isThinking = false;
|
|
1422
|
-
onThinking(false);
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1425
|
-
return {
|
|
1426
|
-
proxyUrl,
|
|
1427
|
-
reset
|
|
1428
|
-
};
|
|
1375
|
+
async function writeSettings(settings) {
|
|
1376
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1377
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1378
|
+
}
|
|
1379
|
+
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1380
|
+
}
|
|
1381
|
+
const credentialsSchema = z__namespace.object({
|
|
1382
|
+
secret: z__namespace.string().base64(),
|
|
1383
|
+
token: z__namespace.string()
|
|
1384
|
+
});
|
|
1385
|
+
async function readCredentials() {
|
|
1386
|
+
if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1391
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1392
|
+
return {
|
|
1393
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1394
|
+
token: credentials.token
|
|
1395
|
+
};
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async function writeCredentials(credentials) {
|
|
1401
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1402
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1403
|
+
}
|
|
1404
|
+
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1405
|
+
secret: types.encodeBase64(credentials.secret),
|
|
1406
|
+
token: credentials.token
|
|
1407
|
+
}, null, 2));
|
|
1429
1408
|
}
|
|
1430
1409
|
|
|
1431
1410
|
async function start(credentials, options = {}) {
|
|
1432
1411
|
const workingDirectory = process.cwd();
|
|
1433
1412
|
const sessionTag = node_crypto.randomUUID();
|
|
1413
|
+
if (options.daemonSpawn && options.startingMode === "local") {
|
|
1414
|
+
types.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
1415
|
+
options.startingMode = "remote";
|
|
1416
|
+
}
|
|
1434
1417
|
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
1435
1418
|
let state = {};
|
|
1436
|
-
|
|
1419
|
+
const settings = await readSettings() || { };
|
|
1420
|
+
let metadata = {
|
|
1421
|
+
path: workingDirectory,
|
|
1422
|
+
host: os.hostname(),
|
|
1423
|
+
version: packageJson.version,
|
|
1424
|
+
os: os.platform(),
|
|
1425
|
+
machineId: settings.machineId
|
|
1426
|
+
};
|
|
1437
1427
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1438
1428
|
types.logger.debug(`Session created: ${response.id}`);
|
|
1429
|
+
if (options.daemonSpawn) {
|
|
1430
|
+
console.log(`daemon:sessionIdCreated:${response.id}`);
|
|
1431
|
+
}
|
|
1439
1432
|
const session = api.session(response);
|
|
1440
1433
|
const pushClient = api.push();
|
|
1441
1434
|
let thinking = false;
|
|
@@ -1443,11 +1436,6 @@ async function start(credentials, options = {}) {
|
|
|
1443
1436
|
let pingInterval = setInterval(() => {
|
|
1444
1437
|
session.keepAlive(thinking, mode);
|
|
1445
1438
|
}, 2e3);
|
|
1446
|
-
const activityTracker = await startClaudeActivityTracker((newThinking) => {
|
|
1447
|
-
thinking = newThinking;
|
|
1448
|
-
session.keepAlive(thinking, mode);
|
|
1449
|
-
});
|
|
1450
|
-
process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
|
|
1451
1439
|
const logPath = await types.logger.logFilePathPromise;
|
|
1452
1440
|
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1453
1441
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -1579,7 +1567,6 @@ async function start(credentials, options = {}) {
|
|
|
1579
1567
|
},
|
|
1580
1568
|
onProcessStart: (processMode) => {
|
|
1581
1569
|
types.logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
|
|
1582
|
-
activityTracker.reset();
|
|
1583
1570
|
types.logger.debug("Starting process - clearing any stale permission requests");
|
|
1584
1571
|
for (const [id, resolve] of requests) {
|
|
1585
1572
|
types.logger.debug(`Rejecting stale permission request: ${id}`);
|
|
@@ -1589,13 +1576,14 @@ async function start(credentials, options = {}) {
|
|
|
1589
1576
|
},
|
|
1590
1577
|
onProcessStop: (processMode) => {
|
|
1591
1578
|
types.logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
|
|
1592
|
-
activityTracker.reset();
|
|
1593
1579
|
types.logger.debug("Stopping process - clearing any stale permission requests");
|
|
1594
1580
|
for (const [id, resolve] of requests) {
|
|
1595
1581
|
types.logger.debug(`Rejecting stale permission request: ${id}`);
|
|
1596
1582
|
resolve({ approved: false, reason: "Process restarted" });
|
|
1597
1583
|
}
|
|
1598
1584
|
requests.clear();
|
|
1585
|
+
thinking = false;
|
|
1586
|
+
session.keepAlive(thinking, mode);
|
|
1599
1587
|
},
|
|
1600
1588
|
mcpServers: {
|
|
1601
1589
|
"permission": {
|
|
@@ -1608,61 +1596,16 @@ async function start(credentials, options = {}) {
|
|
|
1608
1596
|
onAssistantResult,
|
|
1609
1597
|
interruptController,
|
|
1610
1598
|
claudeEnvVars: options.claudeEnvVars,
|
|
1611
|
-
claudeArgs: options.claudeArgs
|
|
1599
|
+
claudeArgs: options.claudeArgs,
|
|
1600
|
+
onThinkingChange: (newThinking) => {
|
|
1601
|
+
thinking = newThinking;
|
|
1602
|
+
session.keepAlive(thinking, mode);
|
|
1603
|
+
}
|
|
1612
1604
|
});
|
|
1613
1605
|
clearInterval(pingInterval);
|
|
1614
1606
|
process.exit(0);
|
|
1615
1607
|
}
|
|
1616
1608
|
|
|
1617
|
-
const defaultSettings = {
|
|
1618
|
-
onboardingCompleted: false
|
|
1619
|
-
};
|
|
1620
|
-
async function readSettings() {
|
|
1621
|
-
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1622
|
-
return { ...defaultSettings };
|
|
1623
|
-
}
|
|
1624
|
-
try {
|
|
1625
|
-
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1626
|
-
return JSON.parse(content);
|
|
1627
|
-
} catch {
|
|
1628
|
-
return { ...defaultSettings };
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
async function writeSettings(settings) {
|
|
1632
|
-
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1633
|
-
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1634
|
-
}
|
|
1635
|
-
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1636
|
-
}
|
|
1637
|
-
const credentialsSchema = z__namespace.object({
|
|
1638
|
-
secret: z__namespace.string().base64(),
|
|
1639
|
-
token: z__namespace.string()
|
|
1640
|
-
});
|
|
1641
|
-
async function readCredentials() {
|
|
1642
|
-
if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
|
|
1643
|
-
return null;
|
|
1644
|
-
}
|
|
1645
|
-
try {
|
|
1646
|
-
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1647
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1648
|
-
return {
|
|
1649
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
1650
|
-
token: credentials.token
|
|
1651
|
-
};
|
|
1652
|
-
} catch {
|
|
1653
|
-
return null;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
async function writeCredentials(credentials) {
|
|
1657
|
-
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1658
|
-
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1659
|
-
}
|
|
1660
|
-
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1661
|
-
secret: types.encodeBase64(credentials.secret),
|
|
1662
|
-
token: credentials.token
|
|
1663
|
-
}, null, 2));
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
1609
|
function displayQRCode(url) {
|
|
1667
1610
|
console.log("=".repeat(80));
|
|
1668
1611
|
console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
|
|
@@ -1690,10 +1633,8 @@ async function doAuth() {
|
|
|
1690
1633
|
console.log("Please, authenticate using mobile app");
|
|
1691
1634
|
const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
|
|
1692
1635
|
displayQRCode(authUrl);
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
console.log(authUrl);
|
|
1696
|
-
}
|
|
1636
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
1637
|
+
console.log(authUrl);
|
|
1697
1638
|
let credentials = null;
|
|
1698
1639
|
while (true) {
|
|
1699
1640
|
try {
|
|
@@ -1741,18 +1682,20 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1741
1682
|
keepAliveInterval = null;
|
|
1742
1683
|
token;
|
|
1743
1684
|
secret;
|
|
1685
|
+
spawnedProcesses = /* @__PURE__ */ new Set();
|
|
1744
1686
|
constructor(token, secret, machineIdentity) {
|
|
1745
1687
|
super();
|
|
1746
1688
|
this.token = token;
|
|
1747
1689
|
this.secret = secret;
|
|
1748
1690
|
this.machineIdentity = machineIdentity;
|
|
1691
|
+
types.logger.daemonDebug(`Connecting to server: ${types.configuration.serverUrl}`);
|
|
1749
1692
|
const socket = socket_ioClient.io(types.configuration.serverUrl, {
|
|
1750
1693
|
auth: {
|
|
1751
1694
|
token: this.token,
|
|
1752
1695
|
clientType: "machine-scoped",
|
|
1753
1696
|
machineId: this.machineIdentity.machineId
|
|
1754
1697
|
},
|
|
1755
|
-
path: "/v1/
|
|
1698
|
+
path: "/v1/updates",
|
|
1756
1699
|
reconnection: true,
|
|
1757
1700
|
reconnectionAttempts: Infinity,
|
|
1758
1701
|
reconnectionDelay: 1e3,
|
|
@@ -1762,68 +1705,146 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1762
1705
|
autoConnect: false
|
|
1763
1706
|
});
|
|
1764
1707
|
socket.on("connect", () => {
|
|
1765
|
-
types.logger.
|
|
1708
|
+
types.logger.daemonDebug("Socket connected");
|
|
1709
|
+
types.logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
1710
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1711
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
1712
|
+
types.logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
|
|
1766
1713
|
this.emit("connected");
|
|
1767
|
-
socket.emit("machine-connect", {
|
|
1768
|
-
token: this.token,
|
|
1769
|
-
machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
|
|
1770
|
-
});
|
|
1771
1714
|
this.startKeepAlive();
|
|
1772
1715
|
});
|
|
1773
|
-
socket.on("
|
|
1774
|
-
types.logger.
|
|
1775
|
-
this.
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1716
|
+
socket.on("rpc-request", async (data, callback) => {
|
|
1717
|
+
types.logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
|
|
1718
|
+
const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1719
|
+
if (data.method === expectedMethod) {
|
|
1720
|
+
types.logger.daemonDebug("Processing spawn-happy-session RPC");
|
|
1721
|
+
try {
|
|
1722
|
+
const { directory } = data.params || {};
|
|
1723
|
+
if (!directory) {
|
|
1724
|
+
throw new Error("Directory is required");
|
|
1725
|
+
}
|
|
1726
|
+
const args = [
|
|
1727
|
+
"--daemon-spawn",
|
|
1728
|
+
"--happy-starting-mode",
|
|
1729
|
+
"remote"
|
|
1730
|
+
// ALWAYS force remote mode for daemon spawns
|
|
1731
|
+
];
|
|
1732
|
+
if (types.configuration.installationLocation === "local") {
|
|
1733
|
+
args.push("--local");
|
|
1734
|
+
}
|
|
1735
|
+
if (types.configuration.serverUrl !== "https://handy-api.korshakov.org") {
|
|
1736
|
+
args.push("--happy-server-url", types.configuration.serverUrl);
|
|
1737
|
+
}
|
|
1738
|
+
types.logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
1739
|
+
const happyPath = process.argv[1];
|
|
1740
|
+
const isTypeScript = happyPath.endsWith(".ts");
|
|
1741
|
+
const happyProcess = isTypeScript ? child_process.spawn("npx", ["tsx", happyPath, ...args], {
|
|
1742
|
+
cwd: directory,
|
|
1743
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1744
|
+
detached: true,
|
|
1745
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1746
|
+
// We need stdout
|
|
1747
|
+
}) : child_process.spawn(process.argv[0], [happyPath, ...args], {
|
|
1748
|
+
cwd: directory,
|
|
1749
|
+
env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
|
|
1802
1750
|
detached: true,
|
|
1803
|
-
stdio: "ignore",
|
|
1804
|
-
|
|
1751
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1752
|
+
// We need stdout
|
|
1753
|
+
});
|
|
1754
|
+
this.spawnedProcesses.add(happyProcess);
|
|
1755
|
+
let sessionId = null;
|
|
1756
|
+
let output = "";
|
|
1757
|
+
let timeoutId = null;
|
|
1758
|
+
const cleanup = () => {
|
|
1759
|
+
happyProcess.stdout.removeAllListeners("data");
|
|
1760
|
+
happyProcess.stderr.removeAllListeners("data");
|
|
1761
|
+
happyProcess.removeAllListeners("error");
|
|
1762
|
+
happyProcess.removeAllListeners("exit");
|
|
1763
|
+
if (timeoutId) {
|
|
1764
|
+
clearTimeout(timeoutId);
|
|
1765
|
+
timeoutId = null;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
happyProcess.stdout.on("data", (data2) => {
|
|
1769
|
+
output += data2.toString();
|
|
1770
|
+
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
1771
|
+
if (match && !sessionId) {
|
|
1772
|
+
sessionId = match[1];
|
|
1773
|
+
types.logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
|
|
1774
|
+
callback({ sessionId });
|
|
1775
|
+
cleanup();
|
|
1776
|
+
happyProcess.unref();
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
happyProcess.stderr.on("data", (data2) => {
|
|
1780
|
+
types.logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
|
|
1781
|
+
});
|
|
1782
|
+
happyProcess.on("error", (error) => {
|
|
1783
|
+
types.logger.daemonDebug("Error spawning session:", error);
|
|
1784
|
+
if (!sessionId) {
|
|
1785
|
+
callback({ error: `Failed to spawn: ${error.message}` });
|
|
1786
|
+
cleanup();
|
|
1787
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1788
|
+
}
|
|
1805
1789
|
});
|
|
1806
|
-
|
|
1790
|
+
happyProcess.on("exit", (code, signal) => {
|
|
1791
|
+
types.logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
|
|
1792
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1793
|
+
if (!sessionId) {
|
|
1794
|
+
callback({ error: `Process exited before session ID received` });
|
|
1795
|
+
cleanup();
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
timeoutId = setTimeout(() => {
|
|
1799
|
+
if (!sessionId) {
|
|
1800
|
+
types.logger.daemonDebug("Timeout waiting for session ID");
|
|
1801
|
+
callback({ error: "Timeout waiting for session" });
|
|
1802
|
+
cleanup();
|
|
1803
|
+
happyProcess.kill();
|
|
1804
|
+
this.spawnedProcesses.delete(happyProcess);
|
|
1805
|
+
}
|
|
1806
|
+
}, 1e4);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
types.logger.daemonDebug("Error spawning session:", error);
|
|
1809
|
+
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
1807
1810
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1811
|
+
} else {
|
|
1812
|
+
types.logger.daemonDebug(`Unknown RPC method: ${data.method}`);
|
|
1813
|
+
callback({ error: `Unknown method: ${data.method}` });
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
socket.on("disconnect", (reason) => {
|
|
1817
|
+
types.logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
|
|
1818
|
+
this.emit("disconnected");
|
|
1819
|
+
this.stopKeepAlive();
|
|
1820
|
+
});
|
|
1821
|
+
socket.on("reconnect", () => {
|
|
1822
|
+
types.logger.daemonDebug("Reconnected to server");
|
|
1823
|
+
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
1824
|
+
socket.emit("rpc-register", { method: rpcMethod });
|
|
1825
|
+
types.logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
|
|
1826
|
+
});
|
|
1827
|
+
socket.on("rpc-registered", (data) => {
|
|
1828
|
+
types.logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
|
|
1829
|
+
});
|
|
1830
|
+
socket.on("rpc-unregistered", (data) => {
|
|
1831
|
+
types.logger.daemonDebug(`RPC unregistered: ${data.method}`);
|
|
1832
|
+
});
|
|
1833
|
+
socket.on("rpc-error", (data) => {
|
|
1834
|
+
types.logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
|
|
1835
|
+
});
|
|
1836
|
+
socket.onAny((event, ...args) => {
|
|
1837
|
+
if (!event.startsWith("machine-alive")) {
|
|
1838
|
+
types.logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
1825
1839
|
}
|
|
1826
1840
|
});
|
|
1841
|
+
socket.on("connect_error", (error) => {
|
|
1842
|
+
types.logger.daemonDebug(`Connection error: ${error.message}`);
|
|
1843
|
+
types.logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
|
|
1844
|
+
});
|
|
1845
|
+
socket.on("error", (error) => {
|
|
1846
|
+
types.logger.daemonDebug(`Socket error: ${error}`);
|
|
1847
|
+
});
|
|
1827
1848
|
socket.on("daemon-command", (data) => {
|
|
1828
1849
|
switch (data.command) {
|
|
1829
1850
|
case "shutdown":
|
|
@@ -1839,6 +1860,7 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1839
1860
|
startKeepAlive() {
|
|
1840
1861
|
this.stopKeepAlive();
|
|
1841
1862
|
this.keepAliveInterval = setInterval(() => {
|
|
1863
|
+
types.logger.daemonDebug("Sending keep-alive ping");
|
|
1842
1864
|
this.socket.volatile.emit("machine-alive", {
|
|
1843
1865
|
time: Date.now()
|
|
1844
1866
|
});
|
|
@@ -1854,22 +1876,42 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1854
1876
|
this.socket.connect();
|
|
1855
1877
|
}
|
|
1856
1878
|
shutdown() {
|
|
1879
|
+
types.logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
1880
|
+
for (const process2 of this.spawnedProcesses) {
|
|
1881
|
+
try {
|
|
1882
|
+
types.logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
|
|
1883
|
+
process2.kill("SIGTERM");
|
|
1884
|
+
setTimeout(() => {
|
|
1885
|
+
try {
|
|
1886
|
+
process2.kill("SIGKILL");
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
}
|
|
1889
|
+
}, 1e3);
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
types.logger.daemonDebug(`Error killing process: ${error}`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
this.spawnedProcesses.clear();
|
|
1857
1895
|
this.stopKeepAlive();
|
|
1858
1896
|
this.socket.close();
|
|
1859
1897
|
this.emit("shutdown");
|
|
1860
1898
|
}
|
|
1861
1899
|
}
|
|
1862
1900
|
|
|
1901
|
+
let pidFileFd = null;
|
|
1863
1902
|
async function startDaemon() {
|
|
1864
|
-
|
|
1903
|
+
if (process.platform !== "darwin") {
|
|
1904
|
+
console.error("ERROR: Daemon is only supported on macOS");
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
types.logger.daemonDebug("Starting daemon process...");
|
|
1908
|
+
types.logger.daemonDebug(`Server URL: ${types.configuration.serverUrl}`);
|
|
1865
1909
|
if (await isDaemonRunning()) {
|
|
1866
|
-
|
|
1910
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
1867
1911
|
process.exit(0);
|
|
1868
1912
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
console.log("[DAEMON] PID file written successfully");
|
|
1872
|
-
types.logger.info("Happy CLI daemon started successfully");
|
|
1913
|
+
pidFileFd = writePidFile();
|
|
1914
|
+
types.logger.daemonDebug("PID file written");
|
|
1873
1915
|
process.on("SIGINT", () => {
|
|
1874
1916
|
stopDaemon().catch(console.error);
|
|
1875
1917
|
});
|
|
@@ -1894,7 +1936,7 @@ async function startDaemon() {
|
|
|
1894
1936
|
};
|
|
1895
1937
|
let credentials = await readCredentials();
|
|
1896
1938
|
if (!credentials) {
|
|
1897
|
-
types.logger.
|
|
1939
|
+
types.logger.daemonDebug("No credentials found, running auth");
|
|
1898
1940
|
await doAuth();
|
|
1899
1941
|
credentials = await readCredentials();
|
|
1900
1942
|
if (!credentials) {
|
|
@@ -1902,64 +1944,64 @@ async function startDaemon() {
|
|
|
1902
1944
|
}
|
|
1903
1945
|
}
|
|
1904
1946
|
const { token, secret } = credentials;
|
|
1905
|
-
const daemon = new ApiDaemonSession(
|
|
1947
|
+
const daemon = new ApiDaemonSession(
|
|
1948
|
+
token,
|
|
1949
|
+
secret,
|
|
1950
|
+
machineIdentity
|
|
1951
|
+
);
|
|
1906
1952
|
daemon.on("connected", () => {
|
|
1907
|
-
types.logger.
|
|
1953
|
+
types.logger.daemonDebug("Connected to server event received");
|
|
1908
1954
|
});
|
|
1909
1955
|
daemon.on("disconnected", () => {
|
|
1910
|
-
types.logger.
|
|
1956
|
+
types.logger.daemonDebug("Disconnected from server event received");
|
|
1911
1957
|
});
|
|
1912
1958
|
daemon.on("shutdown", () => {
|
|
1913
|
-
types.logger.
|
|
1959
|
+
types.logger.daemonDebug("Shutdown requested");
|
|
1914
1960
|
stopDaemon();
|
|
1915
1961
|
process.exit(0);
|
|
1916
1962
|
});
|
|
1917
1963
|
daemon.connect();
|
|
1918
|
-
|
|
1919
|
-
}, 1e3);
|
|
1964
|
+
types.logger.daemonDebug("Daemon started successfully");
|
|
1920
1965
|
} catch (error) {
|
|
1921
|
-
types.logger.
|
|
1966
|
+
types.logger.daemonDebug("Failed to start daemon", error);
|
|
1922
1967
|
stopDaemon();
|
|
1923
1968
|
process.exit(1);
|
|
1924
1969
|
}
|
|
1925
|
-
process.on("SIGINT", () => process.exit(0));
|
|
1926
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
1927
|
-
process.on("exit", () => process.exit(0));
|
|
1928
1970
|
while (true) {
|
|
1929
1971
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1930
1972
|
}
|
|
1931
1973
|
}
|
|
1932
1974
|
async function isDaemonRunning() {
|
|
1933
1975
|
try {
|
|
1934
|
-
|
|
1976
|
+
types.logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
|
|
1935
1977
|
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1936
|
-
|
|
1978
|
+
types.logger.daemonDebug("[isDaemonRunning] PID file exists");
|
|
1937
1979
|
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1938
|
-
|
|
1980
|
+
types.logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
|
|
1939
1981
|
try {
|
|
1940
1982
|
process.kill(pid, 0);
|
|
1941
|
-
|
|
1983
|
+
types.logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1942
1984
|
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1943
|
-
|
|
1985
|
+
types.logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1944
1986
|
if (isHappyDaemon) {
|
|
1945
1987
|
return true;
|
|
1946
1988
|
} else {
|
|
1947
|
-
|
|
1948
|
-
types.logger.debug(`
|
|
1989
|
+
types.logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
1990
|
+
types.logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
|
|
1949
1991
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1950
1992
|
}
|
|
1951
1993
|
} catch (error) {
|
|
1952
|
-
|
|
1953
|
-
types.logger.debug("
|
|
1994
|
+
types.logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
1995
|
+
types.logger.debug("Process not running, cleaning up stale PID file");
|
|
1954
1996
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1955
1997
|
}
|
|
1956
1998
|
} else {
|
|
1957
|
-
|
|
1999
|
+
types.logger.daemonDebug("[isDaemonRunning] No PID file found");
|
|
1958
2000
|
}
|
|
1959
2001
|
return false;
|
|
1960
2002
|
} catch (error) {
|
|
1961
|
-
|
|
1962
|
-
types.logger.debug("
|
|
2003
|
+
types.logger.daemonDebug("[isDaemonRunning] Error:", error);
|
|
2004
|
+
types.logger.debug("Error checking daemon status", error);
|
|
1963
2005
|
return false;
|
|
1964
2006
|
}
|
|
1965
2007
|
}
|
|
@@ -1969,20 +2011,46 @@ function writePidFile() {
|
|
|
1969
2011
|
fs.mkdirSync(happyDir, { recursive: true });
|
|
1970
2012
|
}
|
|
1971
2013
|
try {
|
|
1972
|
-
fs.
|
|
2014
|
+
const fd = fs.openSync(types.configuration.daemonPidFile, "wx");
|
|
2015
|
+
fs.writeSync(fd, process.pid.toString());
|
|
2016
|
+
return fd;
|
|
1973
2017
|
} catch (error) {
|
|
1974
2018
|
if (error.code === "EEXIST") {
|
|
1975
|
-
|
|
1976
|
-
|
|
2019
|
+
try {
|
|
2020
|
+
const fd = fs.openSync(types.configuration.daemonPidFile, "r+");
|
|
2021
|
+
const existingPid = fs.readFileSync(types.configuration.daemonPidFile, "utf-8").trim();
|
|
2022
|
+
fs.closeSync(fd);
|
|
2023
|
+
try {
|
|
2024
|
+
process.kill(parseInt(existingPid), 0);
|
|
2025
|
+
types.logger.daemonDebug("PID file exists and process is running");
|
|
2026
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
2027
|
+
process.exit(0);
|
|
2028
|
+
} catch {
|
|
2029
|
+
types.logger.daemonDebug("PID file exists but process is dead, cleaning up");
|
|
2030
|
+
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
2031
|
+
return writePidFile();
|
|
2032
|
+
}
|
|
2033
|
+
} catch (lockError) {
|
|
2034
|
+
types.logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
|
|
2035
|
+
types.logger.daemonDebug("Happy daemon is already running");
|
|
2036
|
+
process.exit(0);
|
|
2037
|
+
}
|
|
1977
2038
|
}
|
|
1978
2039
|
throw error;
|
|
1979
2040
|
}
|
|
1980
2041
|
}
|
|
1981
2042
|
async function stopDaemon() {
|
|
1982
2043
|
try {
|
|
2044
|
+
if (pidFileFd !== null) {
|
|
2045
|
+
try {
|
|
2046
|
+
fs.closeSync(pidFileFd);
|
|
2047
|
+
} catch {
|
|
2048
|
+
}
|
|
2049
|
+
pidFileFd = null;
|
|
2050
|
+
}
|
|
1983
2051
|
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1984
2052
|
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1985
|
-
types.logger.debug(`
|
|
2053
|
+
types.logger.debug(`Stopping daemon with PID ${pid}`);
|
|
1986
2054
|
try {
|
|
1987
2055
|
process.kill(pid, "SIGTERM");
|
|
1988
2056
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -1992,12 +2060,12 @@ async function stopDaemon() {
|
|
|
1992
2060
|
} catch {
|
|
1993
2061
|
}
|
|
1994
2062
|
} catch (error) {
|
|
1995
|
-
types.logger.debug("
|
|
2063
|
+
types.logger.debug("Process already dead or inaccessible", error);
|
|
1996
2064
|
}
|
|
1997
2065
|
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1998
2066
|
}
|
|
1999
2067
|
} catch (error) {
|
|
2000
|
-
types.logger.debug("
|
|
2068
|
+
types.logger.debug("Error stopping daemon", error);
|
|
2001
2069
|
}
|
|
2002
2070
|
}
|
|
2003
2071
|
async function isProcessHappyDaemon(pid) {
|
|
@@ -2145,7 +2213,12 @@ async function uninstall() {
|
|
|
2145
2213
|
(async () => {
|
|
2146
2214
|
const args = process.argv.slice(2);
|
|
2147
2215
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
2148
|
-
|
|
2216
|
+
let serverUrl;
|
|
2217
|
+
const serverUrlIndex = args.indexOf("--happy-server-url");
|
|
2218
|
+
if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
|
|
2219
|
+
serverUrl = args[serverUrlIndex + 1];
|
|
2220
|
+
}
|
|
2221
|
+
types.initializeConfiguration(installationLocation, serverUrl);
|
|
2149
2222
|
types.initLoggerWithGlobalConfiguration();
|
|
2150
2223
|
types.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
2151
2224
|
const subcommand = args[0];
|
|
@@ -2227,6 +2300,10 @@ Currently only supported on macOS.
|
|
|
2227
2300
|
} else if (arg === "--claude-arg") {
|
|
2228
2301
|
const claudeArg = args[++i];
|
|
2229
2302
|
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
2303
|
+
} else if (arg === "--daemon-spawn") {
|
|
2304
|
+
options.daemonSpawn = true;
|
|
2305
|
+
} else if (arg === "--happy-server-url") {
|
|
2306
|
+
i++;
|
|
2230
2307
|
} else {
|
|
2231
2308
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
2232
2309
|
process.exit(1);
|
|
@@ -2262,6 +2339,8 @@ ${chalk.bold("Options:")}
|
|
|
2262
2339
|
You will require re-login each time you run this in a new directory.
|
|
2263
2340
|
--happy-starting-mode <interactive|remote>
|
|
2264
2341
|
Set the starting mode for new sessions (default: remote)
|
|
2342
|
+
--happy-server-url <url>
|
|
2343
|
+
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
2265
2344
|
|
|
2266
2345
|
${chalk.bold("Examples:")}
|
|
2267
2346
|
happy Start a session with default settings
|
|
@@ -2288,7 +2367,71 @@ ${chalk.bold("Examples:")}
|
|
|
2288
2367
|
}
|
|
2289
2368
|
credentials = res;
|
|
2290
2369
|
}
|
|
2291
|
-
await readSettings() || { };
|
|
2370
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
2371
|
+
process.env.EXPERIMENTAL_FEATURES !== void 0;
|
|
2372
|
+
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
2373
|
+
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
2374
|
+
const rl = node_readline.createInterface({
|
|
2375
|
+
input: process.stdin,
|
|
2376
|
+
output: process.stdout
|
|
2377
|
+
});
|
|
2378
|
+
console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
|
|
2379
|
+
console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
|
|
2380
|
+
console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
|
|
2381
|
+
console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
|
|
2382
|
+
const answer = await new Promise((resolve) => {
|
|
2383
|
+
rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
|
|
2384
|
+
});
|
|
2385
|
+
rl.close();
|
|
2386
|
+
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
2387
|
+
settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
|
|
2388
|
+
if (shouldAutoStart) {
|
|
2389
|
+
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
2390
|
+
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
2391
|
+
} else {
|
|
2392
|
+
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
2393
|
+
}
|
|
2394
|
+
await writeSettings(settings);
|
|
2395
|
+
}
|
|
2396
|
+
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
2397
|
+
console.log("Starting Happy background service...");
|
|
2398
|
+
if (!await isDaemonRunning()) {
|
|
2399
|
+
const happyPath = process.argv[1];
|
|
2400
|
+
const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
|
|
2401
|
+
const daemonArgs = ["daemon", "start"];
|
|
2402
|
+
if (serverUrl) {
|
|
2403
|
+
daemonArgs.push("--happy-server-url", serverUrl);
|
|
2404
|
+
}
|
|
2405
|
+
if (installationLocation === "local") {
|
|
2406
|
+
daemonArgs.push("--local");
|
|
2407
|
+
}
|
|
2408
|
+
const daemonProcess = isBuiltBinary ? child_process.spawn(happyPath, daemonArgs, {
|
|
2409
|
+
detached: true,
|
|
2410
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2411
|
+
// Show stdout/stderr for debugging
|
|
2412
|
+
env: {
|
|
2413
|
+
...process.env,
|
|
2414
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2415
|
+
// Pass through server URL
|
|
2416
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2417
|
+
// Pass through local flag
|
|
2418
|
+
}
|
|
2419
|
+
}) : child_process.spawn("npx", ["tsx", happyPath, ...daemonArgs], {
|
|
2420
|
+
detached: true,
|
|
2421
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2422
|
+
// Show stdout/stderr for debugging
|
|
2423
|
+
env: {
|
|
2424
|
+
...process.env,
|
|
2425
|
+
HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
|
|
2426
|
+
// Pass through server URL
|
|
2427
|
+
HANDY_LOCAL: process.env.HANDY_LOCAL
|
|
2428
|
+
// Pass through local flag
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
daemonProcess.unref();
|
|
2432
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2292
2435
|
try {
|
|
2293
2436
|
await start(credentials, options);
|
|
2294
2437
|
} catch (error) {
|