shennian 0.2.13 → 0.2.15
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 +8 -7
- package/dist/bin/shennian.js +0 -0
- package/dist/src/commands/daemon.d.ts +3 -0
- package/dist/src/commands/daemon.js +171 -48
- package/dist/src/commands/upgrade.d.ts +11 -0
- package/dist/src/commands/upgrade.js +52 -3
- package/dist/src/index.js +10 -5
- package/dist/src/native-fusion/service.js +13 -2
- package/dist/src/native-fusion/types.d.ts +1 -0
- package/dist/src/session/manager.js +10 -6
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @shennian/cli
|
|
2
2
|
|
|
3
|
-
> Shennian AI Agent
|
|
3
|
+
> Shennian AI Agent 移动控制台的本机后台服务 / CLI
|
|
4
4
|
>
|
|
5
5
|
> **架构设计** → [docs/architecture/cli/](../../docs/architecture/cli/)
|
|
6
6
|
> - [Daemon 架构](../../docs/architecture/cli/daemon.md) — 模块拓扑、数据流、守护进程
|
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
| 命令 | 说明 |
|
|
15
15
|
|------|------|
|
|
16
|
-
| `shennian` | 智能启动:必要时 pair
|
|
16
|
+
| `shennian` | 智能启动:必要时 pair 再启动后台服务 |
|
|
17
17
|
| `shennian pair` | 重新与账号 / server 配对 |
|
|
18
|
-
| `shennian
|
|
19
|
-
| `shennian
|
|
20
|
-
| `shennian
|
|
21
|
-
| `shennian
|
|
22
|
-
| `shennian daemon logs -n 100` | 查看最近日志 |
|
|
18
|
+
| `shennian start` | 启动后台服务 |
|
|
19
|
+
| `shennian stop` | 停止后台服务 |
|
|
20
|
+
| `shennian status` | 查看后台服务状态 |
|
|
21
|
+
| `shennian logs -n 100` | 查看最近日志 |
|
|
23
22
|
| `shennian config` | 查看本地配置 |
|
|
24
23
|
| `shennian agent add/list/remove` | 管理自定义 Agent |
|
|
25
24
|
| `shennian upgrade` | 升级 CLI |
|
|
26
25
|
|
|
26
|
+
`start`、`stop`、`status` 可选加 `--json`。`logs` 输出日志文本,不支持 `--json`。
|
|
27
|
+
|
|
27
28
|
## 开发
|
|
28
29
|
|
|
29
30
|
```bash
|
package/dist/bin/shennian.js
CHANGED
|
File without changes
|
|
@@ -5,6 +5,7 @@ export type DaemonStatus = {
|
|
|
5
5
|
running: boolean;
|
|
6
6
|
pid: number | null;
|
|
7
7
|
stale: boolean;
|
|
8
|
+
remoteAccessDisabled: boolean;
|
|
8
9
|
platform: Platform;
|
|
9
10
|
shennianDir: string;
|
|
10
11
|
pidFile: string;
|
|
@@ -21,7 +22,9 @@ export declare function resolveServiceLaunchSpec(input: {
|
|
|
21
22
|
scriptPath: string;
|
|
22
23
|
shennianCommandPath?: string | null;
|
|
23
24
|
npxPath?: string | null;
|
|
25
|
+
desktopBridgePath?: string | null;
|
|
24
26
|
}): ServiceLaunchSpec;
|
|
27
|
+
export declare function isRemoteAccessDisabled(): boolean;
|
|
25
28
|
export declare function getDaemonStatus(opts?: {
|
|
26
29
|
cleanupStale?: boolean;
|
|
27
30
|
}): DaemonStatus;
|
|
@@ -11,6 +11,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
const SHENNIAN_DIR = getShennianDir();
|
|
12
12
|
const PID_FILE = resolveShennianPath('daemon.pid');
|
|
13
13
|
const LOG_FILE = resolveShennianPath('daemon.log');
|
|
14
|
+
const REMOTE_ACCESS_DISABLED_FILE = resolveShennianPath('remote-access.disabled');
|
|
14
15
|
const SHENNIAN_SCRIPT = path.resolve(__dirname, '../../bin/shennian.js');
|
|
15
16
|
const NODE_EXEC = process.execPath;
|
|
16
17
|
function getPlatform() {
|
|
@@ -29,29 +30,38 @@ export function isEphemeralCliPath(candidate) {
|
|
|
29
30
|
}
|
|
30
31
|
export function resolveServiceLaunchSpec(input) {
|
|
31
32
|
if (fs.existsSync(input.scriptPath) && !isEphemeralCliPath(input.scriptPath)) {
|
|
33
|
+
if (process.env.ELECTRON_RUN_AS_NODE === '1' &&
|
|
34
|
+
input.desktopBridgePath &&
|
|
35
|
+
fs.existsSync(input.desktopBridgePath)) {
|
|
36
|
+
return {
|
|
37
|
+
command: input.nodeExec,
|
|
38
|
+
args: [input.desktopBridgePath, input.scriptPath, 'run-service'],
|
|
39
|
+
mode: 'direct',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
32
42
|
return {
|
|
33
43
|
command: input.nodeExec,
|
|
34
|
-
args: [input.scriptPath, '
|
|
44
|
+
args: [input.scriptPath, 'run-service'],
|
|
35
45
|
mode: 'direct',
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
if (input.shennianCommandPath && !isEphemeralCliPath(input.shennianCommandPath)) {
|
|
39
49
|
return {
|
|
40
50
|
command: input.shennianCommandPath,
|
|
41
|
-
args: ['
|
|
51
|
+
args: ['run-service'],
|
|
42
52
|
mode: 'global-shim',
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
if (input.npxPath) {
|
|
46
56
|
return {
|
|
47
57
|
command: input.npxPath,
|
|
48
|
-
args: ['--yes', 'shennian', '
|
|
58
|
+
args: ['--yes', 'shennian', 'run-service'],
|
|
49
59
|
mode: 'npx',
|
|
50
60
|
};
|
|
51
61
|
}
|
|
52
62
|
return {
|
|
53
63
|
command: input.nodeExec,
|
|
54
|
-
args: [input.scriptPath, '
|
|
64
|
+
args: [input.scriptPath, 'run-service'],
|
|
55
65
|
mode: 'direct',
|
|
56
66
|
};
|
|
57
67
|
}
|
|
@@ -75,6 +85,9 @@ function isRunning(pid) {
|
|
|
75
85
|
return false;
|
|
76
86
|
}
|
|
77
87
|
}
|
|
88
|
+
export function isRemoteAccessDisabled() {
|
|
89
|
+
return fs.existsSync(REMOTE_ACCESS_DISABLED_FILE);
|
|
90
|
+
}
|
|
78
91
|
export function getDaemonStatus(opts = {}) {
|
|
79
92
|
const pid = readPid();
|
|
80
93
|
const running = pid !== null && isRunning(pid);
|
|
@@ -91,6 +104,7 @@ export function getDaemonStatus(opts = {}) {
|
|
|
91
104
|
running,
|
|
92
105
|
pid,
|
|
93
106
|
stale,
|
|
107
|
+
remoteAccessDisabled: isRemoteAccessDisabled(),
|
|
94
108
|
platform: getPlatform(),
|
|
95
109
|
shennianDir: SHENNIAN_DIR,
|
|
96
110
|
pidFile: PID_FILE,
|
|
@@ -138,6 +152,7 @@ function resolveCurrentServiceLaunchSpec() {
|
|
|
138
152
|
scriptPath: SHENNIAN_SCRIPT,
|
|
139
153
|
shennianCommandPath: findCommandPath('shennian'),
|
|
140
154
|
npxPath: resolveNpxPath(),
|
|
155
|
+
desktopBridgePath: process.env.SHENNIAN_DESKTOP_CLI_BRIDGE,
|
|
141
156
|
});
|
|
142
157
|
}
|
|
143
158
|
function escapeXml(text) {
|
|
@@ -322,6 +337,10 @@ export function captureEnvForService() {
|
|
|
322
337
|
'OPENAI_BASE_URL',
|
|
323
338
|
'GEMINI_API_KEY',
|
|
324
339
|
'GOOGLE_API_KEY',
|
|
340
|
+
'ELECTRON_RUN_AS_NODE',
|
|
341
|
+
'SHENNIAN_DESKTOP_CLI_SCRIPT',
|
|
342
|
+
'SHENNIAN_DESKTOP_CLI_BRIDGE',
|
|
343
|
+
'SHENNIAN_HOME',
|
|
325
344
|
];
|
|
326
345
|
const env = {};
|
|
327
346
|
for (const k of keep) {
|
|
@@ -502,21 +521,10 @@ export function installService() {
|
|
|
502
521
|
}
|
|
503
522
|
}
|
|
504
523
|
// ─── Subcommand implementations ──────────────────────────────────────────────
|
|
505
|
-
function
|
|
506
|
-
startDaemonProcess({ quiet: opts.json });
|
|
507
|
-
if (opts.json)
|
|
508
|
-
printJson(getDaemonStatus());
|
|
509
|
-
}
|
|
510
|
-
function daemonStop(opts = {}) {
|
|
524
|
+
function stopDaemonProcess() {
|
|
511
525
|
const pid = readPid();
|
|
512
526
|
if (pid === null) {
|
|
513
|
-
|
|
514
|
-
printJson(getDaemonStatus());
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
console.log(chalk.yellow('⚠ No running background service found'));
|
|
518
|
-
}
|
|
519
|
-
return;
|
|
527
|
+
return {};
|
|
520
528
|
}
|
|
521
529
|
if (!isRunning(pid)) {
|
|
522
530
|
try {
|
|
@@ -525,38 +533,115 @@ function daemonStop(opts = {}) {
|
|
|
525
533
|
catch {
|
|
526
534
|
// Stale pid file is already gone.
|
|
527
535
|
}
|
|
528
|
-
|
|
529
|
-
printJson(getDaemonStatus());
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
console.log(chalk.yellow('⚠ Service process no longer exists, cleaned up'));
|
|
533
|
-
}
|
|
534
|
-
return;
|
|
536
|
+
return { stalePid: pid };
|
|
535
537
|
}
|
|
536
538
|
try {
|
|
537
539
|
process.kill(pid, 'SIGTERM');
|
|
540
|
+
return { stoppedPid: pid };
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function stopDaemonProcessAndWait(timeoutMs = 5000) {
|
|
547
|
+
const result = stopDaemonProcess();
|
|
548
|
+
if (!result.stoppedPid)
|
|
549
|
+
return result;
|
|
550
|
+
const stopped = await waitForPidExit(result.stoppedPid, timeoutMs);
|
|
551
|
+
if (!stopped) {
|
|
538
552
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
catch {
|
|
542
|
-
// Best-effort cleanup after SIGTERM.
|
|
543
|
-
}
|
|
544
|
-
if (opts.json) {
|
|
545
|
-
printJson(getDaemonStatus());
|
|
553
|
+
process.kill(result.stoppedPid, 'SIGKILL');
|
|
554
|
+
await waitForPidExit(result.stoppedPid, 2000);
|
|
546
555
|
}
|
|
547
|
-
|
|
548
|
-
|
|
556
|
+
catch (err) {
|
|
557
|
+
return { ...result, error: err instanceof Error ? err.message : String(err) };
|
|
549
558
|
}
|
|
550
559
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
560
|
+
try {
|
|
561
|
+
fs.unlinkSync(PID_FILE);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Best-effort cleanup after the process exits.
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
function enableRemoteAccess(opts = {}) {
|
|
569
|
+
try {
|
|
570
|
+
fs.unlinkSync(REMOTE_ACCESS_DISABLED_FILE);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// Remote access may already be enabled.
|
|
574
|
+
}
|
|
575
|
+
const startedByService = installService();
|
|
576
|
+
if (!startedByService) {
|
|
577
|
+
startDaemonProcess({ quiet: opts.json });
|
|
578
|
+
}
|
|
579
|
+
if (opts.json) {
|
|
580
|
+
printJson(getDaemonStatus());
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
console.log(chalk.green('✓ Background service started'));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function disableRemoteAccess(opts = {}) {
|
|
587
|
+
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
588
|
+
fs.writeFileSync(REMOTE_ACCESS_DISABLED_FILE, new Date().toISOString());
|
|
589
|
+
const platform = getPlatform();
|
|
590
|
+
switch (platform) {
|
|
591
|
+
case 'darwin': {
|
|
592
|
+
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
593
|
+
try {
|
|
594
|
+
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// The job may already be unloaded; keep the plist for future re-enable.
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
break;
|
|
555
601
|
}
|
|
556
|
-
|
|
557
|
-
|
|
602
|
+
case 'linux': {
|
|
603
|
+
try {
|
|
604
|
+
execSync('systemctl --user disable --now shennian', { stdio: 'pipe' });
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// systemd user services may be unavailable; still stop the manual daemon.
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case 'win32': {
|
|
612
|
+
try {
|
|
613
|
+
execSync(`schtasks /change /tn "${WINDOWS_TASK_NAME}" /disable`, { stdio: 'pipe' });
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// Task may not exist yet; still stop the manual daemon.
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
execSync(`schtasks /end /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe' });
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// Task may not be running.
|
|
623
|
+
}
|
|
624
|
+
break;
|
|
558
625
|
}
|
|
559
626
|
}
|
|
627
|
+
const result = await stopDaemonProcessAndWait();
|
|
628
|
+
const status = getDaemonStatus({ cleanupStale: true });
|
|
629
|
+
if (opts.json) {
|
|
630
|
+
printJson(result.error ? { ...status, error: result.error } : status);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (result.error) {
|
|
634
|
+
console.error(chalk.red(`✗ Failed to stop: ${result.error}`));
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
console.log(chalk.green('✓ Background service stopped'));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function daemonStart(opts) {
|
|
641
|
+
enableRemoteAccess(opts);
|
|
642
|
+
}
|
|
643
|
+
async function daemonStop(opts = {}) {
|
|
644
|
+
await disableRemoteAccess(opts);
|
|
560
645
|
}
|
|
561
646
|
async function waitForPidExit(pid, timeoutMs = 5000) {
|
|
562
647
|
const startedAt = Date.now();
|
|
@@ -652,8 +737,8 @@ function daemonLogs(opts) {
|
|
|
652
737
|
console.log(lines.join('\n'));
|
|
653
738
|
}
|
|
654
739
|
}
|
|
655
|
-
function daemonUninstall() {
|
|
656
|
-
daemonStop();
|
|
740
|
+
async function daemonUninstall() {
|
|
741
|
+
await daemonStop();
|
|
657
742
|
const platform = getPlatform();
|
|
658
743
|
switch (platform) {
|
|
659
744
|
case 'darwin': {
|
|
@@ -716,32 +801,70 @@ function daemonUninstall() {
|
|
|
716
801
|
}
|
|
717
802
|
// ─── Register command ─────────────────────────────────────────────────────────
|
|
718
803
|
export function registerDaemonCommand(program) {
|
|
719
|
-
|
|
720
|
-
program.command('restart').description('Restart the background service').action(() => daemonRestart());
|
|
721
|
-
daemon
|
|
804
|
+
program
|
|
722
805
|
.command('start')
|
|
723
806
|
.description('Start the background service')
|
|
724
807
|
.option('--json', 'Print machine-readable status JSON')
|
|
725
808
|
.action((opts) => daemonStart(opts));
|
|
726
|
-
|
|
809
|
+
program
|
|
727
810
|
.command('stop')
|
|
728
811
|
.description('Stop the background service')
|
|
729
812
|
.option('--json', 'Print machine-readable status JSON')
|
|
730
813
|
.action((opts) => daemonStop(opts));
|
|
814
|
+
program
|
|
815
|
+
.command('status')
|
|
816
|
+
.description('Show background service status')
|
|
817
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
818
|
+
.action((opts) => daemonStatus(opts));
|
|
819
|
+
program
|
|
820
|
+
.command('logs')
|
|
821
|
+
.description('Show recent logs')
|
|
822
|
+
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
823
|
+
.action((opts) => daemonLogs({ lines: parseInt(opts.lines, 10) }));
|
|
824
|
+
const daemon = program.command('daemon', { hidden: true }).description('Deprecated: use top-level start/stop/status/logs');
|
|
825
|
+
const warnDeprecated = (replacement, json) => {
|
|
826
|
+
if (!json)
|
|
827
|
+
console.error(chalk.yellow(`⚠ Deprecated command. Use \`shennian ${replacement}\` instead.`));
|
|
828
|
+
};
|
|
829
|
+
daemon
|
|
830
|
+
.command('start')
|
|
831
|
+
.description('Start the background service')
|
|
832
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
833
|
+
.action((opts) => {
|
|
834
|
+
warnDeprecated('start', opts.json);
|
|
835
|
+
daemonStart(opts);
|
|
836
|
+
});
|
|
837
|
+
daemon
|
|
838
|
+
.command('stop')
|
|
839
|
+
.description('Stop the background service')
|
|
840
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
841
|
+
.action(async (opts) => {
|
|
842
|
+
warnDeprecated('stop', opts.json);
|
|
843
|
+
await daemonStop(opts);
|
|
844
|
+
});
|
|
731
845
|
daemon
|
|
732
846
|
.command('restart')
|
|
733
847
|
.description('Restart the background service')
|
|
734
848
|
.option('--json', 'Print machine-readable status JSON')
|
|
735
|
-
.action((opts) =>
|
|
849
|
+
.action((opts) => {
|
|
850
|
+
warnDeprecated('stop && shennian start', opts.json);
|
|
851
|
+
daemonRestart(opts);
|
|
852
|
+
});
|
|
736
853
|
daemon
|
|
737
854
|
.command('status')
|
|
738
855
|
.description('Show background service status')
|
|
739
856
|
.option('--json', 'Print machine-readable status JSON')
|
|
740
|
-
.action((opts) =>
|
|
857
|
+
.action((opts) => {
|
|
858
|
+
warnDeprecated('status', opts.json);
|
|
859
|
+
daemonStatus(opts);
|
|
860
|
+
});
|
|
741
861
|
daemon
|
|
742
862
|
.command('logs')
|
|
743
863
|
.description('Show recent logs')
|
|
744
864
|
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
745
|
-
.action((opts) =>
|
|
865
|
+
.action((opts) => {
|
|
866
|
+
warnDeprecated('logs');
|
|
867
|
+
daemonLogs({ lines: parseInt(opts.lines, 10) });
|
|
868
|
+
});
|
|
746
869
|
daemon.command('uninstall').description('Uninstall auto-start service').action(daemonUninstall);
|
|
747
870
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process';
|
|
1
2
|
import type { Command } from 'commander';
|
|
2
3
|
import type { CliRelayClient } from '../relay/client.js';
|
|
4
|
+
type RestartAfterUpgradeDeps = {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
kill?: typeof process.kill;
|
|
7
|
+
pid?: number;
|
|
8
|
+
spawn?: typeof spawn;
|
|
9
|
+
execSync?: typeof execSync;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
};
|
|
12
|
+
export declare function restartCurrentDaemonAfterUpgrade(deps?: RestartAfterUpgradeDeps): void;
|
|
3
13
|
export declare function registerUpgradeCommand(program: Command): void;
|
|
4
14
|
export declare function handleUpgradeStart(client: CliRelayClient, reqId: string, targetVersion?: string): Promise<void>;
|
|
5
15
|
export declare function handleUpgradeStatus(client: CliRelayClient, reqId: string): Promise<void>;
|
|
16
|
+
export {};
|
|
@@ -1,7 +1,57 @@
|
|
|
1
1
|
// @arch docs/architecture/cli/upgrade.md
|
|
2
|
+
// @test packages/cli/src/__tests__/upgrade-command.test.ts
|
|
2
3
|
import chalk from 'chalk';
|
|
4
|
+
import { execSync, spawn } from 'node:child_process';
|
|
5
|
+
import os from 'node:os';
|
|
3
6
|
import { getCurrentVersion, fetchLatestVersion, compareVersions, performUpgrade, checkForUpdate, } from '../upgrade/engine.js';
|
|
4
7
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
8
|
+
export function restartCurrentDaemonAfterUpgrade(deps = {}) {
|
|
9
|
+
const platform = deps.platform ?? os.platform();
|
|
10
|
+
const kill = deps.kill ?? process.kill;
|
|
11
|
+
const currentPid = deps.pid ?? process.pid;
|
|
12
|
+
const env = deps.env ?? process.env;
|
|
13
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
14
|
+
const execSyncFn = deps.execSync ?? execSync;
|
|
15
|
+
if (platform === 'win32') {
|
|
16
|
+
const command = env.ComSpec || 'cmd.exe';
|
|
17
|
+
const delayedStart = [
|
|
18
|
+
'timeout /t 2 /nobreak >nul',
|
|
19
|
+
'wscript "%USERPROFILE%\\.shennian\\autostart.vbs"',
|
|
20
|
+
].join(' & ');
|
|
21
|
+
try {
|
|
22
|
+
const child = spawnFn(command, ['/d', '/s', '/c', `"${delayedStart}"`], {
|
|
23
|
+
detached: true,
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
child.unref();
|
|
28
|
+
kill(currentPid, 'SIGTERM');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
try {
|
|
33
|
+
execSyncFn('schtasks /run /tn "ShennianAgent"', { stdio: 'pipe' });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Fall through to process exit; manual start/status still works.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
const child = spawnFn('sh', ['-c', 'sleep 2; shennian start >/dev/null 2>&1'], {
|
|
43
|
+
detached: true,
|
|
44
|
+
stdio: 'ignore',
|
|
45
|
+
env,
|
|
46
|
+
});
|
|
47
|
+
child.unref();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// launchd/systemd may still restart this process; otherwise manual start/status still works.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
kill(currentPid, 'SIGTERM');
|
|
54
|
+
}
|
|
5
55
|
// ─── Interactive CLI upgrade command ─────────────────────────────────────────
|
|
6
56
|
export function registerUpgradeCommand(program) {
|
|
7
57
|
program
|
|
@@ -67,8 +117,7 @@ export function registerUpgradeCommand(program) {
|
|
|
67
117
|
console.log();
|
|
68
118
|
console.log(chalk.green(`✓ Upgraded ${result.from} → ${result.to}`));
|
|
69
119
|
console.log(chalk.gray(' Daemon is restarting...'));
|
|
70
|
-
|
|
71
|
-
process.kill(process.pid, 'SIGTERM');
|
|
120
|
+
restartCurrentDaemonAfterUpgrade();
|
|
72
121
|
}
|
|
73
122
|
else {
|
|
74
123
|
console.log();
|
|
@@ -122,7 +171,7 @@ export async function handleUpgradeStart(client, reqId, targetVersion) {
|
|
|
122
171
|
sendEvent({ state: 'restarting', machineId: 'self', from: result.from, to: result.to });
|
|
123
172
|
// Brief delay so the event is flushed before we exit
|
|
124
173
|
await new Promise((r) => setTimeout(r, 500));
|
|
125
|
-
|
|
174
|
+
restartCurrentDaemonAfterUpgrade();
|
|
126
175
|
}
|
|
127
176
|
else {
|
|
128
177
|
sendEvent({
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @arch docs/architecture/cli/daemon.md
|
|
2
2
|
// @test src/__tests__/agents-e2e.ts
|
|
3
|
+
// @test src/__tests__/daemon-autostart.test.ts
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import os from 'node:os';
|
|
@@ -7,7 +8,7 @@ import fs from 'node:fs';
|
|
|
7
8
|
import { loadConfig, saveConfig, configPath, getShennianDir, resolveShennianPath, } from './config/index.js';
|
|
8
9
|
import { CliRelayClient } from './relay/client.js';
|
|
9
10
|
import { registerPairCommand, runSmartStart } from './commands/pair.js';
|
|
10
|
-
import { registerDaemonCommand } from './commands/daemon.js';
|
|
11
|
+
import { isRemoteAccessDisabled, registerDaemonCommand } from './commands/daemon.js';
|
|
11
12
|
import { registerAgentCommand } from './commands/agent.js';
|
|
12
13
|
import { registerUpgradeCommand } from './commands/upgrade.js';
|
|
13
14
|
import { SessionManager } from './session/manager.js';
|
|
@@ -46,12 +47,16 @@ program
|
|
|
46
47
|
const serverUrl = opts.api ?? config.serverUrl ?? undefined;
|
|
47
48
|
await runSmartStart(serverUrl, opts.name);
|
|
48
49
|
});
|
|
49
|
-
//
|
|
50
|
+
// run-service: internal command used by the background service process
|
|
50
51
|
program
|
|
51
|
-
.command('
|
|
52
|
-
.description('(internal) Connect to relay server, called by
|
|
52
|
+
.command('run-service', { hidden: true })
|
|
53
|
+
.description('(internal) Connect to relay server, called by the background service')
|
|
53
54
|
.option('--api <url>', 'Server URL override')
|
|
54
55
|
.action(async (opts) => {
|
|
56
|
+
if (isRemoteAccessDisabled()) {
|
|
57
|
+
console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
55
60
|
const envFile = resolveShennianPath('env.json');
|
|
56
61
|
try {
|
|
57
62
|
const saved = JSON.parse(fs.readFileSync(envFile, 'utf-8'));
|
|
@@ -261,7 +266,7 @@ configCmd
|
|
|
261
266
|
config.serverUrl = regionToUrl(region);
|
|
262
267
|
saveConfig(config);
|
|
263
268
|
console.log(chalk.green(`✓ Server set to ${SERVERS[region].label}`));
|
|
264
|
-
console.log(chalk.yellow(' Restart the
|
|
269
|
+
console.log(chalk.yellow(' Restart the background service for this to take effect: shennian stop && shennian start'));
|
|
265
270
|
return;
|
|
266
271
|
}
|
|
267
272
|
console.error(chalk.red(`✗ Unknown config key: ${key}. Supported: server`));
|
|
@@ -11,6 +11,12 @@ const MAX_BATCH_SIZE = 20;
|
|
|
11
11
|
function normalizeText(text) {
|
|
12
12
|
return text.replace(/\r\n/g, '\n').trim();
|
|
13
13
|
}
|
|
14
|
+
function isCodexRolloutPath(filePath) {
|
|
15
|
+
return filePath.split(/[\\/]+/).includes('.codex');
|
|
16
|
+
}
|
|
17
|
+
function shouldBackfillWindowsCodex(state) {
|
|
18
|
+
return process.platform === 'win32' && state.codexWindowsPathParserFixed !== true;
|
|
19
|
+
}
|
|
14
20
|
export class NativeSessionFusionService {
|
|
15
21
|
client;
|
|
16
22
|
timer = null;
|
|
@@ -85,13 +91,15 @@ export class NativeSessionFusionService {
|
|
|
85
91
|
this.pruneState();
|
|
86
92
|
const state = loadNativeScannerState();
|
|
87
93
|
const nextFilesState = { ...state.files };
|
|
94
|
+
const backfillWindowsCodex = shouldBackfillWindowsCodex(state);
|
|
88
95
|
const batches = [];
|
|
89
96
|
const files = [...listCodexRolloutFiles(), ...listClaudeTranscriptFiles()];
|
|
90
97
|
for (const filePath of files) {
|
|
91
98
|
const stat = fs.statSync(filePath);
|
|
92
99
|
const current = state.files[filePath];
|
|
93
|
-
const
|
|
94
|
-
const
|
|
100
|
+
const isCodex = isCodexRolloutPath(filePath);
|
|
101
|
+
const startOffset = backfillWindowsCodex && isCodex ? 0 : current?.offset ?? 0;
|
|
102
|
+
const parsed = isCodex
|
|
95
103
|
? parseCodexRolloutChunk(filePath, startOffset)
|
|
96
104
|
: parseClaudeTranscriptChunk(filePath, startOffset);
|
|
97
105
|
nextFilesState[filePath] = {
|
|
@@ -121,6 +129,9 @@ export class NativeSessionFusionService {
|
|
|
121
129
|
}, 60_000);
|
|
122
130
|
}
|
|
123
131
|
state.files = nextFilesState;
|
|
132
|
+
if (backfillWindowsCodex) {
|
|
133
|
+
state.codexWindowsPathParserFixed = true;
|
|
134
|
+
}
|
|
124
135
|
saveNativeScannerState(state);
|
|
125
136
|
}
|
|
126
137
|
tryClaimManagedEcho(event) {
|
|
@@ -22,24 +22,28 @@ const MAX_SESSIONS = 50;
|
|
|
22
22
|
function isWindowsAbsolutePath(input) {
|
|
23
23
|
return /^[A-Za-z]:([\\/]|$)/.test(input) || /^\\\\[^\\]+\\[^\\]+/.test(input);
|
|
24
24
|
}
|
|
25
|
+
function normalizeWindowsAbsolutePath(input) {
|
|
26
|
+
return input.replace(/^[/\\]([A-Za-z]:[\\/].*)$/, '$1');
|
|
27
|
+
}
|
|
25
28
|
export function resolveSessionWorkDir(input) {
|
|
26
29
|
if (!input)
|
|
27
30
|
return os.homedir();
|
|
31
|
+
const normalizedInput = normalizeWindowsAbsolutePath(input);
|
|
28
32
|
const joinPath = process.platform === 'win32' ? path.win32.join : path.join;
|
|
29
|
-
if (
|
|
30
|
-
return joinPath(os.homedir(),
|
|
33
|
+
if (normalizedInput.startsWith('~')) {
|
|
34
|
+
return joinPath(os.homedir(), normalizedInput.slice(1).replace(/^[/\\]+/, ''));
|
|
31
35
|
}
|
|
32
36
|
if (process.platform === 'win32') {
|
|
33
|
-
const normalized =
|
|
37
|
+
const normalized = normalizedInput.replace(/\\/g, '/');
|
|
34
38
|
if (normalized === '/tmp' || normalized.startsWith('/tmp/')) {
|
|
35
39
|
const suffix = normalized.slice('/tmp'.length).replace(/^\/+/, '');
|
|
36
40
|
return suffix ? path.win32.join(os.tmpdir(), ...suffix.split('/')) : os.tmpdir();
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
if (isWindowsAbsolutePath(
|
|
40
|
-
return path.win32.resolve(
|
|
43
|
+
if (isWindowsAbsolutePath(normalizedInput)) {
|
|
44
|
+
return path.win32.resolve(normalizedInput);
|
|
41
45
|
}
|
|
42
|
-
return path.resolve(
|
|
46
|
+
return path.resolve(normalizedInput);
|
|
43
47
|
}
|
|
44
48
|
export class SessionManager {
|
|
45
49
|
client;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shennian",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Shennian — AI Agent Mobile Console CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,20 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json",
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"prepublishOnly": "pnpm build:publish"
|
|
40
|
+
},
|
|
35
41
|
"dependencies": {
|
|
36
42
|
"@mariozechner/pi-agent-core": "^0.64.0",
|
|
37
43
|
"@sinclair/typebox": "^0.34.49",
|
|
44
|
+
"@shennian/wire": "workspace:*",
|
|
38
45
|
"chalk": "^5.4.1",
|
|
39
46
|
"commander": "^13.1.0",
|
|
40
47
|
"qrcode-terminal": "^0.12.0",
|
|
41
|
-
"ws": "^8.18.1"
|
|
42
|
-
"@shennian/wire": "0.1.2"
|
|
48
|
+
"ws": "^8.18.1"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
51
|
"@types/node": "^20",
|
|
@@ -47,10 +53,5 @@
|
|
|
47
53
|
"@types/ws": "^8.18.1",
|
|
48
54
|
"tsx": "^4.19.4",
|
|
49
55
|
"typescript": "^5.9.3"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"build": "tsc",
|
|
53
|
-
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json",
|
|
54
|
-
"dev": "tsc --watch"
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|