lunel-cli 0.1.118 → 0.1.119
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.js +226 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -90,10 +90,13 @@ let aiManagerInitPromise = null;
|
|
|
90
90
|
// Proxy tunnel management
|
|
91
91
|
let currentSessionCode = null;
|
|
92
92
|
let currentSessionPassword = null;
|
|
93
|
+
let currentManagerSessionId = null;
|
|
93
94
|
let currentPrimaryGateway = DEFAULT_PROXY_URL;
|
|
94
95
|
let activeGatewayUrl = DEFAULT_PROXY_URL;
|
|
96
|
+
let appPeerConnected = false;
|
|
95
97
|
let shuttingDown = false;
|
|
96
98
|
let activeV2Transport = null;
|
|
99
|
+
const runningAiSessions = new Set();
|
|
97
100
|
const trackedEditorFiles = new Map();
|
|
98
101
|
const trackedEditorDirectories = new Map();
|
|
99
102
|
const pendingTrackedFileChecks = new Set();
|
|
@@ -302,7 +305,21 @@ async function readCliConfig() {
|
|
|
302
305
|
rootDir: entry.rootDir,
|
|
303
306
|
sessionCode: typeof entry.sessionCode === "string" ? entry.sessionCode : null,
|
|
304
307
|
sessionPassword: entry.sessionPassword,
|
|
308
|
+
managerSessionId: typeof entry.managerSessionId === "string" ? entry.managerSessionId : null,
|
|
305
309
|
savedAt: entry.savedAt,
|
|
310
|
+
pushDevices: Array.isArray(entry.pushDevices)
|
|
311
|
+
? entry.pushDevices.filter((device) => (!!device
|
|
312
|
+
&& typeof device.phoneId === "string"
|
|
313
|
+
&& typeof device.notificationsEnabled === "boolean"
|
|
314
|
+
&& typeof device.updatedAt === "number"
|
|
315
|
+
&& (typeof device.expoPushToken === "string" || device.expoPushToken === null))).map((device) => ({
|
|
316
|
+
phoneId: device.phoneId,
|
|
317
|
+
expoPushToken: device.expoPushToken,
|
|
318
|
+
notificationsEnabled: device.notificationsEnabled,
|
|
319
|
+
platform: typeof device.platform === "string" ? device.platform : null,
|
|
320
|
+
updatedAt: device.updatedAt,
|
|
321
|
+
}))
|
|
322
|
+
: [],
|
|
306
323
|
}))
|
|
307
324
|
: [],
|
|
308
325
|
};
|
|
@@ -317,7 +334,9 @@ async function readCliConfig() {
|
|
|
317
334
|
}
|
|
318
335
|
async function writeCliConfig(config) {
|
|
319
336
|
await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
|
|
320
|
-
|
|
337
|
+
const tmpPath = `${CLI_CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
338
|
+
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
339
|
+
await fs.rename(tmpPath, CLI_CONFIG_PATH);
|
|
321
340
|
cliConfigPromise = Promise.resolve(config);
|
|
322
341
|
}
|
|
323
342
|
let cliConfigPromise = null;
|
|
@@ -331,14 +350,17 @@ function getSavedSessionForRoot(config, rootDir) {
|
|
|
331
350
|
const sessions = Array.isArray(config.sessions) ? config.sessions : [];
|
|
332
351
|
return sessions.find((entry) => entry.rootDir === rootDir) || null;
|
|
333
352
|
}
|
|
334
|
-
async function saveSessionForRoot(sessionCode, sessionPassword) {
|
|
353
|
+
async function saveSessionForRoot(sessionCode, sessionPassword, managerSessionId) {
|
|
335
354
|
const config = await getCliConfig();
|
|
336
355
|
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
356
|
+
const previous = sessions.find((entry) => entry.rootDir === ROOT_DIR);
|
|
337
357
|
const nextEntry = {
|
|
338
358
|
rootDir: ROOT_DIR,
|
|
339
359
|
sessionCode,
|
|
340
360
|
sessionPassword,
|
|
361
|
+
managerSessionId,
|
|
341
362
|
savedAt: Date.now(),
|
|
363
|
+
pushDevices: previous?.pushDevices ?? [],
|
|
342
364
|
};
|
|
343
365
|
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
344
366
|
deduped.unshift(nextEntry);
|
|
@@ -347,6 +369,192 @@ async function saveSessionForRoot(sessionCode, sessionPassword) {
|
|
|
347
369
|
sessions: deduped.slice(0, 100),
|
|
348
370
|
});
|
|
349
371
|
}
|
|
372
|
+
async function savePushDeviceForCurrentSession(payload) {
|
|
373
|
+
if (!currentSessionPassword) {
|
|
374
|
+
throw Object.assign(new Error("No active session"), { code: "EUNAVAILABLE" });
|
|
375
|
+
}
|
|
376
|
+
const phoneId = typeof payload.phoneId === "string" ? payload.phoneId.trim() : "";
|
|
377
|
+
if (!phoneId) {
|
|
378
|
+
throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
|
|
379
|
+
}
|
|
380
|
+
const expoPushToken = typeof payload.expoPushToken === "string" && payload.expoPushToken.trim()
|
|
381
|
+
? payload.expoPushToken.trim()
|
|
382
|
+
: null;
|
|
383
|
+
const notificationsEnabled = payload.notificationsEnabled === true && Boolean(expoPushToken);
|
|
384
|
+
const platform = typeof payload.platform === "string" && payload.platform.trim()
|
|
385
|
+
? payload.platform.trim()
|
|
386
|
+
: null;
|
|
387
|
+
const config = await getCliConfig();
|
|
388
|
+
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const currentEntry = sessions.find((entry) => entry.rootDir === ROOT_DIR) ?? {
|
|
391
|
+
rootDir: ROOT_DIR,
|
|
392
|
+
sessionCode: currentSessionCode,
|
|
393
|
+
sessionPassword: currentSessionPassword,
|
|
394
|
+
managerSessionId: currentManagerSessionId,
|
|
395
|
+
savedAt: now,
|
|
396
|
+
pushDevices: [],
|
|
397
|
+
};
|
|
398
|
+
const nextDevices = (currentEntry.pushDevices ?? []).filter((device) => device.phoneId !== phoneId);
|
|
399
|
+
nextDevices.unshift({
|
|
400
|
+
phoneId,
|
|
401
|
+
expoPushToken,
|
|
402
|
+
notificationsEnabled,
|
|
403
|
+
platform,
|
|
404
|
+
updatedAt: now,
|
|
405
|
+
});
|
|
406
|
+
const nextEntry = {
|
|
407
|
+
...currentEntry,
|
|
408
|
+
sessionCode: currentSessionCode,
|
|
409
|
+
sessionPassword: currentSessionPassword,
|
|
410
|
+
managerSessionId: currentManagerSessionId,
|
|
411
|
+
savedAt: now,
|
|
412
|
+
pushDevices: nextDevices.slice(0, 10),
|
|
413
|
+
};
|
|
414
|
+
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
415
|
+
deduped.unshift(nextEntry);
|
|
416
|
+
await writeCliConfig({
|
|
417
|
+
...config,
|
|
418
|
+
sessions: deduped.slice(0, 100),
|
|
419
|
+
});
|
|
420
|
+
return { ok: true, notificationsEnabled, tokenStored: Boolean(expoPushToken) };
|
|
421
|
+
}
|
|
422
|
+
function readAiEventSessionId(event) {
|
|
423
|
+
const properties = event.properties || {};
|
|
424
|
+
const info = properties.info && typeof properties.info === "object" ? properties.info : {};
|
|
425
|
+
return ((typeof properties.sessionID === "string" && properties.sessionID)
|
|
426
|
+
|| (typeof properties.sessionId === "string" && properties.sessionId)
|
|
427
|
+
|| (typeof info.sessionID === "string" && info.sessionID)
|
|
428
|
+
|| (typeof info.sessionId === "string" && info.sessionId)
|
|
429
|
+
|| (typeof info.id === "string" && info.id)
|
|
430
|
+
|| null);
|
|
431
|
+
}
|
|
432
|
+
function readAiStatusType(status) {
|
|
433
|
+
if (typeof status === "string")
|
|
434
|
+
return status.toLowerCase();
|
|
435
|
+
if (status && typeof status === "object") {
|
|
436
|
+
const value = status.type;
|
|
437
|
+
if (typeof value === "string")
|
|
438
|
+
return value.toLowerCase();
|
|
439
|
+
}
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
function isRunningAiStatus(status) {
|
|
443
|
+
const value = readAiStatusType(status);
|
|
444
|
+
return value === "running" || value === "busy" || value === "working" || value === "processing" || value === "retry";
|
|
445
|
+
}
|
|
446
|
+
function isTerminalAiStatus(status) {
|
|
447
|
+
const value = readAiStatusType(status);
|
|
448
|
+
return (value === "idle"
|
|
449
|
+
|| value === "complete"
|
|
450
|
+
|| value === "completed"
|
|
451
|
+
|| value === "done"
|
|
452
|
+
|| value === "finished"
|
|
453
|
+
|| value === "error"
|
|
454
|
+
|| value === "failed"
|
|
455
|
+
|| value === "cancelled"
|
|
456
|
+
|| value === "canceled");
|
|
457
|
+
}
|
|
458
|
+
async function getCurrentNotificationDevices() {
|
|
459
|
+
const config = await getCliConfig();
|
|
460
|
+
const saved = getSavedSessionForRoot(config, ROOT_DIR);
|
|
461
|
+
if (!saved || saved.sessionPassword !== currentSessionPassword)
|
|
462
|
+
return [];
|
|
463
|
+
return (saved.pushDevices ?? []).filter((device) => (device.notificationsEnabled
|
|
464
|
+
&& typeof device.expoPushToken === "string"
|
|
465
|
+
&& device.expoPushToken.length > 0));
|
|
466
|
+
}
|
|
467
|
+
async function clearInvalidNotificationDevices(phoneIds) {
|
|
468
|
+
if (phoneIds.length === 0)
|
|
469
|
+
return;
|
|
470
|
+
const invalidPhoneIds = new Set(phoneIds);
|
|
471
|
+
const config = await getCliConfig();
|
|
472
|
+
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
473
|
+
const saved = getSavedSessionForRoot(config, ROOT_DIR);
|
|
474
|
+
if (!saved?.pushDevices?.length)
|
|
475
|
+
return;
|
|
476
|
+
const nextEntry = {
|
|
477
|
+
...saved,
|
|
478
|
+
pushDevices: saved.pushDevices.map((device) => (invalidPhoneIds.has(device.phoneId)
|
|
479
|
+
? { ...device, expoPushToken: null, notificationsEnabled: false, updatedAt: Date.now() }
|
|
480
|
+
: device)),
|
|
481
|
+
};
|
|
482
|
+
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
483
|
+
deduped.unshift(nextEntry);
|
|
484
|
+
await writeCliConfig({
|
|
485
|
+
...config,
|
|
486
|
+
sessions: deduped.slice(0, 100),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function notifyManagerAiCompletion(backend, aiSessionId) {
|
|
490
|
+
if (appPeerConnected)
|
|
491
|
+
return;
|
|
492
|
+
if (!currentManagerSessionId || !currentSessionPassword)
|
|
493
|
+
return;
|
|
494
|
+
const devices = await getCurrentNotificationDevices();
|
|
495
|
+
if (devices.length === 0)
|
|
496
|
+
return;
|
|
497
|
+
const response = await fetch(new URL("/v1/notifications/ai-complete", MANAGER_URL), {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json" },
|
|
500
|
+
signal: AbortSignal.timeout(10_000),
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
sessionId: currentManagerSessionId,
|
|
503
|
+
resumeToken: currentSessionPassword,
|
|
504
|
+
backend,
|
|
505
|
+
aiSessionId,
|
|
506
|
+
devices: devices.map((device) => ({
|
|
507
|
+
phoneId: device.phoneId,
|
|
508
|
+
expoPushToken: device.expoPushToken,
|
|
509
|
+
platform: device.platform,
|
|
510
|
+
})),
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
if (DEBUG_MODE) {
|
|
515
|
+
console.warn(`[notifications] manager notify failed: ${response.status}`);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const body = await response.json().catch(() => ({}));
|
|
520
|
+
const invalidPhoneIds = Array.isArray(body.invalidPhoneIds)
|
|
521
|
+
? body.invalidPhoneIds.filter((value) => typeof value === "string")
|
|
522
|
+
: [];
|
|
523
|
+
if (invalidPhoneIds.length > 0) {
|
|
524
|
+
await clearInvalidNotificationDevices(invalidPhoneIds);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function handleAiNotificationEvent(backend, event) {
|
|
528
|
+
const sessionId = readAiEventSessionId(event);
|
|
529
|
+
if (!sessionId)
|
|
530
|
+
return;
|
|
531
|
+
const key = `${backend}:${sessionId}`;
|
|
532
|
+
if (event.type === "session.status") {
|
|
533
|
+
if (isRunningAiStatus(event.properties.status)) {
|
|
534
|
+
runningAiSessions.add(key);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!isTerminalAiStatus(event.properties.status))
|
|
538
|
+
return;
|
|
539
|
+
if (!runningAiSessions.delete(key))
|
|
540
|
+
return;
|
|
541
|
+
void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
|
|
542
|
+
if (DEBUG_MODE) {
|
|
543
|
+
console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (event.type !== "session.idle")
|
|
549
|
+
return;
|
|
550
|
+
if (!runningAiSessions.delete(key))
|
|
551
|
+
return;
|
|
552
|
+
void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
|
|
553
|
+
if (DEBUG_MODE) {
|
|
554
|
+
console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
350
558
|
async function clearSavedSessionForRoot() {
|
|
351
559
|
const config = await getCliConfig();
|
|
352
560
|
const sessions = Array.isArray(config.sessions) ? config.sessions : [];
|
|
@@ -1533,6 +1741,7 @@ function handleSystemCapabilities() {
|
|
|
1533
1741
|
platform: os.platform(),
|
|
1534
1742
|
rootDir: ROOT_DIR,
|
|
1535
1743
|
hostname: os.hostname(),
|
|
1744
|
+
managerSessionId: currentManagerSessionId,
|
|
1536
1745
|
};
|
|
1537
1746
|
}
|
|
1538
1747
|
function handleSystemPing() {
|
|
@@ -2506,6 +2715,9 @@ async function processMessage(message) {
|
|
|
2506
2715
|
case "ping":
|
|
2507
2716
|
result = handleSystemPing();
|
|
2508
2717
|
break;
|
|
2718
|
+
case "setPushToken":
|
|
2719
|
+
result = await savePushDeviceForCurrentSession(payload);
|
|
2720
|
+
break;
|
|
2509
2721
|
case "pairDevice": {
|
|
2510
2722
|
throw Object.assign(new Error("pairDevice is no longer supported"), { code: "EINVAL" });
|
|
2511
2723
|
}
|
|
@@ -2895,7 +3107,11 @@ async function assembleWithCode(code) {
|
|
|
2895
3107
|
return;
|
|
2896
3108
|
settled = true;
|
|
2897
3109
|
ws.send(JSON.stringify({ type: "ack" }));
|
|
2898
|
-
resolve({
|
|
3110
|
+
resolve({
|
|
3111
|
+
code: parsed.code,
|
|
3112
|
+
password: parsed.password,
|
|
3113
|
+
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : null,
|
|
3114
|
+
});
|
|
2899
3115
|
}
|
|
2900
3116
|
catch (error) {
|
|
2901
3117
|
fail(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -3157,6 +3373,7 @@ function startAiManagerInBackground() {
|
|
|
3157
3373
|
}
|
|
3158
3374
|
aiManager = manager;
|
|
3159
3375
|
aiManager.subscribe((backend, event) => {
|
|
3376
|
+
handleAiNotificationEvent(backend, event);
|
|
3160
3377
|
emitAppEvent({
|
|
3161
3378
|
v: 1,
|
|
3162
3379
|
id: `evt-${Date.now()}`,
|
|
@@ -3196,16 +3413,19 @@ async function connectWebSocketV2() {
|
|
|
3196
3413
|
if (message.type === "connected")
|
|
3197
3414
|
return;
|
|
3198
3415
|
if (message.type === "peer_connected") {
|
|
3416
|
+
appPeerConnected = true;
|
|
3199
3417
|
console.log("App connected!\n");
|
|
3200
3418
|
void publishDiscoveredPorts(true);
|
|
3201
3419
|
return;
|
|
3202
3420
|
}
|
|
3203
3421
|
if (message.type === "peer_disconnected") {
|
|
3422
|
+
appPeerConnected = false;
|
|
3204
3423
|
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
3205
3424
|
stopPortSync();
|
|
3206
3425
|
return;
|
|
3207
3426
|
}
|
|
3208
3427
|
if (message.type === "app_disconnected") {
|
|
3428
|
+
appPeerConnected = false;
|
|
3209
3429
|
if (message.reconnectDeadline) {
|
|
3210
3430
|
console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
|
|
3211
3431
|
}
|
|
@@ -3310,6 +3530,7 @@ async function main() {
|
|
|
3310
3530
|
displaySavedSessionNotice();
|
|
3311
3531
|
sessionCodeToUse = savedSession.sessionCode;
|
|
3312
3532
|
sessionPasswordToUse = savedSession.sessionPassword;
|
|
3533
|
+
currentManagerSessionId = savedSession.managerSessionId ?? null;
|
|
3313
3534
|
usedSavedSession = true;
|
|
3314
3535
|
}
|
|
3315
3536
|
else {
|
|
@@ -3323,7 +3544,8 @@ async function main() {
|
|
|
3323
3544
|
const assembled = await assembleWithCode(qr.code);
|
|
3324
3545
|
sessionCodeToUse = assembled.code;
|
|
3325
3546
|
sessionPasswordToUse = assembled.password;
|
|
3326
|
-
|
|
3547
|
+
currentManagerSessionId = assembled.sessionId;
|
|
3548
|
+
await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse, currentManagerSessionId);
|
|
3327
3549
|
}
|
|
3328
3550
|
currentSessionCode = sessionCodeToUse;
|
|
3329
3551
|
currentSessionPassword = sessionPasswordToUse;
|