agendex-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +244 -192
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2492,6 +2492,227 @@ function startWatching(onChange) {
2492
2492
  watchDir(dir, (f) => adapter.matches(f), onChange);
2493
2493
  }
2494
2494
  }
2495
+ // src/api.ts
2496
+ import { request as httpRequest } from "node:http";
2497
+ import { request as httpsRequest } from "node:https";
2498
+ import { hostname as osHostname } from "node:os";
2499
+
2500
+ // src/pid.ts
2501
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2502
+ import { homedir as homedir9, hostname } from "node:os";
2503
+ import { dirname, join as join10 } from "node:path";
2504
+ var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2505
+ function writePid() {
2506
+ mkdirSync2(dirname(pidPath), { recursive: true });
2507
+ const info = {
2508
+ pid: process.pid,
2509
+ startedAtMs: Date.now(),
2510
+ hostname: hostname()
2511
+ };
2512
+ writeFileSync2(pidPath, JSON.stringify(info));
2513
+ }
2514
+ function readPidInfo() {
2515
+ if (!existsSync6(pidPath))
2516
+ return null;
2517
+ const raw = readFileSync3(pidPath, "utf-8").trim();
2518
+ const asNumber = Number(raw);
2519
+ if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2520
+ return { pid: asNumber };
2521
+ }
2522
+ try {
2523
+ const parsed = JSON.parse(raw);
2524
+ if (Number.isFinite(parsed.pid) && parsed.pid > 0)
2525
+ return parsed;
2526
+ } catch {}
2527
+ return null;
2528
+ }
2529
+ function readPid() {
2530
+ return readPidInfo()?.pid ?? null;
2531
+ }
2532
+ function removePid() {
2533
+ try {
2534
+ unlinkSync(pidPath);
2535
+ } catch {}
2536
+ }
2537
+ function isRunning(pid) {
2538
+ try {
2539
+ process.kill(pid, 0);
2540
+ return true;
2541
+ } catch {
2542
+ return false;
2543
+ }
2544
+ }
2545
+
2546
+ // src/api.ts
2547
+ var cachedDeviceId;
2548
+ function getCloudConfig() {
2549
+ const config = loadConfig();
2550
+ if (!config?.cloudToken)
2551
+ throw new Error("Not logged in. Run `agendex login` first.");
2552
+ if (!config.convexUrl)
2553
+ throw new Error("No Convex URL configured. Run `agendex login` first.");
2554
+ return { token: config.cloudToken, convexUrl: config.convexUrl };
2555
+ }
2556
+ async function syncPlan(plan) {
2557
+ const { token, convexUrl } = getCloudConfig();
2558
+ const url = `${convexUrl}/api/cli/sync`;
2559
+ let activeToken = token;
2560
+ let res = await requestText(url, {
2561
+ method: "POST",
2562
+ headers: {
2563
+ Authorization: `Bearer ${activeToken}`,
2564
+ Connection: "close",
2565
+ "Content-Type": "application/json"
2566
+ },
2567
+ body: JSON.stringify(plan)
2568
+ });
2569
+ if (res.status === 401) {
2570
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
2571
+ if (refreshed) {
2572
+ activeToken = refreshed;
2573
+ res = await requestText(url, {
2574
+ method: "POST",
2575
+ headers: {
2576
+ Authorization: `Bearer ${activeToken}`,
2577
+ Connection: "close",
2578
+ "Content-Type": "application/json"
2579
+ },
2580
+ body: JSON.stringify(plan)
2581
+ });
2582
+ }
2583
+ }
2584
+ if (res.status < 200 || res.status >= 300) {
2585
+ return { ok: false, error: `${res.status}: ${res.body}` };
2586
+ }
2587
+ return { ok: true };
2588
+ }
2589
+ async function refreshStoredToken(currentToken, convexUrl) {
2590
+ const refreshed = await refreshToken(currentToken, convexUrl);
2591
+ if (!refreshed)
2592
+ return null;
2593
+ const config = loadConfig();
2594
+ if (config) {
2595
+ saveConfig({ ...config, cloudToken: refreshed.token });
2596
+ }
2597
+ return refreshed.token;
2598
+ }
2599
+ async function sendHeartbeat() {
2600
+ try {
2601
+ const { token, convexUrl } = getCloudConfig();
2602
+ const pidInfo = readPidInfo();
2603
+ cachedDeviceId ??= loadOrCreateDeviceId();
2604
+ const heartbeatBody = JSON.stringify({
2605
+ deviceId: cachedDeviceId,
2606
+ hostname: pidInfo?.hostname ?? osHostname(),
2607
+ startedAtMs: pidInfo?.startedAtMs,
2608
+ pid: pidInfo?.pid
2609
+ });
2610
+ let activeToken = token;
2611
+ let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2612
+ method: "POST",
2613
+ headers: {
2614
+ Authorization: `Bearer ${activeToken}`,
2615
+ Connection: "close",
2616
+ "Content-Type": "application/json"
2617
+ },
2618
+ body: heartbeatBody
2619
+ });
2620
+ if (res.status === 401) {
2621
+ const refreshedToken = await refreshStoredToken(activeToken, convexUrl);
2622
+ if (!refreshedToken)
2623
+ return;
2624
+ activeToken = refreshedToken;
2625
+ res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2626
+ method: "POST",
2627
+ headers: {
2628
+ Authorization: `Bearer ${activeToken}`,
2629
+ Connection: "close",
2630
+ "Content-Type": "application/json"
2631
+ },
2632
+ body: heartbeatBody
2633
+ });
2634
+ }
2635
+ if (res.status === 401) {
2636
+ return;
2637
+ }
2638
+ } catch {}
2639
+ }
2640
+ async function refreshToken(currentToken, convexUrl) {
2641
+ const res = await requestText(`${convexUrl}/api/cli/refresh`, {
2642
+ method: "POST",
2643
+ headers: {
2644
+ Authorization: `Bearer ${currentToken}`,
2645
+ Connection: "close",
2646
+ "Content-Type": "application/json"
2647
+ }
2648
+ });
2649
+ if (res.status < 200 || res.status >= 300)
2650
+ return null;
2651
+ const body = JSON.parse(res.body);
2652
+ if (!body.token)
2653
+ return null;
2654
+ return { token: body.token, expiresAt: body.expiresAt ?? 0 };
2655
+ }
2656
+ function requestText(urlString, options) {
2657
+ const url = new URL(urlString);
2658
+ const request = url.protocol === "https:" ? httpsRequest : httpRequest;
2659
+ return new Promise((resolve4, reject) => {
2660
+ const req = request(url, {
2661
+ agent: false,
2662
+ headers: options.headers,
2663
+ method: options.method
2664
+ }, (res) => {
2665
+ let body = "";
2666
+ res.setEncoding("utf8");
2667
+ res.on("data", (chunk) => {
2668
+ body += chunk;
2669
+ });
2670
+ res.on("end", () => {
2671
+ resolve4({
2672
+ status: res.statusCode ?? 0,
2673
+ body
2674
+ });
2675
+ });
2676
+ res.on("error", reject);
2677
+ });
2678
+ req.on("error", reject);
2679
+ if (options.body) {
2680
+ req.write(options.body);
2681
+ }
2682
+ req.end();
2683
+ });
2684
+ }
2685
+ async function fetchDevices() {
2686
+ const { token, convexUrl } = getCloudConfig();
2687
+ const url = `${convexUrl}/api/cli/devices`;
2688
+ let activeToken = token;
2689
+ let res = await requestText(url, {
2690
+ method: "GET",
2691
+ headers: {
2692
+ Authorization: `Bearer ${activeToken}`,
2693
+ Connection: "close"
2694
+ }
2695
+ });
2696
+ if (res.status === 401) {
2697
+ const refreshed = await refreshStoredToken(activeToken, convexUrl);
2698
+ if (refreshed) {
2699
+ activeToken = refreshed;
2700
+ res = await requestText(url, {
2701
+ method: "GET",
2702
+ headers: {
2703
+ Authorization: `Bearer ${activeToken}`,
2704
+ Connection: "close"
2705
+ }
2706
+ });
2707
+ }
2708
+ }
2709
+ if (res.status < 200 || res.status >= 300) {
2710
+ return [];
2711
+ }
2712
+ const body = JSON.parse(res.body);
2713
+ return body.devices ?? [];
2714
+ }
2715
+
2495
2716
  // src/auth.ts
2496
2717
  import { spawn } from "node:child_process";
2497
2718
  import { createServer } from "node:http";
@@ -2690,197 +2911,6 @@ function spawnBrowser(command, args, options = {}) {
2690
2911
  import { spawn as spawn2 } from "node:child_process";
2691
2912
  import { resolve as resolve4 } from "node:path";
2692
2913
  import { fileURLToPath } from "node:url";
2693
-
2694
- // src/api.ts
2695
- import { request as httpRequest } from "node:http";
2696
- import { request as httpsRequest } from "node:https";
2697
- import { hostname as osHostname } from "node:os";
2698
-
2699
- // src/pid.ts
2700
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2701
- import { homedir as homedir9, hostname } from "node:os";
2702
- import { dirname, join as join10 } from "node:path";
2703
- var pidPath = join10(homedir9(), ".agendex", "daemon.pid");
2704
- function writePid() {
2705
- mkdirSync2(dirname(pidPath), { recursive: true });
2706
- const info = {
2707
- pid: process.pid,
2708
- startedAtMs: Date.now(),
2709
- hostname: hostname()
2710
- };
2711
- writeFileSync2(pidPath, JSON.stringify(info));
2712
- }
2713
- function readPidInfo() {
2714
- if (!existsSync6(pidPath))
2715
- return null;
2716
- const raw = readFileSync3(pidPath, "utf-8").trim();
2717
- const asNumber = Number(raw);
2718
- if (Number.isFinite(asNumber) && asNumber > 0 && !raw.startsWith("{")) {
2719
- return { pid: asNumber };
2720
- }
2721
- try {
2722
- const parsed = JSON.parse(raw);
2723
- if (Number.isFinite(parsed.pid) && parsed.pid > 0)
2724
- return parsed;
2725
- } catch {}
2726
- return null;
2727
- }
2728
- function readPid() {
2729
- return readPidInfo()?.pid ?? null;
2730
- }
2731
- function removePid() {
2732
- try {
2733
- unlinkSync(pidPath);
2734
- } catch {}
2735
- }
2736
- function isRunning(pid) {
2737
- try {
2738
- process.kill(pid, 0);
2739
- return true;
2740
- } catch {
2741
- return false;
2742
- }
2743
- }
2744
-
2745
- // src/api.ts
2746
- var cachedDeviceId;
2747
- function getCloudConfig() {
2748
- const config = loadConfig();
2749
- if (!config?.cloudToken)
2750
- throw new Error("Not logged in. Run `agendex login` first.");
2751
- if (!config.convexUrl)
2752
- throw new Error("No Convex URL configured. Run `agendex login` first.");
2753
- return { token: config.cloudToken, convexUrl: config.convexUrl };
2754
- }
2755
- async function syncPlan(plan) {
2756
- const { token, convexUrl } = getCloudConfig();
2757
- const url = `${convexUrl}/api/cli/sync`;
2758
- let activeToken = token;
2759
- let res = await requestText(url, {
2760
- method: "POST",
2761
- headers: {
2762
- Authorization: `Bearer ${activeToken}`,
2763
- Connection: "close",
2764
- "Content-Type": "application/json"
2765
- },
2766
- body: JSON.stringify(plan)
2767
- });
2768
- if (res.status === 401) {
2769
- const refreshed = await refreshStoredToken(activeToken, convexUrl);
2770
- if (refreshed) {
2771
- activeToken = refreshed;
2772
- res = await requestText(url, {
2773
- method: "POST",
2774
- headers: {
2775
- Authorization: `Bearer ${activeToken}`,
2776
- Connection: "close",
2777
- "Content-Type": "application/json"
2778
- },
2779
- body: JSON.stringify(plan)
2780
- });
2781
- }
2782
- }
2783
- if (res.status < 200 || res.status >= 300) {
2784
- return { ok: false, error: `${res.status}: ${res.body}` };
2785
- }
2786
- return { ok: true };
2787
- }
2788
- async function refreshStoredToken(currentToken, convexUrl) {
2789
- const refreshed = await refreshToken(currentToken, convexUrl);
2790
- if (!refreshed)
2791
- return null;
2792
- const config = loadConfig();
2793
- if (config) {
2794
- saveConfig({ ...config, cloudToken: refreshed.token });
2795
- }
2796
- return refreshed.token;
2797
- }
2798
- async function sendHeartbeat() {
2799
- try {
2800
- const { token, convexUrl } = getCloudConfig();
2801
- const pidInfo = readPidInfo();
2802
- const heartbeatBody = JSON.stringify({
2803
- deviceId: cachedDeviceId ??= loadOrCreateDeviceId(),
2804
- hostname: pidInfo?.hostname ?? osHostname(),
2805
- startedAtMs: pidInfo?.startedAtMs
2806
- });
2807
- let activeToken = token;
2808
- let res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2809
- method: "POST",
2810
- headers: {
2811
- Authorization: `Bearer ${activeToken}`,
2812
- Connection: "close",
2813
- "Content-Type": "application/json"
2814
- },
2815
- body: heartbeatBody
2816
- });
2817
- if (res.status === 401) {
2818
- const refreshedToken = await refreshStoredToken(activeToken, convexUrl);
2819
- if (!refreshedToken)
2820
- return;
2821
- activeToken = refreshedToken;
2822
- res = await requestText(`${convexUrl}/api/cli/heartbeat`, {
2823
- method: "POST",
2824
- headers: {
2825
- Authorization: `Bearer ${activeToken}`,
2826
- Connection: "close",
2827
- "Content-Type": "application/json"
2828
- },
2829
- body: heartbeatBody
2830
- });
2831
- }
2832
- if (res.status === 401) {
2833
- return;
2834
- }
2835
- } catch {}
2836
- }
2837
- async function refreshToken(currentToken, convexUrl) {
2838
- const res = await requestText(`${convexUrl}/api/cli/refresh`, {
2839
- method: "POST",
2840
- headers: {
2841
- Authorization: `Bearer ${currentToken}`,
2842
- Connection: "close",
2843
- "Content-Type": "application/json"
2844
- }
2845
- });
2846
- if (res.status < 200 || res.status >= 300)
2847
- return null;
2848
- const body = JSON.parse(res.body);
2849
- if (!body.token)
2850
- return null;
2851
- return { token: body.token, expiresAt: body.expiresAt ?? 0 };
2852
- }
2853
- function requestText(urlString, options) {
2854
- const url = new URL(urlString);
2855
- const request = url.protocol === "https:" ? httpsRequest : httpRequest;
2856
- return new Promise((resolve4, reject) => {
2857
- const req = request(url, {
2858
- agent: false,
2859
- headers: options.headers,
2860
- method: options.method
2861
- }, (res) => {
2862
- let body = "";
2863
- res.setEncoding("utf8");
2864
- res.on("data", (chunk) => {
2865
- body += chunk;
2866
- });
2867
- res.on("end", () => {
2868
- resolve4({
2869
- status: res.statusCode ?? 0,
2870
- body
2871
- });
2872
- });
2873
- res.on("error", reject);
2874
- });
2875
- req.on("error", reject);
2876
- if (options.body) {
2877
- req.write(options.body);
2878
- }
2879
- req.end();
2880
- });
2881
- }
2882
-
2883
- // src/daemon.ts
2884
2914
  var MAX_RESTARTS = 5;
2885
2915
  var RESTART_WINDOW_MS = 60000;
2886
2916
  var RESTART_DELAY_MS = 5000;
@@ -3065,7 +3095,7 @@ import { join as join11 } from "node:path";
3065
3095
  // package.json
3066
3096
  var package_default = {
3067
3097
  name: "agendex-cli",
3068
- version: "0.6.0",
3098
+ version: "0.7.0",
3069
3099
  description: "Agendex CLI for login, sync, and daemon workflows",
3070
3100
  homepage: "https://github.com/Tyru5/Agendex#readme",
3071
3101
  repository: {
@@ -3276,6 +3306,28 @@ async function main() {
3276
3306
  writeStdout(`[agendex] Hostname: n/a`);
3277
3307
  }
3278
3308
  writeStdout(`[agendex] CLI version: ${CLI_VERSION}`);
3309
+ try {
3310
+ if (config?.cloudToken && config?.convexUrl) {
3311
+ const allDevices = await fetchDevices();
3312
+ if (allDevices.length > 0) {
3313
+ const now = Date.now();
3314
+ writeStdout(`[agendex] All daemons:`);
3315
+ for (const device of allDevices) {
3316
+ const age = device.lastSeenAt ? now - device.lastSeenAt : Number.POSITIVE_INFINITY;
3317
+ const status = age < CLI_DAEMON_STALE_AFTER_MS ? "alive" : "stale";
3318
+ const uptimeStr = device.startedAtMs != null ? formatDuration(now - device.startedAtMs) : "~";
3319
+ const pidStr = device.pid != null ? String(device.pid) : "~";
3320
+ const hostnameStr = device.hostname ?? "~";
3321
+ writeStdout(`- hostname: ${hostnameStr}
3322
+ pid: ${pidStr}
3323
+ uptime: ${uptimeStr}
3324
+ status: ${status}`);
3325
+ }
3326
+ } else {
3327
+ writeStdout(`[agendex] All daemons: none`);
3328
+ }
3329
+ }
3330
+ } catch {}
3279
3331
  return 0;
3280
3332
  }
3281
3333
  case "help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {