shennian 0.2.24 → 0.2.25

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.
@@ -504,6 +504,7 @@ export function installService() {
504
504
  }
505
505
  catch {
506
506
  // Some Linux environments lack systemd user services; keep best-effort behavior.
507
+ return false;
507
508
  }
508
509
  // Enable linger so the user systemd session (and thus this service) persists
509
510
  // across reboots even without an active login session.
@@ -513,7 +514,13 @@ export function installService() {
513
514
  catch {
514
515
  // loginctl is unavailable on some distros/containers; auto-start still works after login.
515
516
  }
516
- return false; // systemd enable doesn't start immediately; caller should startDaemonProcess
517
+ try {
518
+ execSync('systemctl --user restart shennian', { stdio: 'pipe' });
519
+ return true;
520
+ }
521
+ catch {
522
+ return false;
523
+ }
517
524
  }
518
525
  case 'win32': {
519
526
  return installWindowsScheduledTask();
@@ -699,6 +706,14 @@ async function daemonRestart(opts = {}) {
699
706
  console.log(chalk.gray('● Background service not running, starting it now'));
700
707
  }
701
708
  }
709
+ if (getPlatform() === 'linux') {
710
+ const startedByService = installService();
711
+ if (startedByService) {
712
+ if (opts.json)
713
+ printJson(getDaemonStatus());
714
+ return;
715
+ }
716
+ }
702
717
  startDaemonProcess({ quiet: opts.json });
703
718
  if (opts.json)
704
719
  printJson(getDaemonStatus());
package/dist/src/index.js CHANGED
@@ -39,6 +39,25 @@ function formatDisconnectInfo(info) {
39
39
  parts.push(`error=${info.error}`);
40
40
  return parts.join(' ');
41
41
  }
42
+ async function waitForPidExit(pid, timeoutMs = 5000) {
43
+ const startedAt = Date.now();
44
+ while (Date.now() - startedAt < timeoutMs) {
45
+ try {
46
+ process.kill(pid, 0);
47
+ }
48
+ catch {
49
+ return true;
50
+ }
51
+ await new Promise((resolve) => setTimeout(resolve, 100));
52
+ }
53
+ try {
54
+ process.kill(pid, 0);
55
+ return false;
56
+ }
57
+ catch {
58
+ return true;
59
+ }
60
+ }
42
61
  const program = new Command();
43
62
  program.name('shennian').description('Shennian — AI Agent Mobile Console').version(cliVersion);
44
63
  program
@@ -70,15 +89,27 @@ program
70
89
  catch {
71
90
  // env.json may not exist yet
72
91
  }
73
- // Single-instance guard: kill any existing daemon before starting
92
+ // Single-instance guard. systemd-managed starts are authoritative and may
93
+ // need to take over from an older detached `shennian start` process.
74
94
  const pidFile = resolveShennianPath('daemon.pid');
75
95
  try {
76
96
  const oldPid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
77
97
  if (oldPid && oldPid !== process.pid) {
78
98
  try {
79
99
  process.kill(oldPid, 0);
80
- console.log(`[${new Date().toISOString()}] daemon already running (PID ${oldPid}), skipping duplicate start`);
81
- process.exit(0);
100
+ if (process.env.INVOCATION_ID || process.env.JOURNAL_STREAM) {
101
+ console.log(`[${new Date().toISOString()}] systemd start taking over from existing daemon (PID ${oldPid})`);
102
+ process.kill(oldPid, 'SIGTERM');
103
+ const stopped = await waitForPidExit(oldPid);
104
+ if (!stopped) {
105
+ process.kill(oldPid, 'SIGKILL');
106
+ await waitForPidExit(oldPid, 2000);
107
+ }
108
+ }
109
+ else {
110
+ console.log(`[${new Date().toISOString()}] daemon already running (PID ${oldPid}), skipping duplicate start`);
111
+ process.exit(0);
112
+ }
82
113
  }
83
114
  catch {
84
115
  // Stale pid; continue booting and overwrite it below.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Shennian — AI Agent Mobile Console CLI",
5
5
  "type": "module",
6
6
  "bin": {