@wbern/cc-ping 0.1.0 → 1.1.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.
- package/README.md +241 -0
- package/dist/cli.js +801 -355
- package/package.json +15 -13
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/paths.ts
|
|
13
|
+
var paths_exports = {};
|
|
14
|
+
__export(paths_exports, {
|
|
15
|
+
resolveConfigDir: () => resolveConfigDir,
|
|
16
|
+
setConfigDir: () => setConfigDir
|
|
17
|
+
});
|
|
13
18
|
import { homedir } from "os";
|
|
14
19
|
import { join as join2 } from "path";
|
|
15
20
|
function setConfigDir(dir) {
|
|
@@ -181,6 +186,26 @@ var init_bell = __esm({
|
|
|
181
186
|
}
|
|
182
187
|
});
|
|
183
188
|
|
|
189
|
+
// src/color.ts
|
|
190
|
+
function isColorEnabled() {
|
|
191
|
+
if (process.env.NO_COLOR) return false;
|
|
192
|
+
if (process.env.FORCE_COLOR === "1") return true;
|
|
193
|
+
if (process.env.FORCE_COLOR === "0") return false;
|
|
194
|
+
return process.stdout.isTTY ?? false;
|
|
195
|
+
}
|
|
196
|
+
function wrap(code, text) {
|
|
197
|
+
return isColorEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
198
|
+
}
|
|
199
|
+
var green, red, yellow;
|
|
200
|
+
var init_color = __esm({
|
|
201
|
+
"src/color.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
green = (text) => wrap("32", text);
|
|
204
|
+
red = (text) => wrap("31", text);
|
|
205
|
+
yellow = (text) => wrap("33", text);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
184
209
|
// src/history.ts
|
|
185
210
|
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
186
211
|
import { join as join5 } from "path";
|
|
@@ -458,13 +483,13 @@ async function runPing(accounts, options) {
|
|
|
458
483
|
parallel: options.parallel
|
|
459
484
|
});
|
|
460
485
|
}
|
|
461
|
-
|
|
462
|
-
|
|
486
|
+
const total = results.length;
|
|
487
|
+
for (let idx = 0; idx < results.length; idx++) {
|
|
488
|
+
const r = results[idx];
|
|
489
|
+
const status = r.success ? green("ok") : red("FAIL");
|
|
463
490
|
const detail = r.error ? ` (${r.error})` : "";
|
|
464
|
-
const cr = r.claudeResponse;
|
|
465
|
-
const costInfo = cr ? ` $${cr.total_cost_usd.toFixed(4)} ${cr.usage.input_tokens + cr.usage.output_tokens} tok` : "";
|
|
466
491
|
logger.log(
|
|
467
|
-
` ${r.handle}: ${status} ${r.durationMs}ms${detail}
|
|
492
|
+
` [${idx + 1}/${total}] ${r.handle}: ${status} ${r.durationMs}ms${detail}`
|
|
468
493
|
);
|
|
469
494
|
appendHistoryEntry({
|
|
470
495
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -475,13 +500,13 @@ async function runPing(accounts, options) {
|
|
|
475
500
|
});
|
|
476
501
|
if (r.success) {
|
|
477
502
|
let meta;
|
|
478
|
-
if (
|
|
503
|
+
if (r.claudeResponse) {
|
|
479
504
|
meta = {
|
|
480
|
-
costUsd:
|
|
481
|
-
inputTokens:
|
|
482
|
-
outputTokens:
|
|
483
|
-
model:
|
|
484
|
-
sessionId:
|
|
505
|
+
costUsd: r.claudeResponse.total_cost_usd,
|
|
506
|
+
inputTokens: r.claudeResponse.usage.input_tokens,
|
|
507
|
+
outputTokens: r.claudeResponse.usage.output_tokens,
|
|
508
|
+
model: r.claudeResponse.model,
|
|
509
|
+
sessionId: r.claudeResponse.session_id
|
|
485
510
|
};
|
|
486
511
|
}
|
|
487
512
|
recordPing(r.handle, /* @__PURE__ */ new Date(), meta);
|
|
@@ -539,6 +564,7 @@ var init_run_ping = __esm({
|
|
|
539
564
|
"src/run-ping.ts"() {
|
|
540
565
|
"use strict";
|
|
541
566
|
init_bell();
|
|
567
|
+
init_color();
|
|
542
568
|
init_history();
|
|
543
569
|
init_logger();
|
|
544
570
|
init_notify();
|
|
@@ -547,262 +573,24 @@ var init_run_ping = __esm({
|
|
|
547
573
|
}
|
|
548
574
|
});
|
|
549
575
|
|
|
550
|
-
// src/cli.ts
|
|
551
|
-
import { Command } from "commander";
|
|
552
|
-
|
|
553
|
-
// src/check.ts
|
|
554
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
555
|
-
import { join } from "path";
|
|
556
|
-
function checkAccount(account) {
|
|
557
|
-
const issues = [];
|
|
558
|
-
if (!existsSync(account.configDir) || !statSync(account.configDir).isDirectory()) {
|
|
559
|
-
issues.push("config directory does not exist");
|
|
560
|
-
return {
|
|
561
|
-
handle: account.handle,
|
|
562
|
-
configDir: account.configDir,
|
|
563
|
-
healthy: false,
|
|
564
|
-
issues
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
const claudeJson = join(account.configDir, ".claude.json");
|
|
568
|
-
if (!existsSync(claudeJson)) {
|
|
569
|
-
issues.push(".claude.json not found");
|
|
570
|
-
return {
|
|
571
|
-
handle: account.handle,
|
|
572
|
-
configDir: account.configDir,
|
|
573
|
-
healthy: false,
|
|
574
|
-
issues
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
let parsed;
|
|
578
|
-
try {
|
|
579
|
-
const raw = readFileSync(claudeJson, "utf-8");
|
|
580
|
-
parsed = JSON.parse(raw);
|
|
581
|
-
} catch {
|
|
582
|
-
issues.push(".claude.json is not valid JSON");
|
|
583
|
-
return {
|
|
584
|
-
handle: account.handle,
|
|
585
|
-
configDir: account.configDir,
|
|
586
|
-
healthy: false,
|
|
587
|
-
issues
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
if (!parsed.oauthAccount) {
|
|
591
|
-
issues.push("no OAuth credentials found");
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
handle: account.handle,
|
|
595
|
-
configDir: account.configDir,
|
|
596
|
-
healthy: issues.length === 0,
|
|
597
|
-
issues
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
function checkAccounts(accounts) {
|
|
601
|
-
return accounts.map((a) => checkAccount(a));
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// src/completions.ts
|
|
605
|
-
var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
|
|
606
|
-
function bashCompletion() {
|
|
607
|
-
return `_cc_ping() {
|
|
608
|
-
local cur prev commands
|
|
609
|
-
COMPREPLY=()
|
|
610
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
611
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
612
|
-
commands="${COMMANDS}"
|
|
613
|
-
|
|
614
|
-
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
615
|
-
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
616
|
-
return 0
|
|
617
|
-
fi
|
|
618
|
-
|
|
619
|
-
case "\${COMP_WORDS[1]}" in
|
|
620
|
-
ping)
|
|
621
|
-
if [[ "\${cur}" == -* ]]; then
|
|
622
|
-
COMPREPLY=( $(compgen -W "--parallel --quiet --json --group --bell --stagger" -- "\${cur}") )
|
|
623
|
-
else
|
|
624
|
-
local handles=$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')
|
|
625
|
-
COMPREPLY=( $(compgen -W "\${handles}" -- "\${cur}") )
|
|
626
|
-
fi
|
|
627
|
-
;;
|
|
628
|
-
add)
|
|
629
|
-
COMPREPLY=( $(compgen -W "--group" -- "\${cur}") )
|
|
630
|
-
;;
|
|
631
|
-
list|history|status|next-reset|check)
|
|
632
|
-
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
633
|
-
;;
|
|
634
|
-
completions)
|
|
635
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
636
|
-
;;
|
|
637
|
-
daemon)
|
|
638
|
-
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
639
|
-
COMPREPLY=( $(compgen -W "start stop status" -- "\${cur}") )
|
|
640
|
-
elif [[ "\${COMP_WORDS[2]}" == "start" && "\${cur}" == -* ]]; then
|
|
641
|
-
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
642
|
-
elif [[ "\${COMP_WORDS[2]}" == "status" && "\${cur}" == -* ]]; then
|
|
643
|
-
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
644
|
-
fi
|
|
645
|
-
;;
|
|
646
|
-
esac
|
|
647
|
-
return 0
|
|
648
|
-
}
|
|
649
|
-
complete -F _cc_ping cc-ping
|
|
650
|
-
`;
|
|
651
|
-
}
|
|
652
|
-
function zshCompletion() {
|
|
653
|
-
return `#compdef cc-ping
|
|
654
|
-
|
|
655
|
-
_cc_ping() {
|
|
656
|
-
local -a commands
|
|
657
|
-
commands=(
|
|
658
|
-
'ping:Ping configured accounts'
|
|
659
|
-
'scan:Auto-discover accounts'
|
|
660
|
-
'add:Add an account manually'
|
|
661
|
-
'remove:Remove an account'
|
|
662
|
-
'list:List configured accounts'
|
|
663
|
-
'status:Show account status'
|
|
664
|
-
'next-reset:Show soonest quota reset'
|
|
665
|
-
'history:Show ping history'
|
|
666
|
-
'suggest:Suggest next account'
|
|
667
|
-
'check:Verify account health'
|
|
668
|
-
'completions:Generate shell completions'
|
|
669
|
-
'moo:Send a test notification'
|
|
670
|
-
'daemon:Run auto-ping on a schedule'
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
_arguments -C \\
|
|
674
|
-
'--config[Config directory]:path:_files -/' \\
|
|
675
|
-
'1:command:->command' \\
|
|
676
|
-
'*::arg:->args'
|
|
677
|
-
|
|
678
|
-
case $state in
|
|
679
|
-
command)
|
|
680
|
-
_describe 'command' commands
|
|
681
|
-
;;
|
|
682
|
-
args)
|
|
683
|
-
case $words[1] in
|
|
684
|
-
ping)
|
|
685
|
-
_arguments \\
|
|
686
|
-
'--parallel[Ping in parallel]' \\
|
|
687
|
-
'--quiet[Suppress output]' \\
|
|
688
|
-
'--json[JSON output]' \\
|
|
689
|
-
'--group[Filter by group]:group:' \\
|
|
690
|
-
'--bell[Ring bell on failure]' \\
|
|
691
|
-
'--stagger[Delay between pings]:minutes:' \\
|
|
692
|
-
'*:handle:->handles'
|
|
693
|
-
if [[ $state == handles ]]; then
|
|
694
|
-
local -a handles
|
|
695
|
-
handles=(\${(f)"$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')"})
|
|
696
|
-
_describe 'handle' handles
|
|
697
|
-
fi
|
|
698
|
-
;;
|
|
699
|
-
completions)
|
|
700
|
-
_arguments '1:shell:(bash zsh fish)'
|
|
701
|
-
;;
|
|
702
|
-
list|history|status|next-reset|check)
|
|
703
|
-
_arguments '--json[JSON output]'
|
|
704
|
-
;;
|
|
705
|
-
add)
|
|
706
|
-
_arguments '--group[Assign group]:group:'
|
|
707
|
-
;;
|
|
708
|
-
daemon)
|
|
709
|
-
local -a subcmds
|
|
710
|
-
subcmds=(
|
|
711
|
-
'start:Start the daemon process'
|
|
712
|
-
'stop:Stop the daemon process'
|
|
713
|
-
'status:Show daemon status'
|
|
714
|
-
)
|
|
715
|
-
_arguments '1:subcommand:->subcmd' '*::arg:->subargs'
|
|
716
|
-
case $state in
|
|
717
|
-
subcmd)
|
|
718
|
-
_describe 'subcommand' subcmds
|
|
719
|
-
;;
|
|
720
|
-
subargs)
|
|
721
|
-
case $words[1] in
|
|
722
|
-
start)
|
|
723
|
-
_arguments \\
|
|
724
|
-
'--interval[Ping interval in minutes]:minutes:' \\
|
|
725
|
-
'--quiet[Suppress ping output]' \\
|
|
726
|
-
'--bell[Ring bell on failure]' \\
|
|
727
|
-
'--notify[Send notification on failure]'
|
|
728
|
-
;;
|
|
729
|
-
status)
|
|
730
|
-
_arguments '--json[JSON output]'
|
|
731
|
-
;;
|
|
732
|
-
esac
|
|
733
|
-
;;
|
|
734
|
-
esac
|
|
735
|
-
;;
|
|
736
|
-
esac
|
|
737
|
-
;;
|
|
738
|
-
esac
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
_cc_ping
|
|
742
|
-
`;
|
|
743
|
-
}
|
|
744
|
-
function fishCompletion() {
|
|
745
|
-
return `# Fish completions for cc-ping
|
|
746
|
-
set -l commands ping scan add remove list status next-reset history suggest check completions moo
|
|
747
|
-
|
|
748
|
-
complete -c cc-ping -f
|
|
749
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a ping -d "Ping configured accounts"
|
|
750
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a scan -d "Auto-discover accounts"
|
|
751
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a add -d "Add an account manually"
|
|
752
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a remove -d "Remove an account"
|
|
753
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a list -d "List configured accounts"
|
|
754
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a status -d "Show account status"
|
|
755
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a next-reset -d "Show soonest quota reset"
|
|
756
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a history -d "Show ping history"
|
|
757
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a suggest -d "Suggest next account"
|
|
758
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a check -d "Verify account health"
|
|
759
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a completions -d "Generate shell completions"
|
|
760
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a moo -d "Send a test notification"
|
|
761
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a daemon -d "Run auto-ping on a schedule"
|
|
762
|
-
|
|
763
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l parallel -d "Ping in parallel"
|
|
764
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s q -l quiet -d "Suppress output"
|
|
765
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l json -d "JSON output"
|
|
766
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s g -l group -r -d "Filter by group"
|
|
767
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l bell -d "Ring bell on failure"
|
|
768
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l stagger -r -d "Delay between pings"
|
|
769
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -a "(cc-ping list 2>/dev/null | string replace -r ' *(.*) ->.*' '$1')"
|
|
770
|
-
|
|
771
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from list history status next-reset check" -l json -d "JSON output"
|
|
772
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from add" -s g -l group -r -d "Assign group"
|
|
773
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
774
|
-
|
|
775
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a start -d "Start the daemon"
|
|
776
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a stop -d "Stop the daemon"
|
|
777
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a status -d "Show daemon status"
|
|
778
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l interval -r -d "Ping interval in minutes"
|
|
779
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -s q -l quiet -d "Suppress output"
|
|
780
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l bell -d "Ring bell on failure"
|
|
781
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l notify -d "Send notification on failure"
|
|
782
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status" -l json -d "JSON output"
|
|
783
|
-
`;
|
|
784
|
-
}
|
|
785
|
-
function generateCompletion(shell) {
|
|
786
|
-
switch (shell) {
|
|
787
|
-
case "bash":
|
|
788
|
-
return bashCompletion();
|
|
789
|
-
case "zsh":
|
|
790
|
-
return zshCompletion();
|
|
791
|
-
case "fish":
|
|
792
|
-
return fishCompletion();
|
|
793
|
-
default:
|
|
794
|
-
throw new Error(
|
|
795
|
-
`Unsupported shell: ${shell}. Supported: bash, zsh, fish`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// src/cli.ts
|
|
801
|
-
init_config();
|
|
802
|
-
|
|
803
576
|
// src/daemon.ts
|
|
804
|
-
|
|
805
|
-
|
|
577
|
+
var daemon_exports = {};
|
|
578
|
+
__export(daemon_exports, {
|
|
579
|
+
daemonLogPath: () => daemonLogPath,
|
|
580
|
+
daemonLoop: () => daemonLoop,
|
|
581
|
+
daemonPidPath: () => daemonPidPath,
|
|
582
|
+
daemonStopPath: () => daemonStopPath,
|
|
583
|
+
getDaemonStatus: () => getDaemonStatus,
|
|
584
|
+
isProcessRunning: () => isProcessRunning,
|
|
585
|
+
parseInterval: () => parseInterval,
|
|
586
|
+
readDaemonState: () => readDaemonState,
|
|
587
|
+
removeDaemonState: () => removeDaemonState,
|
|
588
|
+
runDaemon: () => runDaemon,
|
|
589
|
+
runDaemonWithDefaults: () => runDaemonWithDefaults,
|
|
590
|
+
startDaemon: () => startDaemon,
|
|
591
|
+
stopDaemon: () => stopDaemon,
|
|
592
|
+
writeDaemonState: () => writeDaemonState
|
|
593
|
+
});
|
|
806
594
|
import { execSync, spawn } from "child_process";
|
|
807
595
|
import {
|
|
808
596
|
existsSync as existsSync5,
|
|
@@ -814,9 +602,6 @@ import {
|
|
|
814
602
|
writeFileSync as writeFileSync3
|
|
815
603
|
} from "fs";
|
|
816
604
|
import { join as join6 } from "path";
|
|
817
|
-
var GRACEFUL_POLL_MS = 500;
|
|
818
|
-
var GRACEFUL_POLL_ATTEMPTS = 20;
|
|
819
|
-
var POST_KILL_DELAY_MS = 1e3;
|
|
820
605
|
function daemonPidPath() {
|
|
821
606
|
return join6(resolveConfigDir(), "daemon.json");
|
|
822
607
|
}
|
|
@@ -1049,6 +834,7 @@ async function stopDaemon(deps) {
|
|
|
1049
834
|
}
|
|
1050
835
|
async function runDaemon(intervalMs, options, deps) {
|
|
1051
836
|
const stopPath = daemonStopPath();
|
|
837
|
+
if (existsSync5(stopPath)) unlinkSync(stopPath);
|
|
1052
838
|
const cleanup = () => {
|
|
1053
839
|
if (existsSync5(stopPath)) unlinkSync(stopPath);
|
|
1054
840
|
removeDaemonState();
|
|
@@ -1096,28 +882,549 @@ async function runDaemonWithDefaults(intervalMs, options) {
|
|
|
1096
882
|
exit: (code) => process.exit(code)
|
|
1097
883
|
});
|
|
1098
884
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
885
|
+
var GRACEFUL_POLL_MS, GRACEFUL_POLL_ATTEMPTS, POST_KILL_DELAY_MS;
|
|
886
|
+
var init_daemon = __esm({
|
|
887
|
+
"src/daemon.ts"() {
|
|
888
|
+
"use strict";
|
|
889
|
+
init_paths();
|
|
890
|
+
init_state();
|
|
891
|
+
GRACEFUL_POLL_MS = 500;
|
|
892
|
+
GRACEFUL_POLL_ATTEMPTS = 20;
|
|
893
|
+
POST_KILL_DELAY_MS = 1e3;
|
|
1106
894
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// src/service.ts
|
|
898
|
+
var service_exports = {};
|
|
899
|
+
__export(service_exports, {
|
|
900
|
+
generateLaunchdPlist: () => generateLaunchdPlist,
|
|
901
|
+
generateSystemdUnit: () => generateSystemdUnit,
|
|
902
|
+
getServiceStatus: () => getServiceStatus,
|
|
903
|
+
installService: () => installService,
|
|
904
|
+
resolveExecutable: () => resolveExecutable,
|
|
905
|
+
servicePath: () => servicePath,
|
|
906
|
+
uninstallService: () => uninstallService
|
|
907
|
+
});
|
|
908
|
+
import { execSync as nodeExecSync } from "child_process";
|
|
909
|
+
import {
|
|
910
|
+
existsSync as nodeExistsSync,
|
|
911
|
+
mkdirSync as nodeMkdirSync,
|
|
912
|
+
unlinkSync as nodeUnlinkSync,
|
|
913
|
+
writeFileSync as nodeWriteFileSync
|
|
914
|
+
} from "fs";
|
|
915
|
+
import { homedir as nodeHomedir } from "os";
|
|
916
|
+
import { dirname, join as join9 } from "path";
|
|
917
|
+
function resolveExecutable(deps) {
|
|
918
|
+
try {
|
|
919
|
+
const path = deps.execSync("which cc-ping", {
|
|
920
|
+
encoding: "utf-8",
|
|
921
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
922
|
+
}).trim();
|
|
923
|
+
if (path) {
|
|
924
|
+
return { executable: path, args: [] };
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
executable: process.execPath,
|
|
930
|
+
args: [process.argv[1]]
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function generateLaunchdPlist(options, execInfo, configDir) {
|
|
934
|
+
const intervalMs = parseIntervalForService(options.interval);
|
|
935
|
+
const programArgs = [
|
|
936
|
+
...execInfo.args,
|
|
937
|
+
"daemon",
|
|
938
|
+
"_run",
|
|
939
|
+
"--interval-ms",
|
|
940
|
+
String(intervalMs)
|
|
941
|
+
];
|
|
942
|
+
if (options.quiet) programArgs.push("--quiet");
|
|
943
|
+
if (options.bell) programArgs.push("--bell");
|
|
944
|
+
if (options.notify) programArgs.push("--notify");
|
|
945
|
+
const allArgs = [execInfo.executable, ...programArgs];
|
|
946
|
+
const argsXml = allArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
947
|
+
const logPath = join9(
|
|
948
|
+
configDir || join9(nodeHomedir(), ".config", "cc-ping"),
|
|
949
|
+
"daemon.log"
|
|
950
|
+
);
|
|
951
|
+
let envSection = "";
|
|
952
|
+
if (configDir) {
|
|
953
|
+
envSection = `
|
|
954
|
+
<key>EnvironmentVariables</key>
|
|
955
|
+
<dict>
|
|
956
|
+
<key>CC_PING_CONFIG</key>
|
|
957
|
+
<string>${escapeXml(configDir)}</string>
|
|
958
|
+
</dict>`;
|
|
959
|
+
}
|
|
960
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
961
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
962
|
+
<plist version="1.0">
|
|
963
|
+
<dict>
|
|
964
|
+
<key>Label</key>
|
|
965
|
+
<string>${PLIST_LABEL}</string>
|
|
966
|
+
<key>ProgramArguments</key>
|
|
967
|
+
<array>
|
|
968
|
+
${argsXml}
|
|
969
|
+
</array>
|
|
970
|
+
<key>RunAtLoad</key>
|
|
971
|
+
<true/>
|
|
972
|
+
<key>KeepAlive</key>
|
|
973
|
+
<dict>
|
|
974
|
+
<key>SuccessfulExit</key>
|
|
975
|
+
<false/>
|
|
976
|
+
</dict>
|
|
977
|
+
<key>StandardOutPath</key>
|
|
978
|
+
<string>${escapeXml(logPath)}</string>
|
|
979
|
+
<key>StandardErrorPath</key>
|
|
980
|
+
<string>${escapeXml(logPath)}</string>${envSection}
|
|
981
|
+
</dict>
|
|
982
|
+
</plist>
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
function generateSystemdUnit(options, execInfo, configDir) {
|
|
986
|
+
const intervalMs = parseIntervalForService(options.interval);
|
|
987
|
+
const programArgs = [
|
|
988
|
+
...execInfo.args,
|
|
989
|
+
"daemon",
|
|
990
|
+
"_run",
|
|
991
|
+
"--interval-ms",
|
|
992
|
+
String(intervalMs)
|
|
993
|
+
];
|
|
994
|
+
if (options.quiet) programArgs.push("--quiet");
|
|
995
|
+
if (options.bell) programArgs.push("--bell");
|
|
996
|
+
if (options.notify) programArgs.push("--notify");
|
|
997
|
+
const execStart = [execInfo.executable, ...programArgs].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
|
|
998
|
+
let envLine = "";
|
|
999
|
+
if (configDir) {
|
|
1000
|
+
envLine = `
|
|
1001
|
+
Environment=CC_PING_CONFIG=${configDir}`;
|
|
1002
|
+
}
|
|
1003
|
+
return `[Unit]
|
|
1004
|
+
Description=cc-ping daemon - auto-ping Claude Code sessions
|
|
1005
|
+
|
|
1006
|
+
[Service]
|
|
1007
|
+
Type=simple
|
|
1008
|
+
ExecStart=${execStart}${envLine}
|
|
1009
|
+
Restart=on-failure
|
|
1010
|
+
RestartSec=10
|
|
1011
|
+
|
|
1012
|
+
[Install]
|
|
1013
|
+
WantedBy=default.target
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
function servicePath(platform, home) {
|
|
1017
|
+
switch (platform) {
|
|
1018
|
+
case "darwin":
|
|
1019
|
+
return join9(home, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
1020
|
+
case "linux":
|
|
1021
|
+
return join9(
|
|
1022
|
+
home,
|
|
1023
|
+
".config",
|
|
1024
|
+
"systemd",
|
|
1025
|
+
"user",
|
|
1026
|
+
`${SYSTEMD_SERVICE}.service`
|
|
1027
|
+
);
|
|
1028
|
+
default:
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`Unsupported platform: ${platform}. Only macOS and Linux are supported.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async function installService(options, deps) {
|
|
1035
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1036
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1037
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1038
|
+
const _mkdirSync = deps?.mkdirSync ?? nodeMkdirSync;
|
|
1039
|
+
const _writeFileSync = deps?.writeFileSync ?? nodeWriteFileSync;
|
|
1040
|
+
const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
|
|
1041
|
+
const _stopDaemon = deps?.stopDaemon ?? (async () => {
|
|
1042
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
1043
|
+
return stopDaemon2();
|
|
1044
|
+
});
|
|
1045
|
+
const _configDir = deps?.configDir;
|
|
1046
|
+
const home = _homedir();
|
|
1047
|
+
let path;
|
|
1048
|
+
try {
|
|
1049
|
+
path = servicePath(_platform, home);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
return {
|
|
1052
|
+
success: false,
|
|
1053
|
+
servicePath: "",
|
|
1054
|
+
error: err.message
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (_existsSync(path)) {
|
|
1058
|
+
return {
|
|
1059
|
+
success: false,
|
|
1060
|
+
servicePath: path,
|
|
1061
|
+
error: `Service already installed at ${path}. Run \`daemon uninstall\` first.`
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
await _stopDaemon();
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
const execInfo = resolveExecutable({
|
|
1069
|
+
execSync: _execSync
|
|
1070
|
+
});
|
|
1071
|
+
const configDir = _configDir || void 0;
|
|
1072
|
+
let content;
|
|
1073
|
+
if (_platform === "darwin") {
|
|
1074
|
+
content = generateLaunchdPlist(options, execInfo, configDir);
|
|
1075
|
+
} else {
|
|
1076
|
+
content = generateSystemdUnit(options, execInfo, configDir);
|
|
1077
|
+
}
|
|
1078
|
+
_mkdirSync(dirname(path), { recursive: true });
|
|
1079
|
+
_writeFileSync(path, content);
|
|
1080
|
+
try {
|
|
1081
|
+
if (_platform === "darwin") {
|
|
1082
|
+
_execSync(`launchctl load ${path}`);
|
|
1083
|
+
} else {
|
|
1084
|
+
_execSync("systemctl --user daemon-reload");
|
|
1085
|
+
_execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE}`);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
return {
|
|
1089
|
+
success: false,
|
|
1090
|
+
servicePath: path,
|
|
1091
|
+
error: `Service file written but failed to load: ${err.message}`
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return { success: true, servicePath: path };
|
|
1095
|
+
}
|
|
1096
|
+
async function uninstallService(deps) {
|
|
1097
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1098
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1099
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1100
|
+
const _unlinkSync = deps?.unlinkSync ?? nodeUnlinkSync;
|
|
1101
|
+
const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
|
|
1102
|
+
const home = _homedir();
|
|
1103
|
+
let path;
|
|
1104
|
+
try {
|
|
1105
|
+
path = servicePath(_platform, home);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
return {
|
|
1108
|
+
success: false,
|
|
1109
|
+
error: err.message
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
if (!_existsSync(path)) {
|
|
1113
|
+
return {
|
|
1114
|
+
success: false,
|
|
1115
|
+
servicePath: path,
|
|
1116
|
+
error: "No service installed."
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
if (_platform === "darwin") {
|
|
1121
|
+
_execSync(`launchctl unload ${path}`);
|
|
1122
|
+
} else {
|
|
1123
|
+
_execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE}`);
|
|
1124
|
+
}
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
_unlinkSync(path);
|
|
1128
|
+
return { success: true, servicePath: path };
|
|
1129
|
+
}
|
|
1130
|
+
function getServiceStatus(deps) {
|
|
1131
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1132
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1133
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1134
|
+
const home = _homedir();
|
|
1135
|
+
let path;
|
|
1136
|
+
try {
|
|
1137
|
+
path = servicePath(_platform, home);
|
|
1138
|
+
} catch {
|
|
1139
|
+
return { installed: false, platform: _platform };
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
installed: _existsSync(path),
|
|
1143
|
+
servicePath: path,
|
|
1144
|
+
platform: _platform
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function escapeXml(str) {
|
|
1148
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1149
|
+
}
|
|
1150
|
+
function parseIntervalForService(value) {
|
|
1151
|
+
if (!value) return QUOTA_WINDOW_MS2;
|
|
1152
|
+
const minutes = Number(value);
|
|
1153
|
+
if (Number.isNaN(minutes) || minutes <= 0) return QUOTA_WINDOW_MS2;
|
|
1154
|
+
return minutes * 60 * 1e3;
|
|
1155
|
+
}
|
|
1156
|
+
var PLIST_LABEL, SYSTEMD_SERVICE, QUOTA_WINDOW_MS2;
|
|
1157
|
+
var init_service = __esm({
|
|
1158
|
+
"src/service.ts"() {
|
|
1159
|
+
"use strict";
|
|
1160
|
+
PLIST_LABEL = "com.cc-ping.daemon";
|
|
1161
|
+
SYSTEMD_SERVICE = "cc-ping-daemon";
|
|
1162
|
+
QUOTA_WINDOW_MS2 = 5 * 60 * 60 * 1e3;
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// src/cli.ts
|
|
1167
|
+
import { Command } from "commander";
|
|
1168
|
+
|
|
1169
|
+
// src/check.ts
|
|
1170
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
1171
|
+
import { join } from "path";
|
|
1172
|
+
function checkAccount(account) {
|
|
1173
|
+
const issues = [];
|
|
1174
|
+
if (!existsSync(account.configDir) || !statSync(account.configDir).isDirectory()) {
|
|
1175
|
+
issues.push("config directory does not exist");
|
|
1176
|
+
return {
|
|
1177
|
+
handle: account.handle,
|
|
1178
|
+
configDir: account.configDir,
|
|
1179
|
+
healthy: false,
|
|
1180
|
+
issues
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
const claudeJson = join(account.configDir, ".claude.json");
|
|
1184
|
+
if (!existsSync(claudeJson)) {
|
|
1185
|
+
issues.push(".claude.json not found");
|
|
1186
|
+
return {
|
|
1187
|
+
handle: account.handle,
|
|
1188
|
+
configDir: account.configDir,
|
|
1189
|
+
healthy: false,
|
|
1190
|
+
issues
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
let parsed;
|
|
1194
|
+
try {
|
|
1195
|
+
const raw = readFileSync(claudeJson, "utf-8");
|
|
1196
|
+
parsed = JSON.parse(raw);
|
|
1197
|
+
} catch {
|
|
1198
|
+
issues.push(".claude.json is not valid JSON");
|
|
1199
|
+
return {
|
|
1200
|
+
handle: account.handle,
|
|
1201
|
+
configDir: account.configDir,
|
|
1202
|
+
healthy: false,
|
|
1203
|
+
issues
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
if (!parsed.oauthAccount) {
|
|
1207
|
+
issues.push("no OAuth credentials found");
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
handle: account.handle,
|
|
1211
|
+
configDir: account.configDir,
|
|
1212
|
+
healthy: issues.length === 0,
|
|
1213
|
+
issues
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function checkAccounts(accounts) {
|
|
1217
|
+
return accounts.map((a) => checkAccount(a));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/completions.ts
|
|
1221
|
+
var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
|
|
1222
|
+
function bashCompletion() {
|
|
1223
|
+
return `_cc_ping() {
|
|
1224
|
+
local cur prev commands
|
|
1225
|
+
COMPREPLY=()
|
|
1226
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1227
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1228
|
+
commands="${COMMANDS}"
|
|
1229
|
+
|
|
1230
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
1231
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
1232
|
+
return 0
|
|
1233
|
+
fi
|
|
1234
|
+
|
|
1235
|
+
case "\${COMP_WORDS[1]}" in
|
|
1236
|
+
ping)
|
|
1237
|
+
if [[ "\${cur}" == -* ]]; then
|
|
1238
|
+
COMPREPLY=( $(compgen -W "--parallel --quiet --json --group --bell --stagger" -- "\${cur}") )
|
|
1239
|
+
else
|
|
1240
|
+
local handles=$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')
|
|
1241
|
+
COMPREPLY=( $(compgen -W "\${handles}" -- "\${cur}") )
|
|
1242
|
+
fi
|
|
1243
|
+
;;
|
|
1244
|
+
add)
|
|
1245
|
+
COMPREPLY=( $(compgen -W "--group" -- "\${cur}") )
|
|
1246
|
+
;;
|
|
1247
|
+
list|history|status|next-reset|check)
|
|
1248
|
+
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
1249
|
+
;;
|
|
1250
|
+
completions)
|
|
1251
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
1252
|
+
;;
|
|
1253
|
+
daemon)
|
|
1254
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
1255
|
+
COMPREPLY=( $(compgen -W "start stop status install uninstall" -- "\${cur}") )
|
|
1256
|
+
elif [[ "\${COMP_WORDS[2]}" == "start" && "\${cur}" == -* ]]; then
|
|
1257
|
+
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
1258
|
+
elif [[ "\${COMP_WORDS[2]}" == "install" && "\${cur}" == -* ]]; then
|
|
1259
|
+
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
1260
|
+
elif [[ "\${COMP_WORDS[2]}" == "status" && "\${cur}" == -* ]]; then
|
|
1261
|
+
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
1262
|
+
fi
|
|
1263
|
+
;;
|
|
1264
|
+
esac
|
|
1265
|
+
return 0
|
|
1266
|
+
}
|
|
1267
|
+
complete -F _cc_ping cc-ping
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
function zshCompletion() {
|
|
1271
|
+
return `#compdef cc-ping
|
|
1272
|
+
|
|
1273
|
+
_cc_ping() {
|
|
1274
|
+
local -a commands
|
|
1275
|
+
commands=(
|
|
1276
|
+
'ping:Ping configured accounts'
|
|
1277
|
+
'scan:Auto-discover accounts'
|
|
1278
|
+
'add:Add an account manually'
|
|
1279
|
+
'remove:Remove an account'
|
|
1280
|
+
'list:List configured accounts'
|
|
1281
|
+
'status:Show account status'
|
|
1282
|
+
'next-reset:Show soonest quota reset'
|
|
1283
|
+
'history:Show ping history'
|
|
1284
|
+
'suggest:Suggest next account'
|
|
1285
|
+
'check:Verify account health'
|
|
1286
|
+
'completions:Generate shell completions'
|
|
1287
|
+
'moo:Send a test notification'
|
|
1288
|
+
'daemon:Run auto-ping on a schedule'
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
_arguments -C \\
|
|
1292
|
+
'--config[Config directory]:path:_files -/' \\
|
|
1293
|
+
'1:command:->command' \\
|
|
1294
|
+
'*::arg:->args'
|
|
1295
|
+
|
|
1296
|
+
case $state in
|
|
1297
|
+
command)
|
|
1298
|
+
_describe 'command' commands
|
|
1299
|
+
;;
|
|
1300
|
+
args)
|
|
1301
|
+
case $words[1] in
|
|
1302
|
+
ping)
|
|
1303
|
+
_arguments \\
|
|
1304
|
+
'--parallel[Ping in parallel]' \\
|
|
1305
|
+
'--quiet[Suppress output]' \\
|
|
1306
|
+
'--json[JSON output]' \\
|
|
1307
|
+
'--group[Filter by group]:group:' \\
|
|
1308
|
+
'--bell[Ring bell on failure]' \\
|
|
1309
|
+
'--stagger[Delay between pings]:minutes:' \\
|
|
1310
|
+
'*:handle:->handles'
|
|
1311
|
+
if [[ $state == handles ]]; then
|
|
1312
|
+
local -a handles
|
|
1313
|
+
handles=(\${(f)"$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')"})
|
|
1314
|
+
_describe 'handle' handles
|
|
1315
|
+
fi
|
|
1316
|
+
;;
|
|
1317
|
+
completions)
|
|
1318
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
1319
|
+
;;
|
|
1320
|
+
list|history|status|next-reset|check)
|
|
1321
|
+
_arguments '--json[JSON output]'
|
|
1322
|
+
;;
|
|
1323
|
+
add)
|
|
1324
|
+
_arguments '--group[Assign group]:group:'
|
|
1325
|
+
;;
|
|
1326
|
+
daemon)
|
|
1327
|
+
local -a subcmds
|
|
1328
|
+
subcmds=(
|
|
1329
|
+
'start:Start the daemon process'
|
|
1330
|
+
'stop:Stop the daemon process'
|
|
1331
|
+
'status:Show daemon status'
|
|
1332
|
+
'install:Install as system service'
|
|
1333
|
+
'uninstall:Remove system service'
|
|
1334
|
+
)
|
|
1335
|
+
_arguments '1:subcommand:->subcmd' '*::arg:->subargs'
|
|
1336
|
+
case $state in
|
|
1337
|
+
subcmd)
|
|
1338
|
+
_describe 'subcommand' subcmds
|
|
1339
|
+
;;
|
|
1340
|
+
subargs)
|
|
1341
|
+
case $words[1] in
|
|
1342
|
+
start|install)
|
|
1343
|
+
_arguments \\
|
|
1344
|
+
'--interval[Ping interval in minutes]:minutes:' \\
|
|
1345
|
+
'--quiet[Suppress ping output]' \\
|
|
1346
|
+
'--bell[Ring bell on failure]' \\
|
|
1347
|
+
'--notify[Send notification on failure]'
|
|
1348
|
+
;;
|
|
1349
|
+
status)
|
|
1350
|
+
_arguments '--json[JSON output]'
|
|
1351
|
+
;;
|
|
1352
|
+
esac
|
|
1353
|
+
;;
|
|
1354
|
+
esac
|
|
1355
|
+
;;
|
|
1356
|
+
esac
|
|
1357
|
+
;;
|
|
1358
|
+
esac
|
|
1109
1359
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1360
|
+
|
|
1361
|
+
_cc_ping
|
|
1362
|
+
`;
|
|
1363
|
+
}
|
|
1364
|
+
function fishCompletion() {
|
|
1365
|
+
return `# Fish completions for cc-ping
|
|
1366
|
+
set -l commands ping scan add remove list status next-reset history suggest check completions moo
|
|
1367
|
+
|
|
1368
|
+
complete -c cc-ping -f
|
|
1369
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a ping -d "Ping configured accounts"
|
|
1370
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a scan -d "Auto-discover accounts"
|
|
1371
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a add -d "Add an account manually"
|
|
1372
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a remove -d "Remove an account"
|
|
1373
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a list -d "List configured accounts"
|
|
1374
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a status -d "Show account status"
|
|
1375
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a next-reset -d "Show soonest quota reset"
|
|
1376
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a history -d "Show ping history"
|
|
1377
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a suggest -d "Suggest next account"
|
|
1378
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a check -d "Verify account health"
|
|
1379
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a completions -d "Generate shell completions"
|
|
1380
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a moo -d "Send a test notification"
|
|
1381
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a daemon -d "Run auto-ping on a schedule"
|
|
1382
|
+
|
|
1383
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l parallel -d "Ping in parallel"
|
|
1384
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s q -l quiet -d "Suppress output"
|
|
1385
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l json -d "JSON output"
|
|
1386
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s g -l group -r -d "Filter by group"
|
|
1387
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l bell -d "Ring bell on failure"
|
|
1388
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l stagger -r -d "Delay between pings"
|
|
1389
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -a "(cc-ping list 2>/dev/null | string replace -r ' *(.*) ->.*' '$1')"
|
|
1390
|
+
|
|
1391
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from list history status next-reset check" -l json -d "JSON output"
|
|
1392
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from add" -s g -l group -r -d "Assign group"
|
|
1393
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
1394
|
+
|
|
1395
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a start -d "Start the daemon"
|
|
1396
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a stop -d "Stop the daemon"
|
|
1397
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a status -d "Show daemon status"
|
|
1398
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a install -d "Install as system service"
|
|
1399
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a uninstall -d "Remove system service"
|
|
1400
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l interval -r -d "Ping interval in minutes"
|
|
1401
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -s q -l quiet -d "Suppress output"
|
|
1402
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l bell -d "Ring bell on failure"
|
|
1403
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l notify -d "Send notification on failure"
|
|
1404
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status" -l json -d "JSON output"
|
|
1405
|
+
`;
|
|
1406
|
+
}
|
|
1407
|
+
function generateCompletion(shell) {
|
|
1408
|
+
switch (shell) {
|
|
1409
|
+
case "bash":
|
|
1410
|
+
return bashCompletion();
|
|
1411
|
+
case "zsh":
|
|
1412
|
+
return zshCompletion();
|
|
1413
|
+
case "fish":
|
|
1414
|
+
return fishCompletion();
|
|
1415
|
+
default:
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
`Unsupported shell: ${shell}. Supported: bash, zsh, fish`
|
|
1418
|
+
);
|
|
1115
1419
|
}
|
|
1116
|
-
return filtered;
|
|
1117
1420
|
}
|
|
1118
1421
|
|
|
1119
1422
|
// src/cli.ts
|
|
1120
|
-
|
|
1423
|
+
init_config();
|
|
1424
|
+
init_daemon();
|
|
1425
|
+
|
|
1426
|
+
// src/default-command.ts
|
|
1427
|
+
init_config();
|
|
1121
1428
|
|
|
1122
1429
|
// src/identity.ts
|
|
1123
1430
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -1166,76 +1473,25 @@ function findDuplicates(accounts) {
|
|
|
1166
1473
|
return result;
|
|
1167
1474
|
}
|
|
1168
1475
|
|
|
1169
|
-
// src/next-reset.ts
|
|
1170
|
-
init_state();
|
|
1171
|
-
function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
|
|
1172
|
-
let best = null;
|
|
1173
|
-
for (const account of accounts) {
|
|
1174
|
-
const window = getWindowReset(account.handle, now);
|
|
1175
|
-
if (!window) continue;
|
|
1176
|
-
if (best === null || window.remainingMs < best.remainingMs) {
|
|
1177
|
-
best = {
|
|
1178
|
-
handle: account.handle,
|
|
1179
|
-
configDir: account.configDir,
|
|
1180
|
-
remainingMs: window.remainingMs,
|
|
1181
|
-
resetAt: window.resetAt.toISOString(),
|
|
1182
|
-
timeUntilReset: formatTimeRemaining(window.remainingMs)
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
return best;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// src/cli.ts
|
|
1190
|
-
init_notify();
|
|
1191
|
-
init_paths();
|
|
1192
|
-
init_run_ping();
|
|
1193
|
-
|
|
1194
|
-
// src/scan.ts
|
|
1195
|
-
import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
|
|
1196
|
-
import { homedir as homedir2 } from "os";
|
|
1197
|
-
import { join as join8 } from "path";
|
|
1198
|
-
var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
|
|
1199
|
-
function scanAccounts() {
|
|
1200
|
-
if (!existsSync6(ACCOUNTS_DIR)) return [];
|
|
1201
|
-
return readdirSync(ACCOUNTS_DIR).filter((name) => {
|
|
1202
|
-
const full = join8(ACCOUNTS_DIR, name);
|
|
1203
|
-
return statSync2(full).isDirectory() && !name.startsWith(".");
|
|
1204
|
-
}).map((name) => ({
|
|
1205
|
-
handle: name,
|
|
1206
|
-
configDir: join8(ACCOUNTS_DIR, name)
|
|
1207
|
-
}));
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// src/stagger.ts
|
|
1211
|
-
init_state();
|
|
1212
|
-
function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
|
|
1213
|
-
if (accountCount <= 1) return 0;
|
|
1214
|
-
return Math.floor(windowMs / accountCount);
|
|
1215
|
-
}
|
|
1216
|
-
function parseStagger(value, accountCount) {
|
|
1217
|
-
if (value === "auto") {
|
|
1218
|
-
return calculateStagger(accountCount);
|
|
1219
|
-
}
|
|
1220
|
-
const minutes = Number(value);
|
|
1221
|
-
if (Number.isNaN(minutes)) {
|
|
1222
|
-
throw new Error(`Invalid stagger value: ${value}`);
|
|
1223
|
-
}
|
|
1224
|
-
if (minutes <= 0) {
|
|
1225
|
-
throw new Error("Stagger must be a positive number");
|
|
1226
|
-
}
|
|
1227
|
-
return minutes * 60 * 1e3;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
1476
|
// src/status.ts
|
|
1477
|
+
init_color();
|
|
1231
1478
|
init_config();
|
|
1232
1479
|
init_state();
|
|
1480
|
+
function colorizeStatus(windowStatus) {
|
|
1481
|
+
switch (windowStatus) {
|
|
1482
|
+
case "active":
|
|
1483
|
+
return green(windowStatus);
|
|
1484
|
+
case "needs ping":
|
|
1485
|
+
return red(windowStatus);
|
|
1486
|
+
default:
|
|
1487
|
+
return yellow(windowStatus);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1233
1490
|
function formatStatusLine(status) {
|
|
1234
1491
|
const ping = status.lastPing === null ? "never" : status.lastPing.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
1235
1492
|
const reset = status.timeUntilReset !== null ? ` (resets in ${status.timeUntilReset})` : "";
|
|
1236
|
-
const cost = status.lastCostUsd !== null && status.lastTokens !== null ? ` $${status.lastCostUsd.toFixed(4)} ${status.lastTokens} tok` : "";
|
|
1237
1493
|
const dup = status.duplicateOf ? ` [duplicate of ${status.duplicateOf}]` : "";
|
|
1238
|
-
return ` ${status.handle}: ${status.windowStatus} last ping: ${ping}${reset}${
|
|
1494
|
+
return ` ${status.handle}: ${colorizeStatus(status.windowStatus)} last ping: ${ping}${reset}${dup}`;
|
|
1239
1495
|
}
|
|
1240
1496
|
function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates) {
|
|
1241
1497
|
const dupLookup = /* @__PURE__ */ new Map();
|
|
@@ -1270,7 +1526,7 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
|
|
|
1270
1526
|
handle: account.handle,
|
|
1271
1527
|
configDir: account.configDir,
|
|
1272
1528
|
lastPing: lastPing.toISOString(),
|
|
1273
|
-
windowStatus: window ? "active" : "
|
|
1529
|
+
windowStatus: window ? "active" : "needs ping",
|
|
1274
1530
|
timeUntilReset: window ? formatTimeRemaining(window.remainingMs) : null,
|
|
1275
1531
|
lastCostUsd,
|
|
1276
1532
|
lastTokens,
|
|
@@ -1291,6 +1547,118 @@ function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date())
|
|
|
1291
1547
|
}
|
|
1292
1548
|
}
|
|
1293
1549
|
|
|
1550
|
+
// src/default-command.ts
|
|
1551
|
+
function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
|
|
1552
|
+
const accounts = listAccounts();
|
|
1553
|
+
if (accounts.length === 0) {
|
|
1554
|
+
log("No accounts configured.");
|
|
1555
|
+
log("\nGet started:");
|
|
1556
|
+
log(" cc-ping scan Auto-discover accounts");
|
|
1557
|
+
log(" cc-ping add <h> <d> Add an account manually");
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const dupes = findDuplicates(accounts);
|
|
1561
|
+
const statuses = getAccountStatuses(accounts, now, dupes);
|
|
1562
|
+
for (const s of statuses) {
|
|
1563
|
+
log(formatStatusLine(s));
|
|
1564
|
+
}
|
|
1565
|
+
const needsPing = statuses.filter((s) => s.windowStatus !== "active");
|
|
1566
|
+
if (needsPing.length > 0) {
|
|
1567
|
+
log("");
|
|
1568
|
+
log("Suggested next steps:");
|
|
1569
|
+
const handles = needsPing.map((s) => s.handle).join(" ");
|
|
1570
|
+
if (needsPing.length < statuses.length) {
|
|
1571
|
+
log(` cc-ping ping ${handles} Ping accounts that need it`);
|
|
1572
|
+
} else {
|
|
1573
|
+
log(" cc-ping ping Ping all accounts");
|
|
1574
|
+
}
|
|
1575
|
+
log(" cc-ping daemon start Auto-ping on a schedule");
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/filter-accounts.ts
|
|
1580
|
+
function filterAccounts(accounts, handles) {
|
|
1581
|
+
if (handles.length === 0) return accounts;
|
|
1582
|
+
const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
|
|
1583
|
+
if (unknown.length > 0) {
|
|
1584
|
+
throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
|
|
1585
|
+
}
|
|
1586
|
+
const set = new Set(handles);
|
|
1587
|
+
return accounts.filter((a) => set.has(a.handle));
|
|
1588
|
+
}
|
|
1589
|
+
function filterByGroup(accounts, group) {
|
|
1590
|
+
if (!group) return accounts;
|
|
1591
|
+
const filtered = accounts.filter((a) => a.group === group);
|
|
1592
|
+
if (filtered.length === 0) {
|
|
1593
|
+
throw new Error(`No accounts in group: ${group}`);
|
|
1594
|
+
}
|
|
1595
|
+
return filtered;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/cli.ts
|
|
1599
|
+
init_history();
|
|
1600
|
+
|
|
1601
|
+
// src/next-reset.ts
|
|
1602
|
+
init_state();
|
|
1603
|
+
function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
|
|
1604
|
+
let best = null;
|
|
1605
|
+
for (const account of accounts) {
|
|
1606
|
+
const window = getWindowReset(account.handle, now);
|
|
1607
|
+
if (!window) continue;
|
|
1608
|
+
if (best === null || window.remainingMs < best.remainingMs) {
|
|
1609
|
+
best = {
|
|
1610
|
+
handle: account.handle,
|
|
1611
|
+
configDir: account.configDir,
|
|
1612
|
+
remainingMs: window.remainingMs,
|
|
1613
|
+
resetAt: window.resetAt.toISOString(),
|
|
1614
|
+
timeUntilReset: formatTimeRemaining(window.remainingMs)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return best;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/cli.ts
|
|
1622
|
+
init_notify();
|
|
1623
|
+
init_paths();
|
|
1624
|
+
init_run_ping();
|
|
1625
|
+
|
|
1626
|
+
// src/scan.ts
|
|
1627
|
+
import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
|
|
1628
|
+
import { homedir as homedir2 } from "os";
|
|
1629
|
+
import { join as join8 } from "path";
|
|
1630
|
+
var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
|
|
1631
|
+
function scanAccounts() {
|
|
1632
|
+
if (!existsSync6(ACCOUNTS_DIR)) return [];
|
|
1633
|
+
return readdirSync(ACCOUNTS_DIR).filter((name) => {
|
|
1634
|
+
const full = join8(ACCOUNTS_DIR, name);
|
|
1635
|
+
return statSync2(full).isDirectory() && !name.startsWith(".");
|
|
1636
|
+
}).map((name) => ({
|
|
1637
|
+
handle: name,
|
|
1638
|
+
configDir: join8(ACCOUNTS_DIR, name)
|
|
1639
|
+
}));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/stagger.ts
|
|
1643
|
+
init_state();
|
|
1644
|
+
function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
|
|
1645
|
+
if (accountCount <= 1) return 0;
|
|
1646
|
+
return Math.floor(windowMs / accountCount);
|
|
1647
|
+
}
|
|
1648
|
+
function parseStagger(value, accountCount) {
|
|
1649
|
+
if (value === "auto") {
|
|
1650
|
+
return calculateStagger(accountCount);
|
|
1651
|
+
}
|
|
1652
|
+
const minutes = Number(value);
|
|
1653
|
+
if (Number.isNaN(minutes)) {
|
|
1654
|
+
throw new Error(`Invalid stagger value: ${value}`);
|
|
1655
|
+
}
|
|
1656
|
+
if (minutes <= 0) {
|
|
1657
|
+
throw new Error("Stagger must be a positive number");
|
|
1658
|
+
}
|
|
1659
|
+
return minutes * 60 * 1e3;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1294
1662
|
// src/suggest.ts
|
|
1295
1663
|
init_state();
|
|
1296
1664
|
function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
@@ -1322,7 +1690,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
|
1322
1690
|
}
|
|
1323
1691
|
|
|
1324
1692
|
// src/cli.ts
|
|
1325
|
-
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("
|
|
1693
|
+
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.1.0").option(
|
|
1326
1694
|
"--config <path>",
|
|
1327
1695
|
"Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
|
|
1328
1696
|
).hook("preAction", (thisCommand) => {
|
|
@@ -1330,6 +1698,8 @@ var program = new Command().name("cc-ping").description("Ping Claude Code sessio
|
|
|
1330
1698
|
if (opts.config) {
|
|
1331
1699
|
setConfigDir(opts.config);
|
|
1332
1700
|
}
|
|
1701
|
+
}).action(() => {
|
|
1702
|
+
showDefault();
|
|
1333
1703
|
});
|
|
1334
1704
|
program.command("ping").description("Ping configured accounts to start quota windows").argument("[handles...]", "Specific account handles to ping (default: all)").option("--parallel", "Ping all accounts in parallel", false).option("-q, --quiet", "Suppress all output except errors (for cron)", false).option("--json", "Output results as JSON", false).option("-g, --group <group>", "Ping only accounts in this group").option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option(
|
|
1335
1705
|
"--stagger <minutes|auto>",
|
|
@@ -1516,7 +1886,7 @@ var daemon = program.command("daemon").description("Run auto-ping on a schedule"
|
|
|
1516
1886
|
daemon.command("start").description("Start the daemon process").option(
|
|
1517
1887
|
"--interval <minutes>",
|
|
1518
1888
|
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
1519
|
-
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).action((opts) => {
|
|
1889
|
+
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).action(async (opts) => {
|
|
1520
1890
|
const result = startDaemon({
|
|
1521
1891
|
interval: opts.interval,
|
|
1522
1892
|
quiet: opts.quiet,
|
|
@@ -1528,6 +1898,13 @@ daemon.command("start").description("Start the daemon process").option(
|
|
|
1528
1898
|
process.exit(1);
|
|
1529
1899
|
}
|
|
1530
1900
|
console.log(`Daemon started (PID: ${result.pid})`);
|
|
1901
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1902
|
+
const svc = getServiceStatus2();
|
|
1903
|
+
if (!svc.installed) {
|
|
1904
|
+
console.log(
|
|
1905
|
+
"Hint: won't survive a reboot. Use `daemon install` for a persistent service."
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1531
1908
|
printAccountTable();
|
|
1532
1909
|
});
|
|
1533
1910
|
daemon.command("stop").description("Stop the daemon process").action(async () => {
|
|
@@ -1537,24 +1914,51 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
|
|
|
1537
1914
|
process.exit(1);
|
|
1538
1915
|
}
|
|
1539
1916
|
console.log(`Daemon stopped (PID: ${result.pid})`);
|
|
1917
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1918
|
+
const svc = getServiceStatus2();
|
|
1919
|
+
if (svc.installed) {
|
|
1920
|
+
console.log(
|
|
1921
|
+
"Note: system service is installed. The daemon may restart. Use `daemon uninstall` to fully remove."
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1540
1924
|
});
|
|
1541
|
-
daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action((opts) => {
|
|
1925
|
+
daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
|
|
1926
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1927
|
+
const svc = getServiceStatus2();
|
|
1542
1928
|
const status = getDaemonStatus();
|
|
1543
1929
|
if (opts.json) {
|
|
1930
|
+
const serviceInfo = svc.installed ? {
|
|
1931
|
+
service: {
|
|
1932
|
+
installed: true,
|
|
1933
|
+
path: svc.servicePath,
|
|
1934
|
+
platform: svc.platform
|
|
1935
|
+
}
|
|
1936
|
+
} : { service: { installed: false } };
|
|
1544
1937
|
if (!status.running) {
|
|
1545
|
-
console.log(JSON.stringify(status, null, 2));
|
|
1938
|
+
console.log(JSON.stringify({ ...status, ...serviceInfo }, null, 2));
|
|
1546
1939
|
return;
|
|
1547
1940
|
}
|
|
1548
1941
|
const accounts = listAccounts();
|
|
1549
1942
|
const dupes = findDuplicates(accounts);
|
|
1550
1943
|
const accountStatuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
|
|
1551
1944
|
console.log(
|
|
1552
|
-
JSON.stringify(
|
|
1945
|
+
JSON.stringify(
|
|
1946
|
+
{ ...status, ...serviceInfo, accounts: accountStatuses },
|
|
1947
|
+
null,
|
|
1948
|
+
2
|
|
1949
|
+
)
|
|
1553
1950
|
);
|
|
1554
1951
|
return;
|
|
1555
1952
|
}
|
|
1556
1953
|
if (!status.running) {
|
|
1557
|
-
|
|
1954
|
+
if (svc.installed) {
|
|
1955
|
+
const kind = svc.platform === "darwin" ? "launchd" : "systemd";
|
|
1956
|
+
console.log(
|
|
1957
|
+
`Daemon is not running (system service: installed via ${kind})`
|
|
1958
|
+
);
|
|
1959
|
+
} else {
|
|
1960
|
+
console.log("Daemon is not running");
|
|
1961
|
+
}
|
|
1558
1962
|
return;
|
|
1559
1963
|
}
|
|
1560
1964
|
console.log(`Daemon is running (PID: ${status.pid})`);
|
|
@@ -1566,15 +1970,57 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
|
|
|
1566
1970
|
if (status.nextPingIn) {
|
|
1567
1971
|
console.log(` Next ping in: ${status.nextPingIn}`);
|
|
1568
1972
|
}
|
|
1973
|
+
if (svc.installed) {
|
|
1974
|
+
const kind = svc.platform === "darwin" ? "launchd" : "systemd";
|
|
1975
|
+
console.log(` System service: installed (${kind})`);
|
|
1976
|
+
}
|
|
1569
1977
|
console.log("");
|
|
1570
1978
|
printAccountTable();
|
|
1571
1979
|
});
|
|
1980
|
+
daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
|
|
1981
|
+
"--interval <minutes>",
|
|
1982
|
+
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
1983
|
+
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).action(async (opts) => {
|
|
1984
|
+
const { installService: installService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1985
|
+
const result = await installService2({
|
|
1986
|
+
interval: opts.interval,
|
|
1987
|
+
quiet: opts.quiet,
|
|
1988
|
+
bell: opts.bell,
|
|
1989
|
+
notify: opts.notify
|
|
1990
|
+
});
|
|
1991
|
+
if (!result.success) {
|
|
1992
|
+
console.error(result.error);
|
|
1993
|
+
process.exit(1);
|
|
1994
|
+
}
|
|
1995
|
+
console.log(`Service installed: ${result.servicePath}`);
|
|
1996
|
+
console.log(
|
|
1997
|
+
"The daemon will start automatically on login. Use `daemon uninstall` to remove."
|
|
1998
|
+
);
|
|
1999
|
+
});
|
|
2000
|
+
daemon.command("uninstall").description("Remove daemon system service").action(async () => {
|
|
2001
|
+
const { uninstallService: uninstallService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
2002
|
+
const result = await uninstallService2();
|
|
2003
|
+
if (!result.success) {
|
|
2004
|
+
console.error(result.error);
|
|
2005
|
+
process.exit(1);
|
|
2006
|
+
}
|
|
2007
|
+
console.log(`Service removed: ${result.servicePath}`);
|
|
2008
|
+
});
|
|
1572
2009
|
daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping interval in milliseconds").option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).action(async (opts) => {
|
|
1573
2010
|
const intervalMs = Number(opts.intervalMs);
|
|
1574
2011
|
if (!intervalMs || intervalMs <= 0) {
|
|
1575
2012
|
console.error("Invalid --interval-ms");
|
|
1576
2013
|
process.exit(1);
|
|
1577
2014
|
}
|
|
2015
|
+
if (!readDaemonState()) {
|
|
2016
|
+
const { resolveConfigDir: resolveConfigDir2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
2017
|
+
writeDaemonState({
|
|
2018
|
+
pid: process.pid,
|
|
2019
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2020
|
+
intervalMs,
|
|
2021
|
+
configDir: resolveConfigDir2()
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
1578
2024
|
await runDaemonWithDefaults(intervalMs, {
|
|
1579
2025
|
quiet: opts.quiet,
|
|
1580
2026
|
bell: opts.bell,
|