@jackwener/opencli 1.5.6 → 1.5.7
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/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -1
- package/SKILL.md +879 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +7 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.js +22 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +544 -137
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +108 -2
- package/dist/clis/weread/commands.test.js +262 -152
- package/dist/clis/weread/utils.d.ts +10 -0
- package/dist/clis/weread/utils.js +27 -7
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/commanderAdapter.js +9 -0
- package/dist/commanderAdapter.test.js +25 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/index.md +67 -66
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +174 -10
- package/extension/src/cdp.ts +12 -0
- package/extension/src/protocol.ts +7 -5
- package/package.json +1 -1
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +18 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +142 -2
- package/src/clis/weread/commands.test.ts +314 -154
- package/src/clis/weread/utils.ts +33 -4
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/commanderAdapter.test.ts +30 -0
- package/src/commanderAdapter.ts +7 -0
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- package/src/clis/chatwise/shared.ts +0 -8
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('chalk', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
green: (s: string) => s,
|
|
6
|
+
yellow: (s: string) => s,
|
|
7
|
+
red: (s: string) => s,
|
|
8
|
+
dim: (s: string) => s,
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockConnect = vi.fn();
|
|
13
|
+
vi.mock('../browser/mcp.js', () => ({
|
|
14
|
+
BrowserBridge: class {
|
|
15
|
+
connect = mockConnect;
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
|
|
20
|
+
|
|
21
|
+
describe('daemon commands', () => {
|
|
22
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
23
|
+
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
27
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
mockConnect.mockReset();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('daemonStatus', () => {
|
|
36
|
+
it('shows "not running" when daemon is unreachable', async () => {
|
|
37
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
38
|
+
|
|
39
|
+
await daemonStatus();
|
|
40
|
+
|
|
41
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('shows "not running" when daemon returns non-ok response', async () => {
|
|
45
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
|
|
46
|
+
|
|
47
|
+
await daemonStatus();
|
|
48
|
+
|
|
49
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('shows daemon info when running', async () => {
|
|
53
|
+
const status = {
|
|
54
|
+
ok: true,
|
|
55
|
+
pid: 12345,
|
|
56
|
+
uptime: 3661,
|
|
57
|
+
extensionConnected: true,
|
|
58
|
+
pending: 0,
|
|
59
|
+
lastCliRequestTime: Date.now() - 30_000,
|
|
60
|
+
memoryMB: 64,
|
|
61
|
+
port: 19825,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
vi.stubGlobal(
|
|
65
|
+
'fetch',
|
|
66
|
+
vi.fn().mockResolvedValue({
|
|
67
|
+
ok: true,
|
|
68
|
+
json: () => Promise.resolve(status),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await daemonStatus();
|
|
73
|
+
|
|
74
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
75
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
|
|
76
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
|
|
77
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
78
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
|
|
79
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('shows disconnected when extension is not connected', async () => {
|
|
83
|
+
const status = {
|
|
84
|
+
ok: true,
|
|
85
|
+
pid: 99,
|
|
86
|
+
uptime: 120,
|
|
87
|
+
extensionConnected: false,
|
|
88
|
+
pending: 0,
|
|
89
|
+
lastCliRequestTime: Date.now() - 5000,
|
|
90
|
+
memoryMB: 32,
|
|
91
|
+
port: 19825,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
vi.stubGlobal(
|
|
95
|
+
'fetch',
|
|
96
|
+
vi.fn().mockResolvedValue({
|
|
97
|
+
ok: true,
|
|
98
|
+
json: () => Promise.resolve(status),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await daemonStatus();
|
|
103
|
+
|
|
104
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('daemonStop', () => {
|
|
109
|
+
it('reports "not running" when daemon is unreachable', async () => {
|
|
110
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
111
|
+
|
|
112
|
+
await daemonStop();
|
|
113
|
+
|
|
114
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sends shutdown and reports success', async () => {
|
|
118
|
+
const statusResponse = {
|
|
119
|
+
ok: true,
|
|
120
|
+
json: () =>
|
|
121
|
+
Promise.resolve({
|
|
122
|
+
ok: true,
|
|
123
|
+
pid: 12345,
|
|
124
|
+
uptime: 100,
|
|
125
|
+
extensionConnected: true,
|
|
126
|
+
pending: 0,
|
|
127
|
+
lastCliRequestTime: Date.now(),
|
|
128
|
+
memoryMB: 50,
|
|
129
|
+
port: 19825,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
const shutdownResponse = { ok: true };
|
|
133
|
+
|
|
134
|
+
const mockFetch = vi.fn()
|
|
135
|
+
.mockResolvedValueOnce(statusResponse)
|
|
136
|
+
.mockResolvedValueOnce(shutdownResponse);
|
|
137
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
138
|
+
|
|
139
|
+
await daemonStop();
|
|
140
|
+
|
|
141
|
+
// Verify shutdown was called with POST
|
|
142
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
143
|
+
const shutdownCall = mockFetch.mock.calls[1];
|
|
144
|
+
expect(shutdownCall[0]).toContain('/shutdown');
|
|
145
|
+
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
146
|
+
|
|
147
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('reports failure when shutdown request fails', async () => {
|
|
151
|
+
const statusResponse = {
|
|
152
|
+
ok: true,
|
|
153
|
+
json: () =>
|
|
154
|
+
Promise.resolve({
|
|
155
|
+
ok: true,
|
|
156
|
+
pid: 12345,
|
|
157
|
+
uptime: 100,
|
|
158
|
+
extensionConnected: true,
|
|
159
|
+
pending: 0,
|
|
160
|
+
lastCliRequestTime: Date.now(),
|
|
161
|
+
memoryMB: 50,
|
|
162
|
+
port: 19825,
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
const shutdownResponse = { ok: false };
|
|
166
|
+
|
|
167
|
+
const mockFetch = vi.fn()
|
|
168
|
+
.mockResolvedValueOnce(statusResponse)
|
|
169
|
+
.mockResolvedValueOnce(shutdownResponse);
|
|
170
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
171
|
+
|
|
172
|
+
await daemonStop();
|
|
173
|
+
|
|
174
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('daemonRestart', () => {
|
|
179
|
+
const statusData = {
|
|
180
|
+
ok: true,
|
|
181
|
+
pid: 12345,
|
|
182
|
+
uptime: 100,
|
|
183
|
+
extensionConnected: true,
|
|
184
|
+
pending: 0,
|
|
185
|
+
lastCliRequestTime: Date.now(),
|
|
186
|
+
memoryMB: 50,
|
|
187
|
+
port: 19825,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
it('starts daemon directly when not running', async () => {
|
|
191
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
192
|
+
mockConnect.mockResolvedValue(undefined);
|
|
193
|
+
|
|
194
|
+
await daemonRestart();
|
|
195
|
+
|
|
196
|
+
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
197
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('stops then starts when daemon is running', async () => {
|
|
201
|
+
const mockFetch = vi.fn()
|
|
202
|
+
// First call: fetchStatus in daemonRestart — daemon is running
|
|
203
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
204
|
+
// Second call: requestShutdown — success
|
|
205
|
+
.mockResolvedValueOnce({ ok: true })
|
|
206
|
+
// Subsequent calls: polling fetchStatus until unreachable
|
|
207
|
+
.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
208
|
+
|
|
209
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
210
|
+
mockConnect.mockResolvedValue(undefined);
|
|
211
|
+
|
|
212
|
+
await daemonRestart();
|
|
213
|
+
|
|
214
|
+
// Verify shutdown was called
|
|
215
|
+
const shutdownCall = mockFetch.mock.calls[1];
|
|
216
|
+
expect(shutdownCall[0]).toContain('/shutdown');
|
|
217
|
+
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
218
|
+
|
|
219
|
+
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
220
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('aborts when shutdown fails', async () => {
|
|
224
|
+
const mockFetch = vi.fn()
|
|
225
|
+
// fetchStatus — daemon is running
|
|
226
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
227
|
+
// requestShutdown — failure
|
|
228
|
+
.mockResolvedValueOnce({ ok: false });
|
|
229
|
+
|
|
230
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
231
|
+
|
|
232
|
+
await daemonRestart();
|
|
233
|
+
|
|
234
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
|
|
235
|
+
expect(mockConnect).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for daemon lifecycle management:
|
|
3
|
+
* opencli daemon status — show daemon state
|
|
4
|
+
* opencli daemon stop — graceful shutdown
|
|
5
|
+
* opencli daemon restart — stop + respawn
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
10
|
+
|
|
11
|
+
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
12
|
+
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
13
|
+
|
|
14
|
+
interface DaemonStatus {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
pid: number;
|
|
17
|
+
uptime: number;
|
|
18
|
+
extensionConnected: boolean;
|
|
19
|
+
pending: number;
|
|
20
|
+
lastCliRequestTime: number;
|
|
21
|
+
memoryMB: number;
|
|
22
|
+
port: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fetchStatus(): Promise<DaemonStatus | null> {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
30
|
+
headers: { 'X-OpenCLI': '1' },
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
return await res.json() as DaemonStatus;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function requestShutdown(): Promise<boolean> {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`${DAEMON_URL}/shutdown`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'X-OpenCLI': '1' },
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
return res.ok;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatUptime(seconds: number): string {
|
|
60
|
+
const h = Math.floor(seconds / 3600);
|
|
61
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
62
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
63
|
+
if (m > 0) return `${m}m`;
|
|
64
|
+
return `${Math.floor(seconds)}s`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatTimeSince(timestampMs: number): string {
|
|
68
|
+
const seconds = (Date.now() - timestampMs) / 1000;
|
|
69
|
+
if (seconds < 60) return `${Math.floor(seconds)}s ago`;
|
|
70
|
+
const m = Math.floor(seconds / 60);
|
|
71
|
+
if (m < 60) return `${m} min ago`;
|
|
72
|
+
const h = Math.floor(m / 60);
|
|
73
|
+
return `${h}h ${m % 60}m ago`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function daemonStatus(): Promise<void> {
|
|
77
|
+
const status = await fetchStatus();
|
|
78
|
+
if (!status) {
|
|
79
|
+
console.log(`Daemon: ${chalk.dim('not running')}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
|
|
84
|
+
console.log(`Uptime: ${formatUptime(status.uptime)}`);
|
|
85
|
+
console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
|
|
86
|
+
console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
|
|
87
|
+
console.log(`Memory: ${status.memoryMB} MB`);
|
|
88
|
+
console.log(`Port: ${status.port}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function daemonStop(): Promise<void> {
|
|
92
|
+
const status = await fetchStatus();
|
|
93
|
+
if (!status) {
|
|
94
|
+
console.log(chalk.dim('Daemon is not running.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ok = await requestShutdown();
|
|
99
|
+
if (ok) {
|
|
100
|
+
console.log(chalk.green('Daemon stopped.'));
|
|
101
|
+
} else {
|
|
102
|
+
console.error(chalk.red('Failed to stop daemon.'));
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function daemonRestart(): Promise<void> {
|
|
108
|
+
const status = await fetchStatus();
|
|
109
|
+
if (status) {
|
|
110
|
+
const ok = await requestShutdown();
|
|
111
|
+
if (!ok) {
|
|
112
|
+
console.error(chalk.red('Failed to stop daemon.'));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Wait for daemon to actually exit (poll until unreachable)
|
|
117
|
+
const deadline = Date.now() + 5000;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
await new Promise(r => setTimeout(r, 200));
|
|
120
|
+
if (!(await fetchStatus())) break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Import BrowserBridge to spawn a new daemon
|
|
125
|
+
const { BrowserBridge } = await import('../browser/mcp.js');
|
|
126
|
+
const bridge = new BrowserBridge();
|
|
127
|
+
try {
|
|
128
|
+
console.log('Starting daemon...');
|
|
129
|
+
await bridge.connect({ timeout: 10 });
|
|
130
|
+
console.log(chalk.green('Daemon restarted.'));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(chalk.red(`Failed to restart daemon: ${err instanceof Error ? err.message : err}`));
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/completion.ts
CHANGED
|
@@ -57,9 +57,10 @@ export function getCompletions(words: string[], cursor: number): string[] {
|
|
|
57
57
|
for (const [, cmd] of getRegistry()) {
|
|
58
58
|
if (cmd.site === site) {
|
|
59
59
|
subcommands.push(cmd.name);
|
|
60
|
+
if (cmd.aliases?.length) subcommands.push(...cmd.aliases);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
return subcommands.sort();
|
|
63
|
+
return [...new Set(subcommands)].sort();
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// cursor >= 3 → no further completion
|
package/src/constants.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
/** Default daemon port for HTTP/WebSocket communication with browser extension */
|
|
6
6
|
export const DEFAULT_DAEMON_PORT = 19825;
|
|
7
7
|
|
|
8
|
+
/** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
|
|
9
|
+
export const DEFAULT_DAEMON_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
|
|
10
|
+
|
|
8
11
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
9
12
|
export const VOLATILE_PARAMS = new Set([
|
|
10
13
|
'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { IdleManager } from './idle-manager.js';
|
|
3
|
+
|
|
4
|
+
describe('IdleManager', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not start timer when extension is connected', () => {
|
|
14
|
+
const exit = vi.fn();
|
|
15
|
+
const mgr = new IdleManager(300_000, exit);
|
|
16
|
+
|
|
17
|
+
mgr.setExtensionConnected(true);
|
|
18
|
+
mgr.onCliRequest();
|
|
19
|
+
|
|
20
|
+
vi.advanceTimersByTime(300_000 + 1000);
|
|
21
|
+
expect(exit).not.toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('starts timer when extension disconnects and CLI is idle', () => {
|
|
25
|
+
const exit = vi.fn();
|
|
26
|
+
const mgr = new IdleManager(300_000, exit);
|
|
27
|
+
|
|
28
|
+
mgr.onCliRequest();
|
|
29
|
+
mgr.setExtensionConnected(true);
|
|
30
|
+
mgr.setExtensionConnected(false);
|
|
31
|
+
|
|
32
|
+
expect(exit).not.toHaveBeenCalled();
|
|
33
|
+
|
|
34
|
+
vi.advanceTimersByTime(300_000 + 1000);
|
|
35
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exits immediately on extension disconnect if CLI has been idle past timeout', () => {
|
|
39
|
+
const exit = vi.fn();
|
|
40
|
+
const mgr = new IdleManager(300_000, exit);
|
|
41
|
+
|
|
42
|
+
mgr.onCliRequest();
|
|
43
|
+
mgr.setExtensionConnected(true); // connect before timeout elapses
|
|
44
|
+
vi.advanceTimersByTime(400_000); // CLI idle time exceeds timeout, but extension is connected so no exit
|
|
45
|
+
|
|
46
|
+
expect(exit).not.toHaveBeenCalled();
|
|
47
|
+
|
|
48
|
+
mgr.setExtensionConnected(false); // disconnect → should exit immediately since CLI idle > timeout
|
|
49
|
+
|
|
50
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('resets timer on new CLI request', () => {
|
|
54
|
+
const exit = vi.fn();
|
|
55
|
+
const mgr = new IdleManager(300_000, exit);
|
|
56
|
+
|
|
57
|
+
mgr.onCliRequest();
|
|
58
|
+
vi.advanceTimersByTime(200_000);
|
|
59
|
+
mgr.onCliRequest();
|
|
60
|
+
|
|
61
|
+
vi.advanceTimersByTime(200_000);
|
|
62
|
+
expect(exit).not.toHaveBeenCalled();
|
|
63
|
+
|
|
64
|
+
vi.advanceTimersByTime(100_001);
|
|
65
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not exit when timeout is 0 (disabled)', () => {
|
|
69
|
+
const exit = vi.fn();
|
|
70
|
+
const mgr = new IdleManager(0, exit);
|
|
71
|
+
|
|
72
|
+
mgr.onCliRequest();
|
|
73
|
+
vi.advanceTimersByTime(24 * 60 * 60 * 1000);
|
|
74
|
+
expect(exit).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('clears timer when extension connects', () => {
|
|
78
|
+
const exit = vi.fn();
|
|
79
|
+
const mgr = new IdleManager(300_000, exit);
|
|
80
|
+
|
|
81
|
+
mgr.onCliRequest();
|
|
82
|
+
vi.advanceTimersByTime(200_000);
|
|
83
|
+
|
|
84
|
+
mgr.setExtensionConnected(true);
|
|
85
|
+
vi.advanceTimersByTime(200_000);
|
|
86
|
+
expect(exit).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/daemon.ts
CHANGED
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Lifecycle:
|
|
17
17
|
* - Auto-spawned by opencli on first browser command
|
|
18
|
-
* - Auto-exits after
|
|
18
|
+
* - Auto-exits after idle timeout (default 4h, configurable via OPENCLI_DAEMON_TIMEOUT)
|
|
19
19
|
* - Listens on localhost:19825
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
23
23
|
import { WebSocketServer, WebSocket, type RawData } from 'ws';
|
|
24
|
-
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
24
|
+
import { DEFAULT_DAEMON_PORT, DEFAULT_DAEMON_IDLE_TIMEOUT } from './constants.js';
|
|
25
25
|
import { EXIT_CODES } from './errors.js';
|
|
26
|
+
import { IdleManager } from './idle-manager.js';
|
|
26
27
|
|
|
27
28
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
28
|
-
const IDLE_TIMEOUT =
|
|
29
|
+
const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_DAEMON_IDLE_TIMEOUT);
|
|
29
30
|
|
|
30
31
|
// ─── State ───────────────────────────────────────────────────────────
|
|
31
32
|
|
|
@@ -36,8 +37,6 @@ const pending = new Map<string, {
|
|
|
36
37
|
reject: (error: Error) => void;
|
|
37
38
|
timer: ReturnType<typeof setTimeout>;
|
|
38
39
|
}>();
|
|
39
|
-
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
-
|
|
41
40
|
// Extension log ring buffer
|
|
42
41
|
interface LogEntry { level: string; msg: string; ts: number; }
|
|
43
42
|
const LOG_BUFFER_SIZE = 200;
|
|
@@ -50,13 +49,10 @@ function pushLog(entry: LogEntry): void {
|
|
|
50
49
|
|
|
51
50
|
// ─── Idle auto-exit ──────────────────────────────────────────────────
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
58
|
-
}, IDLE_TIMEOUT);
|
|
59
|
-
}
|
|
52
|
+
const idleManager = new IdleManager(IDLE_TIMEOUT, () => {
|
|
53
|
+
console.error('[daemon] Idle timeout (no CLI requests + no Extension), shutting down');
|
|
54
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
55
|
+
});
|
|
60
56
|
|
|
61
57
|
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
62
58
|
|
|
@@ -128,11 +124,18 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
128
124
|
}
|
|
129
125
|
|
|
130
126
|
if (req.method === 'GET' && pathname === '/status') {
|
|
127
|
+
const uptime = process.uptime();
|
|
128
|
+
const mem = process.memoryUsage();
|
|
131
129
|
jsonResponse(res, 200, {
|
|
132
130
|
ok: true,
|
|
131
|
+
pid: process.pid,
|
|
132
|
+
uptime,
|
|
133
133
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
134
134
|
extensionVersion,
|
|
135
135
|
pending: pending.size,
|
|
136
|
+
lastCliRequestTime: idleManager.lastCliRequestTime,
|
|
137
|
+
memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
|
138
|
+
port: PORT,
|
|
136
139
|
});
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
@@ -153,8 +156,14 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
153
156
|
return;
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
if (req.method === 'POST' && pathname === '/shutdown') {
|
|
160
|
+
jsonResponse(res, 200, { ok: true, message: 'Shutting down' });
|
|
161
|
+
setTimeout(() => shutdown(), 100);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
if (req.method === 'POST' && url === '/command') {
|
|
157
|
-
|
|
166
|
+
idleManager.onCliRequest();
|
|
158
167
|
try {
|
|
159
168
|
const body = JSON.parse(await readBody(req));
|
|
160
169
|
if (!body.id) {
|
|
@@ -212,6 +221,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
212
221
|
console.error('[daemon] Extension connected');
|
|
213
222
|
extensionWs = ws;
|
|
214
223
|
extensionVersion = null; // cleared until hello message arrives
|
|
224
|
+
idleManager.setExtensionConnected(true);
|
|
215
225
|
|
|
216
226
|
// ── Heartbeat: ping every 15s, close if 2 pongs missed ──
|
|
217
227
|
let missedPongs = 0;
|
|
@@ -270,6 +280,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
270
280
|
if (extensionWs === ws) {
|
|
271
281
|
extensionWs = null;
|
|
272
282
|
extensionVersion = null;
|
|
283
|
+
idleManager.setExtensionConnected(false);
|
|
273
284
|
// Reject all pending requests since the extension is gone
|
|
274
285
|
for (const [id, p] of pending) {
|
|
275
286
|
clearTimeout(p.timer);
|
|
@@ -284,6 +295,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
284
295
|
if (extensionWs === ws) {
|
|
285
296
|
extensionWs = null;
|
|
286
297
|
extensionVersion = null;
|
|
298
|
+
idleManager.setExtensionConnected(false);
|
|
287
299
|
// Reject pending requests in case 'close' does not follow this 'error'
|
|
288
300
|
for (const [, p] of pending) {
|
|
289
301
|
clearTimeout(p.timer);
|
|
@@ -298,7 +310,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
298
310
|
|
|
299
311
|
httpServer.listen(PORT, '127.0.0.1', () => {
|
|
300
312
|
console.error(`[daemon] Listening on http://127.0.0.1:${PORT}`);
|
|
301
|
-
|
|
313
|
+
idleManager.onCliRequest();
|
|
302
314
|
});
|
|
303
315
|
|
|
304
316
|
httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
package/src/discovery.ts
CHANGED
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as os from 'node:os';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
|
-
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
15
|
import yaml from 'js-yaml';
|
|
16
16
|
import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerCommand } from './registry.js';
|
|
17
17
|
import { getErrorMessage } from './errors.js';
|
|
18
18
|
import { log } from './logger.js';
|
|
19
19
|
import type { ManifestEntry } from './build-manifest.js';
|
|
20
20
|
|
|
21
|
+
/** User runtime directory: ~/.opencli */
|
|
22
|
+
export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
|
|
23
|
+
/** User CLIs directory: ~/.opencli/clis */
|
|
24
|
+
export const USER_CLIS_DIR = path.join(USER_OPENCLI_DIR, 'clis');
|
|
21
25
|
/** Plugins directory: ~/.opencli/plugins/ */
|
|
22
|
-
export const PLUGINS_DIR = path.join(
|
|
26
|
+
export const PLUGINS_DIR = path.join(USER_OPENCLI_DIR, 'plugins');
|
|
23
27
|
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
24
28
|
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
25
29
|
|
|
@@ -33,6 +37,47 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
|
|
|
33
37
|
|
|
34
38
|
import { isRecord } from './utils.js';
|
|
35
39
|
|
|
40
|
+
function resolveHostRuntimeModulePath(moduleName: string): string {
|
|
41
|
+
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
for (const ext of ['.js', '.ts']) {
|
|
43
|
+
const candidate = path.join(runtimeDir, `${moduleName}${ext}`);
|
|
44
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
45
|
+
}
|
|
46
|
+
return path.join(runtimeDir, `${moduleName}.js`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function writeCompatShimIfNeeded(filePath: string, content: string): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
const existing = await fs.promises.readFile(filePath, 'utf-8');
|
|
52
|
+
if (existing === content) return;
|
|
53
|
+
} catch {
|
|
54
|
+
// Fall through to write missing shim
|
|
55
|
+
}
|
|
56
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
|
|
61
|
+
* importing ../../registry(.js) and ../../errors(.js).
|
|
62
|
+
*/
|
|
63
|
+
export async function ensureUserCliCompatShims(baseDir: string = USER_OPENCLI_DIR): Promise<void> {
|
|
64
|
+
await fs.promises.mkdir(baseDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const registryUrl = pathToFileURL(resolveHostRuntimeModulePath('registry-api')).href;
|
|
67
|
+
const errorsUrl = pathToFileURL(resolveHostRuntimeModulePath('errors')).href;
|
|
68
|
+
|
|
69
|
+
await Promise.all([
|
|
70
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'registry'), `export * from '${registryUrl}';\n`),
|
|
71
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'registry.js'), `export * from '${registryUrl}';\n`),
|
|
72
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'errors'), `export * from '${errorsUrl}';\n`),
|
|
73
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'errors.js'), `export * from '${errorsUrl}';\n`),
|
|
74
|
+
writeCompatShimIfNeeded(
|
|
75
|
+
path.join(baseDir, 'package.json'),
|
|
76
|
+
`${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`,
|
|
77
|
+
),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
/**
|
|
37
82
|
* Discover and register CLI commands.
|
|
38
83
|
* Uses pre-compiled manifest when available for instant startup.
|
|
@@ -68,6 +113,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
68
113
|
const cmd: CliCommand = {
|
|
69
114
|
site: entry.site,
|
|
70
115
|
name: entry.name,
|
|
116
|
+
aliases: entry.aliases,
|
|
71
117
|
description: entry.description ?? '',
|
|
72
118
|
domain: entry.domain,
|
|
73
119
|
strategy,
|
|
@@ -90,6 +136,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
90
136
|
const cmd: InternalCliCommand = {
|
|
91
137
|
site: entry.site,
|
|
92
138
|
name: entry.name,
|
|
139
|
+
aliases: entry.aliases,
|
|
93
140
|
description: entry.description ?? '',
|
|
94
141
|
domain: entry.domain,
|
|
95
142
|
strategy,
|
|
@@ -163,6 +210,9 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
163
210
|
const cmd: CliCommand = {
|
|
164
211
|
site,
|
|
165
212
|
name,
|
|
213
|
+
aliases: isRecord(cliDef) && Array.isArray((cliDef as Record<string, unknown>).aliases)
|
|
214
|
+
? ((cliDef as Record<string, unknown>).aliases as unknown[]).filter((value): value is string => typeof value === 'string')
|
|
215
|
+
: undefined,
|
|
166
216
|
description: cliDef.description ?? '',
|
|
167
217
|
domain: cliDef.domain,
|
|
168
218
|
strategy,
|